@nalvietnam/avatar-cli 1.2.2 → 1.2.4
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
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/ai.ts
|
|
7
|
-
import { spawnSync as spawnSync4 } from "child_process";
|
|
8
7
|
import { promises as fs4 } from "fs";
|
|
9
8
|
import { join as join5 } from "path";
|
|
10
9
|
import { confirm } from "@inquirer/prompts";
|
|
@@ -654,6 +653,102 @@ async function runAiSetupPhase(args) {
|
|
|
654
653
|
}
|
|
655
654
|
}
|
|
656
655
|
|
|
656
|
+
// src/lib/test-ai-provider-by-detected-mode.ts
|
|
657
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
658
|
+
var FETCH_TIMEOUT_MS2 = 1e4;
|
|
659
|
+
var CLAUDE_PRINT_TIMEOUT_MS = 3e4;
|
|
660
|
+
var TEST_CHAT_MAX_TOKENS = 5;
|
|
661
|
+
var TEST_CHAT_PROMPT = "say ok";
|
|
662
|
+
async function testLLMLiteProvider(baseUrl, token, model) {
|
|
663
|
+
log.info(`Testing LLMLite provider: ${baseUrl} (key: ${maskApiKey(token)})`);
|
|
664
|
+
const controller = new AbortController();
|
|
665
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
|
|
666
|
+
try {
|
|
667
|
+
const modelsRes = await fetch(`${baseUrl}/v1/models`, {
|
|
668
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
669
|
+
signal: controller.signal
|
|
670
|
+
});
|
|
671
|
+
if (modelsRes.status === 401 || modelsRes.status === 403) {
|
|
672
|
+
throw new Error(`API key invalid (HTTP ${modelsRes.status}). Re-run: avatar ai setup`);
|
|
673
|
+
}
|
|
674
|
+
if (!modelsRes.ok) {
|
|
675
|
+
throw new Error(`Endpoint /v1/models l\u1ED7i (HTTP ${modelsRes.status}).`);
|
|
676
|
+
}
|
|
677
|
+
const modelsJson = await modelsRes.json();
|
|
678
|
+
const models = (modelsJson.data || []).map((m) => typeof m.id === "string" ? m.id : null).filter((id) => id !== null);
|
|
679
|
+
log.success(`Connectivity OK \xB7 ${models.length} models available`);
|
|
680
|
+
if (models.length > 0) {
|
|
681
|
+
const preview = models.slice(0, 5).join(", ");
|
|
682
|
+
const more = models.length > 5 ? ` ...+${models.length - 5} more` : "";
|
|
683
|
+
log.dim(` Models: ${preview}${more}`);
|
|
684
|
+
}
|
|
685
|
+
log.info(`Testing chat completion v\u1EDBi model "${model}"...`);
|
|
686
|
+
const chatRes = await fetch(`${baseUrl}/v1/chat/completions`, {
|
|
687
|
+
method: "POST",
|
|
688
|
+
headers: {
|
|
689
|
+
Authorization: `Bearer ${token}`,
|
|
690
|
+
"Content-Type": "application/json"
|
|
691
|
+
},
|
|
692
|
+
body: JSON.stringify({
|
|
693
|
+
model,
|
|
694
|
+
messages: [{ role: "user", content: TEST_CHAT_PROMPT }],
|
|
695
|
+
max_tokens: TEST_CHAT_MAX_TOKENS
|
|
696
|
+
}),
|
|
697
|
+
signal: controller.signal
|
|
698
|
+
});
|
|
699
|
+
if (!chatRes.ok) {
|
|
700
|
+
const errBody = (await chatRes.text()).slice(0, 200);
|
|
701
|
+
throw new Error(`Chat completion fail (HTTP ${chatRes.status}). ${errBody}`);
|
|
702
|
+
}
|
|
703
|
+
const chatJson = await chatRes.json();
|
|
704
|
+
const reply = typeof chatJson.choices?.[0]?.message?.content === "string" ? chatJson.choices[0].message.content : "(empty response)";
|
|
705
|
+
const tokens = chatJson.usage?.total_tokens ?? "?";
|
|
706
|
+
log.success(`Response: "${String(reply).trim().slice(0, 100)}"`);
|
|
707
|
+
log.dim(` Tokens used: ${tokens}`);
|
|
708
|
+
} catch (err) {
|
|
709
|
+
if (err.name === "AbortError") {
|
|
710
|
+
throw new Error(`Timeout ${FETCH_TIMEOUT_MS2 / 1e3}s. Check m\u1EA1ng / endpoint ${baseUrl}.`);
|
|
711
|
+
}
|
|
712
|
+
throw err;
|
|
713
|
+
} finally {
|
|
714
|
+
clearTimeout(timer);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function testSubscriptionProvider() {
|
|
718
|
+
log.info("Testing Subscription provider qua `claude --print`...");
|
|
719
|
+
const result = spawnSync4("claude", ["--print", TEST_CHAT_PROMPT], {
|
|
720
|
+
encoding: "utf8",
|
|
721
|
+
timeout: CLAUDE_PRINT_TIMEOUT_MS
|
|
722
|
+
});
|
|
723
|
+
if (result.signal === "SIGTERM") {
|
|
724
|
+
throw new Error(`Timeout ${CLAUDE_PRINT_TIMEOUT_MS / 1e3}s. Check m\u1EA1ng / endpoint.`);
|
|
725
|
+
}
|
|
726
|
+
if (result.status !== 0) {
|
|
727
|
+
const stderr = (result.stderr || "").toLowerCase();
|
|
728
|
+
if (stderr.includes("401") || stderr.includes("invalid authentication") || stderr.includes("unauthorized")) {
|
|
729
|
+
throw new Error(
|
|
730
|
+
"Token Claude Code stale (401). Fix: `claude auth logout && claude auth login`."
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
throw new Error(
|
|
734
|
+
`Test fail (exit ${result.status}). Stderr: ${(result.stderr || "").slice(0, 200)}`
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
log.success(`Response: "${(result.stdout || "").trim().slice(0, 100)}"`);
|
|
738
|
+
}
|
|
739
|
+
async function testAiProviderByDetectedMode(settings) {
|
|
740
|
+
const env = settings.env || {};
|
|
741
|
+
const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
|
|
742
|
+
const token = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : void 0;
|
|
743
|
+
const model = typeof settings.model === "string" ? settings.model : "default";
|
|
744
|
+
if (baseUrl && token) {
|
|
745
|
+
await testLLMLiteProvider(baseUrl, token, model);
|
|
746
|
+
return { ok: true, provider: "llmlite", message: "LLMLite provider working" };
|
|
747
|
+
}
|
|
748
|
+
testSubscriptionProvider();
|
|
749
|
+
return { ok: true, provider: "subscription", message: "Subscription provider working" };
|
|
750
|
+
}
|
|
751
|
+
|
|
657
752
|
// src/commands/ai.ts
|
|
658
753
|
async function ensureWorkspaceCwd() {
|
|
659
754
|
const cwd = process.cwd();
|
|
@@ -691,24 +786,15 @@ async function runAiStatus() {
|
|
|
691
786
|
log.info(`Token: ${token ? maskApiKey(token) : "(kh\xF4ng set \u2014 d\xF9ng subscription auth)"}`);
|
|
692
787
|
}
|
|
693
788
|
async function runAiTest() {
|
|
694
|
-
await ensureWorkspaceCwd();
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
})
|
|
700
|
-
|
|
701
|
-
log.error("Timeout sau 30s. Check m\u1EA1ng / endpoint.");
|
|
702
|
-
process.exit(1);
|
|
703
|
-
}
|
|
704
|
-
if (result.status !== 0) {
|
|
705
|
-
log.error(`Test th\u1EA5t b\u1EA1i (exit ${result.status}).`);
|
|
706
|
-
if (result.stderr) process.stderr.write(`${result.stderr}
|
|
707
|
-
`);
|
|
789
|
+
const workspacePath = await ensureWorkspaceCwd();
|
|
790
|
+
const settings = await readWorkspaceSettings(workspacePath);
|
|
791
|
+
try {
|
|
792
|
+
const result = await testAiProviderByDetectedMode(settings);
|
|
793
|
+
log.success(`\u2713 ${result.message}`);
|
|
794
|
+
} catch (err) {
|
|
795
|
+
log.error(`Test fail: ${err.message}`);
|
|
708
796
|
process.exit(1);
|
|
709
797
|
}
|
|
710
|
-
log.success(`Response: ${(result.stdout || "").trim().slice(0, 200)}`);
|
|
711
|
-
log.success("AI provider working");
|
|
712
798
|
}
|
|
713
799
|
async function runAiReset(opts) {
|
|
714
800
|
const workspacePath = await ensureWorkspaceCwd();
|
|
@@ -1108,7 +1194,7 @@ async function applyFixes(checks) {
|
|
|
1108
1194
|
|
|
1109
1195
|
// src/commands/init.ts
|
|
1110
1196
|
import { basename, join as join16, relative as relative2, resolve } from "path";
|
|
1111
|
-
import { confirm as
|
|
1197
|
+
import { confirm as confirm3, input as input2, select as select6 } from "@inquirer/prompts";
|
|
1112
1198
|
import boxen3 from "boxen";
|
|
1113
1199
|
|
|
1114
1200
|
// src/lib/avatar-ascii-banner.ts
|
|
@@ -1257,7 +1343,7 @@ function createGithubRemoteFromFolder(input3) {
|
|
|
1257
1343
|
}
|
|
1258
1344
|
|
|
1259
1345
|
// src/lib/create-workspace-remote-via-gh.ts
|
|
1260
|
-
import { spawnSync as
|
|
1346
|
+
import { spawnSync as spawnSync15 } from "child_process";
|
|
1261
1347
|
|
|
1262
1348
|
// src/lib/check-gh-cli-auth-status.ts
|
|
1263
1349
|
import { spawnSync as spawnSync8 } from "child_process";
|
|
@@ -1290,8 +1376,124 @@ function detectPackageManager() {
|
|
|
1290
1376
|
return null;
|
|
1291
1377
|
}
|
|
1292
1378
|
|
|
1293
|
-
// src/lib/
|
|
1379
|
+
// src/lib/handle-remote-access-failure-with-account-switch.ts
|
|
1380
|
+
import { spawnSync as spawnSync11 } from "child_process";
|
|
1381
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
1382
|
+
|
|
1383
|
+
// src/lib/verify-git-remote-accessible.ts
|
|
1294
1384
|
import { spawnSync as spawnSync10 } from "child_process";
|
|
1385
|
+
var TIMEOUT_MS = 5e3;
|
|
1386
|
+
function classifyRemoteError(stderr) {
|
|
1387
|
+
const text = stderr.toLowerCase();
|
|
1388
|
+
if (text.includes("authentication") || text.includes("could not read username") || text.includes("permission denied") || text.includes("403") || text.includes("access denied") || text.includes("repository not found")) {
|
|
1389
|
+
return "no-access";
|
|
1390
|
+
}
|
|
1391
|
+
if (text.includes("404") || text.includes("does not exist")) {
|
|
1392
|
+
return "not-found";
|
|
1393
|
+
}
|
|
1394
|
+
if (text.includes("could not resolve host") || text.includes("network") || text.includes("connection refused") || text.includes("connection timed out")) {
|
|
1395
|
+
return "network";
|
|
1396
|
+
}
|
|
1397
|
+
return "unknown";
|
|
1398
|
+
}
|
|
1399
|
+
function tryVerifyGitRemoteAccessible(url) {
|
|
1400
|
+
const r = spawnSync10("git", ["ls-remote", "--exit-code", url, "HEAD"], {
|
|
1401
|
+
encoding: "utf8",
|
|
1402
|
+
timeout: TIMEOUT_MS,
|
|
1403
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1404
|
+
});
|
|
1405
|
+
if (r.status === 0) return { ok: true };
|
|
1406
|
+
if (r.signal === "SIGTERM") {
|
|
1407
|
+
return { ok: false, reason: "timeout", detail: "git ls-remote > 5s" };
|
|
1408
|
+
}
|
|
1409
|
+
const stderr = (r.stderr || "").trim();
|
|
1410
|
+
const reason = classifyRemoteError(stderr);
|
|
1411
|
+
return { ok: false, reason, detail: stderr.slice(0, 300) };
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// src/lib/handle-remote-access-failure-with-account-switch.ts
|
|
1415
|
+
var RemoteAccessAbortedError = class extends Error {
|
|
1416
|
+
constructor(message) {
|
|
1417
|
+
super(message);
|
|
1418
|
+
this.name = "RemoteAccessAbortedError";
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
function getCurrentGhUser() {
|
|
1422
|
+
const r = spawnSync11("gh", ["api", "user", "--jq", ".login"], {
|
|
1423
|
+
encoding: "utf8",
|
|
1424
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1425
|
+
});
|
|
1426
|
+
if (r.status !== 0) return null;
|
|
1427
|
+
return r.stdout.trim() || null;
|
|
1428
|
+
}
|
|
1429
|
+
function triggerGhAuthLoginInteractive() {
|
|
1430
|
+
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
1431
|
+
const r = spawnSync11("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
1432
|
+
if (r.status !== 0) {
|
|
1433
|
+
log.warn(`gh auth login exit ${r.status}. B\u1EA1n c\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
function getReasonHint(reason, url, ghUser) {
|
|
1437
|
+
switch (reason) {
|
|
1438
|
+
case "no-access":
|
|
1439
|
+
return ghUser ? `Repo c\xF3 th\u1EC3 private v\xE0 account '${ghUser}' kh\xF4ng c\xF3 quy\u1EC1n access. Switch sang account c\xF3 quy\u1EC1n, ho\u1EB7c xin invite t\u1EEB owner repo.` : "Repo c\xF3 th\u1EC3 private v\xE0 gh CLI ch\u01B0a login. Login tr\u01B0\u1EDBc r\u1ED3i retry.";
|
|
1440
|
+
case "not-found":
|
|
1441
|
+
return `URL c\xF3 th\u1EC3 sai ch\xEDnh t\u1EA3, ho\u1EB7c repo \u0111\xE3 b\u1ECB x\xF3a/rename. Check: ${url}`;
|
|
1442
|
+
case "network":
|
|
1443
|
+
return "Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c GitHub. Check m\u1EA1ng / VPN / firewall.";
|
|
1444
|
+
case "timeout":
|
|
1445
|
+
return "Network ch\u1EADm > 5s. Check m\u1EA1ng r\u1ED3i retry.";
|
|
1446
|
+
default:
|
|
1447
|
+
return "L\u1ED7i kh\xF4ng x\xE1c \u0111\u1ECBnh. Check URL + gh auth status.";
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
async function handleRemoteAccessFailureWithAccountSwitch(args) {
|
|
1451
|
+
let reason = args.initialReason;
|
|
1452
|
+
let detail = args.initialDetail;
|
|
1453
|
+
while (true) {
|
|
1454
|
+
const ghUser = getCurrentGhUser();
|
|
1455
|
+
log.warn(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c ${args.url}`);
|
|
1456
|
+
log.dim(` L\xFD do: ${reason}${detail ? ` \u2014 ${detail.slice(0, 150)}` : ""}`);
|
|
1457
|
+
log.info(getReasonHint(reason, args.url, ghUser));
|
|
1458
|
+
if (ghUser) log.dim(` gh CLI hi\u1EC7n \u0111ang login: ${ghUser}`);
|
|
1459
|
+
const action = await select3({
|
|
1460
|
+
message: "C\xE1ch x\u1EED l\xFD?",
|
|
1461
|
+
choices: [
|
|
1462
|
+
{
|
|
1463
|
+
name: "Switch GitHub account (gh auth login \u2014 m\u1EDF browser)",
|
|
1464
|
+
value: "switch"
|
|
1465
|
+
},
|
|
1466
|
+
{
|
|
1467
|
+
name: "T\xF4i v\u1EEBa fix (accept invite / s\u1EEDa URL) \u2014 retry verify",
|
|
1468
|
+
value: "retry"
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
name: "T\u1EA1m ng\u01B0ng init \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau",
|
|
1472
|
+
value: "abort"
|
|
1473
|
+
}
|
|
1474
|
+
]
|
|
1475
|
+
});
|
|
1476
|
+
if (action === "abort") {
|
|
1477
|
+
throw new RemoteAccessAbortedError(
|
|
1478
|
+
`User ch\u1ECDn t\u1EA1m ng\u01B0ng. Fix access ${args.url} r\u1ED3i ch\u1EA1y l\u1EA1i 'avatar init'.`
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
if (action === "switch") {
|
|
1482
|
+
triggerGhAuthLoginInteractive();
|
|
1483
|
+
}
|
|
1484
|
+
log.info("Verify remote l\u1EA1i...");
|
|
1485
|
+
const result = tryVerifyGitRemoteAccessible(args.url);
|
|
1486
|
+
if (result.ok) {
|
|
1487
|
+
log.success(`Remote accessible: ${args.url}`);
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
reason = result.reason ?? "unknown";
|
|
1491
|
+
detail = result.detail;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// src/lib/install-gh-cli-via-package-manager.ts
|
|
1496
|
+
import { spawnSync as spawnSync12 } from "child_process";
|
|
1295
1497
|
var INSTALL_COMMANDS = {
|
|
1296
1498
|
brew: { cmd: "brew", args: ["install", "gh"] },
|
|
1297
1499
|
apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
|
|
@@ -1302,7 +1504,7 @@ var INSTALL_COMMANDS = {
|
|
|
1302
1504
|
function installGhCliViaPackageManager(pm) {
|
|
1303
1505
|
const spec = INSTALL_COMMANDS[pm];
|
|
1304
1506
|
log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
|
|
1305
|
-
const r =
|
|
1507
|
+
const r = spawnSync12(spec.cmd, spec.args, { stdio: "inherit" });
|
|
1306
1508
|
if (r.status !== 0) {
|
|
1307
1509
|
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.`);
|
|
1308
1510
|
}
|
|
@@ -1310,9 +1512,9 @@ function installGhCliViaPackageManager(pm) {
|
|
|
1310
1512
|
}
|
|
1311
1513
|
|
|
1312
1514
|
// src/lib/setup-git-credential-via-gh.ts
|
|
1313
|
-
import { spawnSync as
|
|
1515
|
+
import { spawnSync as spawnSync13 } from "child_process";
|
|
1314
1516
|
function setupGitCredentialViaGh() {
|
|
1315
|
-
const r =
|
|
1517
|
+
const r = spawnSync13("gh", ["auth", "setup-git"], { stdio: "ignore" });
|
|
1316
1518
|
if (r.status !== 0) {
|
|
1317
1519
|
log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
|
|
1318
1520
|
return;
|
|
@@ -1321,10 +1523,10 @@ function setupGitCredentialViaGh() {
|
|
|
1321
1523
|
}
|
|
1322
1524
|
|
|
1323
1525
|
// src/lib/trigger-gh-cli-auth-login.ts
|
|
1324
|
-
import { spawnSync as
|
|
1526
|
+
import { spawnSync as spawnSync14 } from "child_process";
|
|
1325
1527
|
function triggerGhCliAuthLogin() {
|
|
1326
1528
|
log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
|
|
1327
|
-
const r =
|
|
1529
|
+
const r = spawnSync14(
|
|
1328
1530
|
"gh",
|
|
1329
1531
|
["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
|
|
1330
1532
|
{ stdio: "inherit" }
|
|
@@ -1335,25 +1537,6 @@ function triggerGhCliAuthLogin() {
|
|
|
1335
1537
|
log.success("\u0110\xE3 \u0111\u0103ng nh\u1EADp GitHub");
|
|
1336
1538
|
}
|
|
1337
1539
|
|
|
1338
|
-
// src/lib/verify-git-remote-accessible.ts
|
|
1339
|
-
import { spawnSync as spawnSync13 } from "child_process";
|
|
1340
|
-
var TIMEOUT_MS = 5e3;
|
|
1341
|
-
var RemoteNotAccessibleError = class extends Error {
|
|
1342
|
-
constructor(url, reason) {
|
|
1343
|
-
super(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c remote ${url}: ${reason}`);
|
|
1344
|
-
this.name = "RemoteNotAccessibleError";
|
|
1345
|
-
}
|
|
1346
|
-
};
|
|
1347
|
-
function verifyGitRemoteAccessible(url) {
|
|
1348
|
-
const r = spawnSync13("git", ["ls-remote", "--exit-code", url, "HEAD"], {
|
|
1349
|
-
stdio: "ignore",
|
|
1350
|
-
timeout: TIMEOUT_MS
|
|
1351
|
-
});
|
|
1352
|
-
if (r.status === 0) return;
|
|
1353
|
-
if (r.signal === "SIGTERM") throw new RemoteNotAccessibleError(url, "timeout 5s");
|
|
1354
|
-
throw new RemoteNotAccessibleError(url, `git ls-remote exit ${r.status}`);
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
1540
|
// src/lib/git-auth-and-install-orchestrator.ts
|
|
1358
1541
|
async function ensureGitHubReady(remoteUrl) {
|
|
1359
1542
|
let state = checkGhCliAuthStatus();
|
|
@@ -1379,20 +1562,58 @@ async function ensureGitHubReady(remoteUrl) {
|
|
|
1379
1562
|
log.success("gh CLI s\u1EB5n s\xE0ng");
|
|
1380
1563
|
setupGitCredentialViaGh();
|
|
1381
1564
|
if (remoteUrl) {
|
|
1382
|
-
|
|
1383
|
-
|
|
1565
|
+
const result = tryVerifyGitRemoteAccessible(remoteUrl);
|
|
1566
|
+
if (result.ok) {
|
|
1567
|
+
log.success(`Remote accessible: ${remoteUrl}`);
|
|
1568
|
+
} else {
|
|
1569
|
+
await handleRemoteAccessFailureWithAccountSwitch({
|
|
1570
|
+
url: remoteUrl,
|
|
1571
|
+
initialReason: result.reason ?? "unknown",
|
|
1572
|
+
initialDetail: result.detail
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1384
1575
|
}
|
|
1385
1576
|
}
|
|
1386
1577
|
|
|
1387
1578
|
// src/lib/create-workspace-remote-via-gh.ts
|
|
1579
|
+
function canCreateInNamespace(org, ghUser) {
|
|
1580
|
+
if (org.toLowerCase() === ghUser.toLowerCase()) return { ok: true };
|
|
1581
|
+
const r = spawnSync15("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
|
|
1582
|
+
stdio: "ignore"
|
|
1583
|
+
});
|
|
1584
|
+
if (r.status === 0) return { ok: true };
|
|
1585
|
+
const orgCheck = spawnSync15("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
|
|
1586
|
+
if (orgCheck.status !== 0) {
|
|
1587
|
+
const userCheck = spawnSync15("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
|
|
1588
|
+
if (userCheck.status === 0) {
|
|
1589
|
+
return {
|
|
1590
|
+
ok: false,
|
|
1591
|
+
reason: `'${org}' l\xE0 personal account kh\xE1c (kh\xF4ng ph\u1EA3i org). B\u1EA1n (${ghUser}) kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi account c\u1EE7a user kh\xE1c. Pass --repo-org=${ghUser} ho\u1EB7c switch gh: gh auth login`
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
return {
|
|
1595
|
+
ok: false,
|
|
1596
|
+
reason: `'${org}' kh\xF4ng t\u1ED3n t\u1EA1i tr\xEAn GitHub. Check ch\xEDnh t\u1EA3 ho\u1EB7c t\u1EA1o org tr\u01B0\u1EDBc.`
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
return {
|
|
1600
|
+
ok: false,
|
|
1601
|
+
reason: `'${ghUser}' kh\xF4ng ph\u1EA3i member c\u1EE7a org '${org}'. Li\xEAn h\u1EC7 admin org \u0111\u1EC3 \u0111\u01B0\u1EE3c invite, ho\u1EB7c pass --repo-org=${ghUser} t\u1EA1o d\u01B0\u1EDBi personal account.`
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1388
1604
|
async function createWorkspaceRemoteViaGh(input3) {
|
|
1389
1605
|
validateRepoName(input3.workspaceName);
|
|
1390
1606
|
validateRepoVisibility(input3.visibility);
|
|
1391
1607
|
await ensureGitHubReady();
|
|
1392
|
-
const
|
|
1608
|
+
const ghUser = resolveGithubUsernameDefault();
|
|
1609
|
+
const org = input3.org ?? ghUser;
|
|
1610
|
+
const namespaceCheck = canCreateInNamespace(org, ghUser);
|
|
1611
|
+
if (!namespaceCheck.ok) {
|
|
1612
|
+
throw new Error(`Kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi '${org}/': ${namespaceCheck.reason}`);
|
|
1613
|
+
}
|
|
1393
1614
|
const fullName = `${org}/${input3.workspaceName}`;
|
|
1394
1615
|
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input3.visibility})...`);
|
|
1395
|
-
const r =
|
|
1616
|
+
const r = spawnSync15(
|
|
1396
1617
|
"gh",
|
|
1397
1618
|
[
|
|
1398
1619
|
"repo",
|
|
@@ -1405,9 +1626,12 @@ async function createWorkspaceRemoteViaGh(input3) {
|
|
|
1405
1626
|
"origin",
|
|
1406
1627
|
"--push"
|
|
1407
1628
|
],
|
|
1408
|
-
{ stdio: "inherit" }
|
|
1629
|
+
{ stdio: ["inherit", "inherit", "pipe"], encoding: "utf8" }
|
|
1409
1630
|
);
|
|
1410
1631
|
if (r.status !== 0) {
|
|
1632
|
+
const stderr = (r.stderr || "").trim();
|
|
1633
|
+
if (stderr) process.stderr.write(`${stderr}
|
|
1634
|
+
`);
|
|
1411
1635
|
throw new Error(
|
|
1412
1636
|
`T\u1EA1o workspace remote th\u1EA5t b\u1EA1i (exit ${r.status}). Workspace v\u1EABn d\xF9ng \u0111\u01B0\u1EE3c local. Setup remote sau qua: gh repo create ${fullName} --${input3.visibility} --source=. --remote=origin --push`
|
|
1413
1637
|
);
|
|
@@ -1420,7 +1644,7 @@ async function createWorkspaceRemoteViaGh(input3) {
|
|
|
1420
1644
|
|
|
1421
1645
|
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
1422
1646
|
import { readdirSync } from "fs";
|
|
1423
|
-
import { select as
|
|
1647
|
+
import { select as select4 } from "@inquirer/prompts";
|
|
1424
1648
|
import { simpleGit as simpleGit3 } from "simple-git";
|
|
1425
1649
|
|
|
1426
1650
|
// src/lib/check-folder-has-git.ts
|
|
@@ -1551,7 +1775,7 @@ async function promptBootstrapStrategy(state, opts) {
|
|
|
1551
1775
|
if (opts.presetStrategy) return opts.presetStrategy;
|
|
1552
1776
|
if (opts.autoYes) return "stash";
|
|
1553
1777
|
if (state === "empty" || state === "clean") return "commit-all";
|
|
1554
|
-
return await
|
|
1778
|
+
return await select4({
|
|
1555
1779
|
message: state === "dirty" ? "Folder c\xF3 changes ch\u01B0a commit. C\xE1ch x\u1EED l\xFD:" : "Folder c\xF3 file ch\u01B0a version. C\xE1ch x\u1EED l\xFD:",
|
|
1556
1780
|
choices: [
|
|
1557
1781
|
{
|
|
@@ -1687,25 +1911,110 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
|
|
|
1687
1911
|
// src/lib/team-pack-submodule-manager.ts
|
|
1688
1912
|
import { join as join14 } from "path";
|
|
1689
1913
|
|
|
1914
|
+
// src/lib/check-team-pack-access-with-retry-loop.ts
|
|
1915
|
+
import { spawnSync as spawnSync16 } from "child_process";
|
|
1916
|
+
import { confirm as confirm2, select as select5 } from "@inquirer/prompts";
|
|
1917
|
+
function parseRepoSlugFromGitUrl(url) {
|
|
1918
|
+
const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
1919
|
+
if (httpsMatch) return httpsMatch[1];
|
|
1920
|
+
return null;
|
|
1921
|
+
}
|
|
1922
|
+
function checkRepoAccess(repoSlug) {
|
|
1923
|
+
const r = spawnSync16("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
|
|
1924
|
+
return r.status === 0;
|
|
1925
|
+
}
|
|
1926
|
+
function getCurrentGhUser2() {
|
|
1927
|
+
const r = spawnSync16("gh", ["api", "user", "--jq", ".login"], {
|
|
1928
|
+
encoding: "utf8",
|
|
1929
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1930
|
+
});
|
|
1931
|
+
if (r.status !== 0) return null;
|
|
1932
|
+
return r.stdout.trim() || null;
|
|
1933
|
+
}
|
|
1934
|
+
async function copyInfoToClipboardWithConsent(info) {
|
|
1935
|
+
const ok = await confirm2({
|
|
1936
|
+
message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
|
|
1937
|
+
default: true
|
|
1938
|
+
});
|
|
1939
|
+
if (!ok) return;
|
|
1940
|
+
try {
|
|
1941
|
+
const { default: clipboardy } = await import("clipboardy");
|
|
1942
|
+
await clipboardy.write(info);
|
|
1943
|
+
log.success("\u0110\xE3 copy v\xE0o clipboard");
|
|
1944
|
+
} catch (err) {
|
|
1945
|
+
log.dim(`Copy clipboard fail: ${err.message}`);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
function buildAccessRequestInfo(ghUser, ssoEmail) {
|
|
1949
|
+
const lines = [
|
|
1950
|
+
"Request access team-ai-pack (NAL)",
|
|
1951
|
+
"",
|
|
1952
|
+
`GitHub username: ${ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)"}`,
|
|
1953
|
+
`NAL email: ${ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)"}`,
|
|
1954
|
+
`Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
|
|
1955
|
+
];
|
|
1956
|
+
return lines.join("\n");
|
|
1957
|
+
}
|
|
1958
|
+
async function ensureTeamPackAccessWithRetry(args) {
|
|
1959
|
+
if (checkRepoAccess(args.repoSlug)) return true;
|
|
1960
|
+
const ghUser = getCurrentGhUser2();
|
|
1961
|
+
const info = buildAccessRequestInfo(ghUser, args.ssoEmail ?? null);
|
|
1962
|
+
log.warn(`B\u1EA1n ch\u01B0a c\xF3 quy\u1EC1n access v\xE0o b\u1ED9 package ${args.repoSlug}.`);
|
|
1963
|
+
log.info("Li\xEAn h\u1EC7 admin (Luke @nal.vn) \u0111\u1EC3 \u0111\u01B0\u1EE3c add v\xE0o org nalvn.");
|
|
1964
|
+
log.plain("");
|
|
1965
|
+
log.plain(info);
|
|
1966
|
+
log.plain("");
|
|
1967
|
+
await copyInfoToClipboardWithConsent(info);
|
|
1968
|
+
while (true) {
|
|
1969
|
+
const action = await select5({
|
|
1970
|
+
message: "Ti\u1EBFp t\u1EE5c?",
|
|
1971
|
+
choices: [
|
|
1972
|
+
{ name: "\u0110\xE3 \u0111\u01B0\u1EE3c add \u2014 ki\u1EC3m tra l\u1EA1i v\xE0 ti\u1EBFp t\u1EE5c", value: "retry" },
|
|
1973
|
+
{ name: "T\u1EA1m ng\u01B0ng \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau", value: "abort" }
|
|
1974
|
+
]
|
|
1975
|
+
});
|
|
1976
|
+
if (action === "abort") {
|
|
1977
|
+
log.dim("T\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\xE3 accept invite t\u1EEB GitHub.");
|
|
1978
|
+
return false;
|
|
1979
|
+
}
|
|
1980
|
+
log.info("Ki\u1EC3m tra access...");
|
|
1981
|
+
if (checkRepoAccess(args.repoSlug)) {
|
|
1982
|
+
log.success("\u0110\xE3 c\xF3 access \u2014 ti\u1EBFp t\u1EE5c.");
|
|
1983
|
+
return true;
|
|
1984
|
+
}
|
|
1985
|
+
log.warn("V\u1EABn ch\u01B0a c\xF3 access. \u0110\u1EA3m b\u1EA3o b\u1EA1n \u0111\xE3 accept email invite t\u1EEB GitHub (Inbox + Spam).");
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1690
1989
|
// src/lib/resolve-team-pack-repo-url.ts
|
|
1691
|
-
var
|
|
1990
|
+
var ORG_DEFAULT = "https://github.com/nalvn/team-ai-pack.git";
|
|
1692
1991
|
function resolveTeamPackRepoUrl() {
|
|
1693
1992
|
if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
|
|
1694
1993
|
return process.env.AVATAR_TEAM_PACK_REPO_URL;
|
|
1695
1994
|
}
|
|
1696
|
-
|
|
1697
|
-
const ghUser = resolveGithubUsernameDefault();
|
|
1698
|
-
if (ghUser) return `https://github.com/${ghUser}/team-ai-pack.git`;
|
|
1699
|
-
} catch {
|
|
1700
|
-
}
|
|
1701
|
-
return LEGACY_FALLBACK;
|
|
1995
|
+
return ORG_DEFAULT;
|
|
1702
1996
|
}
|
|
1703
1997
|
|
|
1704
1998
|
// src/lib/team-pack-submodule-manager.ts
|
|
1705
1999
|
var TEAM_PACK_REPO_URL = resolveTeamPackRepoUrl();
|
|
1706
2000
|
var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
|
|
1707
|
-
|
|
2001
|
+
var TeamPackAccessAbortedError = class extends Error {
|
|
2002
|
+
constructor(message) {
|
|
2003
|
+
super(message);
|
|
2004
|
+
this.name = "TeamPackAccessAbortedError";
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
|
|
1708
2008
|
const url = resolveTeamPackRepoUrl();
|
|
2009
|
+
const repoSlug = parseRepoSlugFromGitUrl(url);
|
|
2010
|
+
if (repoSlug) {
|
|
2011
|
+
const hasAccess = await ensureTeamPackAccessWithRetry({ repoSlug, ssoEmail });
|
|
2012
|
+
if (!hasAccess) {
|
|
2013
|
+
throw new TeamPackAccessAbortedError(
|
|
2014
|
+
"User ch\u1ECDn t\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\u01B0\u1EE3c add v\xE0o org."
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
1709
2018
|
try {
|
|
1710
2019
|
await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
|
|
1711
2020
|
} catch (err) {
|
|
@@ -1992,6 +2301,14 @@ function registerInitCommand(program2) {
|
|
|
1992
2301
|
log.dim(err.message);
|
|
1993
2302
|
process.exit(0);
|
|
1994
2303
|
}
|
|
2304
|
+
if (err instanceof TeamPackAccessAbortedError) {
|
|
2305
|
+
log.dim(err.message);
|
|
2306
|
+
process.exit(0);
|
|
2307
|
+
}
|
|
2308
|
+
if (err instanceof RemoteAccessAbortedError) {
|
|
2309
|
+
log.dim(err.message);
|
|
2310
|
+
process.exit(0);
|
|
2311
|
+
}
|
|
1995
2312
|
log.error(err instanceof Error ? err.message : String(err));
|
|
1996
2313
|
process.exit(1);
|
|
1997
2314
|
}
|
|
@@ -2026,7 +2343,7 @@ async function runInit(opts) {
|
|
|
2026
2343
|
}
|
|
2027
2344
|
}
|
|
2028
2345
|
async function promptProjectStatus() {
|
|
2029
|
-
return await
|
|
2346
|
+
return await select6({
|
|
2030
2347
|
message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
|
|
2031
2348
|
choices: [
|
|
2032
2349
|
{ name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
|
|
@@ -2104,7 +2421,7 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
2104
2421
|
message: "T\xEAn d\u1EF1 \xE1n:",
|
|
2105
2422
|
validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
|
|
2106
2423
|
});
|
|
2107
|
-
const visibility = opts.repoVisibility ?? await
|
|
2424
|
+
const visibility = opts.repoVisibility ?? await select6({
|
|
2108
2425
|
message: "Visibility?",
|
|
2109
2426
|
choices: [
|
|
2110
2427
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
@@ -2164,7 +2481,7 @@ async function getOrCreateOriginRemote(folderPath, opts) {
|
|
|
2164
2481
|
log.success(`Folder \u0111\xE3 c\xF3 remote origin: ${origin.refs.push}`);
|
|
2165
2482
|
return origin.refs.push;
|
|
2166
2483
|
}
|
|
2167
|
-
const shouldCreate = opts.createRemote ?? await
|
|
2484
|
+
const shouldCreate = opts.createRemote ?? await confirm3({
|
|
2168
2485
|
message: "Folder ch\u01B0a c\xF3 remote. T\u1EA1o GitHub repo ngay \u0111\u1EC3 share team?",
|
|
2169
2486
|
default: true
|
|
2170
2487
|
});
|
|
@@ -2173,7 +2490,7 @@ async function getOrCreateOriginRemote(folderPath, opts) {
|
|
|
2173
2490
|
return void 0;
|
|
2174
2491
|
}
|
|
2175
2492
|
await ensureGitHubReady();
|
|
2176
|
-
const visibility = opts.repoVisibility ?? await
|
|
2493
|
+
const visibility = opts.repoVisibility ?? await select6({
|
|
2177
2494
|
message: "Visibility?",
|
|
2178
2495
|
choices: [
|
|
2179
2496
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
@@ -2264,13 +2581,13 @@ async function maybeCreateWorkspaceRemote(args) {
|
|
|
2264
2581
|
let shouldCreate = args.createWorkspaceRemote;
|
|
2265
2582
|
if (shouldCreate === void 0) {
|
|
2266
2583
|
if (args.autoYes) return;
|
|
2267
|
-
shouldCreate = await
|
|
2584
|
+
shouldCreate = await confirm3({
|
|
2268
2585
|
message: "T\u1EA1o remote GitHub cho workspace \u0111\u1EC3 share team? (Avatar state)",
|
|
2269
2586
|
default: false
|
|
2270
2587
|
});
|
|
2271
2588
|
}
|
|
2272
2589
|
if (!shouldCreate) return;
|
|
2273
|
-
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await
|
|
2590
|
+
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select6({
|
|
2274
2591
|
message: "Workspace visibility?",
|
|
2275
2592
|
choices: [
|
|
2276
2593
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
|
|
@@ -2301,7 +2618,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
|
|
|
2301
2618
|
log.info(`--force: d\xF9ng ${alternative}`);
|
|
2302
2619
|
return alternative;
|
|
2303
2620
|
}
|
|
2304
|
-
const useAlt = await
|
|
2621
|
+
const useAlt = await confirm3({ message: `D\xF9ng "${alternative}" thay th\u1EBF?`, default: true });
|
|
2305
2622
|
if (!useAlt) throw new Error("H\u1EE7y init. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c.");
|
|
2306
2623
|
return alternative;
|
|
2307
2624
|
}
|
|
@@ -2476,7 +2793,7 @@ function registerToolsCommand(program2) {
|
|
|
2476
2793
|
|
|
2477
2794
|
// src/commands/uninstall.ts
|
|
2478
2795
|
import { relative as relative3 } from "path";
|
|
2479
|
-
import { confirm as
|
|
2796
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
2480
2797
|
import boxen5 from "boxen";
|
|
2481
2798
|
|
|
2482
2799
|
// src/lib/create-uninstall-backup-snapshot.ts
|
|
@@ -2631,7 +2948,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
|
|
|
2631
2948
|
}
|
|
2632
2949
|
|
|
2633
2950
|
// src/commands/uninstall.ts
|
|
2634
|
-
var CLI_VERSION = "1.2.
|
|
2951
|
+
var CLI_VERSION = "1.2.4";
|
|
2635
2952
|
function registerUninstallCommand(program2) {
|
|
2636
2953
|
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) => {
|
|
2637
2954
|
try {
|
|
@@ -2655,7 +2972,7 @@ async function runUninstall(opts) {
|
|
|
2655
2972
|
return;
|
|
2656
2973
|
}
|
|
2657
2974
|
if (!opts.yes) {
|
|
2658
|
-
const ok = await
|
|
2975
|
+
const ok = await confirm4({
|
|
2659
2976
|
message: "Ti\u1EBFp t\u1EE5c g\u1EE1 Avatar?",
|
|
2660
2977
|
default: false
|
|
2661
2978
|
});
|
|
@@ -2713,7 +3030,7 @@ function printUninstallSuccessBox(backupPath) {
|
|
|
2713
3030
|
}
|
|
2714
3031
|
|
|
2715
3032
|
// src/index.ts
|
|
2716
|
-
var CLI_VERSION2 = "1.2.
|
|
3033
|
+
var CLI_VERSION2 = "1.2.4";
|
|
2717
3034
|
var program = new Command();
|
|
2718
3035
|
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(
|
|
2719
3036
|
"beforeAll",
|