@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
- log.info("Calling provider v\u1EDBi prompt 'say ok'...");
696
- const result = spawnSync4("claude", ["--print", "say ok"], {
697
- encoding: "utf8",
698
- timeout: 3e4
699
- });
700
- if (result.signal === "SIGTERM") {
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 confirm2, input as input2, select as select4 } from "@inquirer/prompts";
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 spawnSync14 } from "child_process";
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/install-gh-cli-via-package-manager.ts
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 = spawnSync10(spec.cmd, spec.args, { stdio: "inherit" });
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 spawnSync11 } from "child_process";
1515
+ import { spawnSync as spawnSync13 } from "child_process";
1314
1516
  function setupGitCredentialViaGh() {
1315
- const r = spawnSync11("gh", ["auth", "setup-git"], { stdio: "ignore" });
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 spawnSync12 } from "child_process";
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 = spawnSync12(
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
- verifyGitRemoteAccessible(remoteUrl);
1383
- log.success(`Remote accessible: ${remoteUrl}`);
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 org = input3.org ?? resolveGithubUsernameDefault();
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 = spawnSync14(
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 select3 } from "@inquirer/prompts";
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 select3({
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 LEGACY_FALLBACK = "https://github.com/LukeNALS/team-ai-pack.git";
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
- try {
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
- async function addTeamPackSubmodule(projectRoot, tag) {
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 select4({
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 select4({
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 confirm2({
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 select4({
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 confirm2({
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 select4({
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 confirm2({ message: `D\xF9ng "${alternative}" thay th\u1EBF?`, default: true });
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 confirm3 } from "@inquirer/prompts";
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.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 confirm3({
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.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",