@nalvietnam/avatar-cli 1.2.3 → 1.2.5

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
@@ -546,19 +546,19 @@ function applyUseGlobal(existing, source) {
546
546
  ...sourceModel ? { model: sourceModel } : {}
547
547
  };
548
548
  }
549
- async function writeClaudeSettings(workspacePath, input3) {
549
+ async function writeClaudeSettings(workspacePath, input4) {
550
550
  const path = getClaudeSettingsPath(workspacePath);
551
551
  const existing = await readExistingSettings(path);
552
552
  let merged;
553
- switch (input3.provider) {
553
+ switch (input4.provider) {
554
554
  case "subscription":
555
- merged = applySubscription(existing, input3.model);
555
+ merged = applySubscription(existing, input4.model);
556
556
  break;
557
557
  case "llmlite":
558
- merged = applyLLMLite(existing, input3.apiKey, input3.baseUrl, input3.model);
558
+ merged = applyLLMLite(existing, input4.apiKey, input4.baseUrl, input4.model);
559
559
  break;
560
560
  case "use-global":
561
- merged = applyUseGlobal(existing, input3.sourceSettings);
561
+ merged = applyUseGlobal(existing, input4.sourceSettings);
562
562
  break;
563
563
  }
564
564
  await writeJsonAtomic(path, merged, SECRET_FILE_MODE2);
@@ -1194,9 +1194,199 @@ async function applyFixes(checks) {
1194
1194
 
1195
1195
  // src/commands/init.ts
1196
1196
  import { basename, join as join16, relative as relative2, resolve } from "path";
1197
- import { confirm as confirm3, input as input2, select as select5 } from "@inquirer/prompts";
1197
+ import { confirm as confirm3, input as input3, select as select7 } from "@inquirer/prompts";
1198
1198
  import boxen3 from "boxen";
1199
1199
 
1200
+ // src/lib/prompt-recovery-action-on-failure.ts
1201
+ import { input as input2, select as select3 } from "@inquirer/prompts";
1202
+ var UserAbortedRecoveryError = class extends Error {
1203
+ constructor(message) {
1204
+ super(message);
1205
+ this.name = "UserAbortedRecoveryError";
1206
+ }
1207
+ };
1208
+ async function promptRetryOrSkip(args) {
1209
+ log.warn(`${args.taskName} th\u1EA5t b\u1EA1i: ${args.reason}`);
1210
+ if (args.hint) log.info(args.hint);
1211
+ const choices = [
1212
+ { name: "Th\u1EED l\u1EA1i (Retry)", value: "retry" }
1213
+ ];
1214
+ if (args.allowSkip) {
1215
+ choices.push({ name: "B\u1ECF qua b\u01B0\u1EDBc n\xE0y v\xE0 ti\u1EBFp t\u1EE5c (Skip)", value: "skip" });
1216
+ }
1217
+ choices.push({ name: "T\u1EA1m ng\u01B0ng init \u2014 ch\u1EA1y l\u1EA1i sau (Abort)", value: "abort" });
1218
+ return await select3({
1219
+ message: "C\xE1ch x\u1EED l\xFD?",
1220
+ choices
1221
+ });
1222
+ }
1223
+
1224
+ // src/lib/team-pack-submodule-manager.ts
1225
+ import { join as join10 } from "path";
1226
+
1227
+ // src/lib/check-team-pack-access-with-retry-loop.ts
1228
+ import { spawnSync as spawnSync6 } from "child_process";
1229
+ import { confirm as confirm2, select as select4 } from "@inquirer/prompts";
1230
+ function parseRepoSlugFromGitUrl(url) {
1231
+ const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
1232
+ if (httpsMatch) return httpsMatch[1];
1233
+ return null;
1234
+ }
1235
+ function checkRepoAccess(repoSlug) {
1236
+ const r = spawnSync6("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
1237
+ return r.status === 0;
1238
+ }
1239
+ function getCurrentGhUser() {
1240
+ const r = spawnSync6("gh", ["api", "user", "--jq", ".login"], {
1241
+ encoding: "utf8",
1242
+ stdio: ["ignore", "pipe", "pipe"]
1243
+ });
1244
+ if (r.status !== 0) return null;
1245
+ return r.stdout.trim() || null;
1246
+ }
1247
+ async function copyInfoToClipboardWithConsent(info) {
1248
+ const ok = await confirm2({
1249
+ message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
1250
+ default: true
1251
+ });
1252
+ if (!ok) return;
1253
+ try {
1254
+ const { default: clipboardy } = await import("clipboardy");
1255
+ await clipboardy.write(info);
1256
+ log.success("\u0110\xE3 copy v\xE0o clipboard");
1257
+ } catch (err) {
1258
+ log.dim(`Copy clipboard fail: ${err.message}`);
1259
+ }
1260
+ }
1261
+ function buildAccessRequestInfo(ghUser, ssoEmail) {
1262
+ const lines = [
1263
+ "Request access team-ai-pack (NAL)",
1264
+ "",
1265
+ `GitHub username: ${ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)"}`,
1266
+ `NAL email: ${ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)"}`,
1267
+ `Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
1268
+ ];
1269
+ return lines.join("\n");
1270
+ }
1271
+ async function ensureTeamPackAccessWithRetry(args) {
1272
+ if (checkRepoAccess(args.repoSlug)) return true;
1273
+ const ghUser = getCurrentGhUser();
1274
+ const info = buildAccessRequestInfo(ghUser, args.ssoEmail ?? null);
1275
+ log.warn(`B\u1EA1n ch\u01B0a c\xF3 quy\u1EC1n access v\xE0o b\u1ED9 package ${args.repoSlug}.`);
1276
+ log.info("Li\xEAn h\u1EC7 admin (Luke @nal.vn) \u0111\u1EC3 \u0111\u01B0\u1EE3c add v\xE0o org nalvn.");
1277
+ log.plain("");
1278
+ log.plain(info);
1279
+ log.plain("");
1280
+ await copyInfoToClipboardWithConsent(info);
1281
+ while (true) {
1282
+ const action = await select4({
1283
+ message: "Ti\u1EBFp t\u1EE5c?",
1284
+ choices: [
1285
+ { name: "\u0110\xE3 \u0111\u01B0\u1EE3c add \u2014 ki\u1EC3m tra l\u1EA1i v\xE0 ti\u1EBFp t\u1EE5c", value: "retry" },
1286
+ { name: "T\u1EA1m ng\u01B0ng \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau", value: "abort" }
1287
+ ]
1288
+ });
1289
+ if (action === "abort") {
1290
+ log.dim("T\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\xE3 accept invite t\u1EEB GitHub.");
1291
+ return false;
1292
+ }
1293
+ log.info("Ki\u1EC3m tra access...");
1294
+ if (checkRepoAccess(args.repoSlug)) {
1295
+ log.success("\u0110\xE3 c\xF3 access \u2014 ti\u1EBFp t\u1EE5c.");
1296
+ return true;
1297
+ }
1298
+ 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).");
1299
+ }
1300
+ }
1301
+
1302
+ // src/lib/resolve-team-pack-repo-url.ts
1303
+ var ORG_DEFAULT = "https://github.com/nalvn/team-ai-pack.git";
1304
+ function resolveTeamPackRepoUrl() {
1305
+ if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
1306
+ return process.env.AVATAR_TEAM_PACK_REPO_URL;
1307
+ }
1308
+ return ORG_DEFAULT;
1309
+ }
1310
+
1311
+ // src/lib/team-pack-submodule-manager.ts
1312
+ var TEAM_PACK_REPO_URL = resolveTeamPackRepoUrl();
1313
+ var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
1314
+ var TeamPackAccessAbortedError = class extends Error {
1315
+ constructor(message) {
1316
+ super(message);
1317
+ this.name = "TeamPackAccessAbortedError";
1318
+ }
1319
+ };
1320
+ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
1321
+ const url = resolveTeamPackRepoUrl();
1322
+ const repoSlug = parseRepoSlugFromGitUrl(url);
1323
+ if (repoSlug) {
1324
+ const hasAccess = await ensureTeamPackAccessWithRetry({ repoSlug, ssoEmail });
1325
+ if (!hasAccess) {
1326
+ throw new TeamPackAccessAbortedError(
1327
+ "User ch\u1ECDn t\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\u01B0\u1EE3c add v\xE0o org."
1328
+ );
1329
+ }
1330
+ }
1331
+ try {
1332
+ await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
1333
+ } catch (err) {
1334
+ const msg = err instanceof Error ? err.message : String(err);
1335
+ if (msg.includes("Repository not found") || msg.includes("not found")) {
1336
+ log.error(
1337
+ `Repo team-ai-pack kh\xF4ng t\u1ED3n t\u1EA1i: ${url}
1338
+ C\xE1ch fix:
1339
+ 1. T\u1EA1o repo: gh repo create <owner>/team-ai-pack --private --add-readme
1340
+ 2. Ho\u1EB7c override URL: export AVATAR_TEAM_PACK_REPO_URL=<url-repo-c\u1EE7a-b\u1EA1n>
1341
+ 3. Ho\u1EB7c d\xF9ng flag --skip-team-pack`
1342
+ );
1343
+ }
1344
+ throw err;
1345
+ }
1346
+ let target = tag ?? null;
1347
+ if (!target) {
1348
+ target = await latestTag(join10(projectRoot, TEAM_PACK_RELATIVE_PATH));
1349
+ }
1350
+ if (target) {
1351
+ await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
1352
+ }
1353
+ return { pinnedTag: target };
1354
+ }
1355
+ async function readPinnedPackVersion(projectRoot) {
1356
+ const submoduleRoot = join10(projectRoot, TEAM_PACK_RELATIVE_PATH);
1357
+ const tag = await latestTag(submoduleRoot);
1358
+ if (tag) return tag;
1359
+ const sha = await currentCommitSha(submoduleRoot);
1360
+ return sha.slice(0, 7);
1361
+ }
1362
+
1363
+ // src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
1364
+ async function addTeamPackSubmoduleWithRetryOnNetworkFail(projectRoot, tag) {
1365
+ while (true) {
1366
+ try {
1367
+ const result = await addTeamPackSubmodule(projectRoot, tag);
1368
+ return { pinnedTag: result.pinnedTag, skipped: false };
1369
+ } catch (err) {
1370
+ if (err instanceof TeamPackAccessAbortedError) throw err;
1371
+ const action = await promptRetryOrSkip({
1372
+ taskName: "Pull team-ai-pack submodule",
1373
+ reason: err instanceof Error ? err.message : String(err),
1374
+ allowSkip: true,
1375
+ hint: "Network glitch? Retry th\u01B0\u1EDDng work. N\u1EBFu skip, d\xF9ng `avatar sync` sau \u0111\u1EC3 pull pack."
1376
+ });
1377
+ if (action === "abort") {
1378
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc pull team-ai-pack.");
1379
+ }
1380
+ if (action === "skip") {
1381
+ log.warn(
1382
+ "Skip team-ai-pack. Workspace d\xF9ng \u0111\u01B0\u1EE3c nh\u01B0ng kh\xF4ng c\xF3 shared knowledge. Pull sau qua `avatar sync`."
1383
+ );
1384
+ return { pinnedTag: null, skipped: true };
1385
+ }
1386
+ }
1387
+ }
1388
+ }
1389
+
1200
1390
  // src/lib/avatar-ascii-banner.ts
1201
1391
  import chalk2 from "chalk";
1202
1392
  var BANNER_LINES = [
@@ -1259,27 +1449,27 @@ ${renderAvatarBanner(opts)}
1259
1449
  }
1260
1450
 
1261
1451
  // src/lib/execute-gh-repo-create.ts
1262
- import { spawnSync as spawnSync6 } from "child_process";
1452
+ import { spawnSync as spawnSync7 } from "child_process";
1263
1453
  var RepoAlreadyExistsError = class extends Error {
1264
1454
  constructor(fullName) {
1265
1455
  super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
1266
1456
  this.name = "RepoAlreadyExistsError";
1267
1457
  }
1268
1458
  };
1269
- function executeGhRepoCreate(input3) {
1270
- const fullName = `${input3.org}/${input3.name}`;
1459
+ function executeGhRepoCreate(input4) {
1460
+ const fullName = `${input4.org}/${input4.name}`;
1271
1461
  const args = [
1272
1462
  "repo",
1273
1463
  "create",
1274
1464
  fullName,
1275
- `--${input3.visibility}`,
1465
+ `--${input4.visibility}`,
1276
1466
  "--source",
1277
- input3.folder,
1467
+ input4.folder,
1278
1468
  "--remote",
1279
1469
  "origin",
1280
1470
  "--push"
1281
1471
  ];
1282
- const r = spawnSync6("gh", args, { stdio: "inherit" });
1472
+ const r = spawnSync7("gh", args, { stdio: "inherit" });
1283
1473
  if (r.status !== 0) {
1284
1474
  if (r.status === 1) {
1285
1475
  throw new RepoAlreadyExistsError(fullName);
@@ -1293,9 +1483,9 @@ function executeGhRepoCreate(input3) {
1293
1483
  }
1294
1484
 
1295
1485
  // src/lib/resolve-github-username-default.ts
1296
- import { spawnSync as spawnSync7 } from "child_process";
1486
+ import { spawnSync as spawnSync8 } from "child_process";
1297
1487
  function resolveGithubUsernameDefault() {
1298
- const r = spawnSync7("gh", ["api", "user", "--jq", ".login"], {
1488
+ const r = spawnSync8("gh", ["api", "user", "--jq", ".login"], {
1299
1489
  encoding: "utf8",
1300
1490
  stdio: ["ignore", "pipe", "pipe"]
1301
1491
  });
@@ -1327,28 +1517,28 @@ function validateRepoVisibility(v) {
1327
1517
  }
1328
1518
 
1329
1519
  // src/lib/create-github-remote-from-folder.ts
1330
- function createGithubRemoteFromFolder(input3) {
1331
- validateRepoName(input3.name);
1332
- validateRepoVisibility(input3.visibility);
1333
- const org = input3.org ?? resolveGithubUsernameDefault();
1334
- log.info(`T\u1EA1o GitHub repo ${org}/${input3.name} (${input3.visibility})...`);
1520
+ function createGithubRemoteFromFolder(input4) {
1521
+ validateRepoName(input4.name);
1522
+ validateRepoVisibility(input4.visibility);
1523
+ const org = input4.org ?? resolveGithubUsernameDefault();
1524
+ log.info(`T\u1EA1o GitHub repo ${org}/${input4.name} (${input4.visibility})...`);
1335
1525
  const urls = executeGhRepoCreate({
1336
- folder: input3.folder,
1526
+ folder: input4.folder,
1337
1527
  org,
1338
- name: input3.name,
1339
- visibility: input3.visibility
1528
+ name: input4.name,
1529
+ visibility: input4.visibility
1340
1530
  });
1341
1531
  log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
1342
1532
  return urls;
1343
1533
  }
1344
1534
 
1345
1535
  // src/lib/create-workspace-remote-via-gh.ts
1346
- import { spawnSync as spawnSync14 } from "child_process";
1536
+ import { spawnSync as spawnSync16 } from "child_process";
1347
1537
 
1348
1538
  // src/lib/check-gh-cli-auth-status.ts
1349
- import { spawnSync as spawnSync8 } from "child_process";
1539
+ import { spawnSync as spawnSync9 } from "child_process";
1350
1540
  function checkGhCliAuthStatus() {
1351
- const r = spawnSync8("gh", ["auth", "status"], { stdio: "ignore" });
1541
+ const r = spawnSync9("gh", ["auth", "status"], { stdio: "ignore" });
1352
1542
  if (r.error && r.error.code === "ENOENT") {
1353
1543
  return "not-installed";
1354
1544
  }
@@ -1356,12 +1546,12 @@ function checkGhCliAuthStatus() {
1356
1546
  }
1357
1547
 
1358
1548
  // src/lib/detect-package-manager.ts
1359
- import { spawnSync as spawnSync9 } from "child_process";
1549
+ import { spawnSync as spawnSync10 } from "child_process";
1360
1550
  function hasBinary(name) {
1361
1551
  const platform2 = detectHostPlatform();
1362
1552
  const probe = platform2 === "win32" ? "where" : "command";
1363
1553
  const args = platform2 === "win32" ? [name] : ["-v", name];
1364
- const r = spawnSync9(probe, args, {
1554
+ const r = spawnSync10(probe, args, {
1365
1555
  shell: platform2 !== "win32",
1366
1556
  stdio: "ignore"
1367
1557
  });
@@ -1376,8 +1566,124 @@ function detectPackageManager() {
1376
1566
  return null;
1377
1567
  }
1378
1568
 
1569
+ // src/lib/handle-remote-access-failure-with-account-switch.ts
1570
+ import { spawnSync as spawnSync12 } from "child_process";
1571
+ import { select as select5 } from "@inquirer/prompts";
1572
+
1573
+ // src/lib/verify-git-remote-accessible.ts
1574
+ import { spawnSync as spawnSync11 } from "child_process";
1575
+ var TIMEOUT_MS = 5e3;
1576
+ function classifyRemoteError(stderr) {
1577
+ const text = stderr.toLowerCase();
1578
+ 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")) {
1579
+ return "no-access";
1580
+ }
1581
+ if (text.includes("404") || text.includes("does not exist")) {
1582
+ return "not-found";
1583
+ }
1584
+ if (text.includes("could not resolve host") || text.includes("network") || text.includes("connection refused") || text.includes("connection timed out")) {
1585
+ return "network";
1586
+ }
1587
+ return "unknown";
1588
+ }
1589
+ function tryVerifyGitRemoteAccessible(url) {
1590
+ const r = spawnSync11("git", ["ls-remote", "--exit-code", url, "HEAD"], {
1591
+ encoding: "utf8",
1592
+ timeout: TIMEOUT_MS,
1593
+ stdio: ["ignore", "pipe", "pipe"]
1594
+ });
1595
+ if (r.status === 0) return { ok: true };
1596
+ if (r.signal === "SIGTERM") {
1597
+ return { ok: false, reason: "timeout", detail: "git ls-remote > 5s" };
1598
+ }
1599
+ const stderr = (r.stderr || "").trim();
1600
+ const reason = classifyRemoteError(stderr);
1601
+ return { ok: false, reason, detail: stderr.slice(0, 300) };
1602
+ }
1603
+
1604
+ // src/lib/handle-remote-access-failure-with-account-switch.ts
1605
+ var RemoteAccessAbortedError = class extends Error {
1606
+ constructor(message) {
1607
+ super(message);
1608
+ this.name = "RemoteAccessAbortedError";
1609
+ }
1610
+ };
1611
+ function getCurrentGhUser2() {
1612
+ const r = spawnSync12("gh", ["api", "user", "--jq", ".login"], {
1613
+ encoding: "utf8",
1614
+ stdio: ["ignore", "pipe", "pipe"]
1615
+ });
1616
+ if (r.status !== 0) return null;
1617
+ return r.stdout.trim() || null;
1618
+ }
1619
+ function triggerGhAuthLoginInteractive() {
1620
+ log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
1621
+ const r = spawnSync12("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1622
+ if (r.status !== 0) {
1623
+ log.warn(`gh auth login exit ${r.status}. B\u1EA1n c\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
1624
+ }
1625
+ }
1626
+ function getReasonHint(reason, url, ghUser) {
1627
+ switch (reason) {
1628
+ case "no-access":
1629
+ 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.";
1630
+ case "not-found":
1631
+ return `URL c\xF3 th\u1EC3 sai ch\xEDnh t\u1EA3, ho\u1EB7c repo \u0111\xE3 b\u1ECB x\xF3a/rename. Check: ${url}`;
1632
+ case "network":
1633
+ return "Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c GitHub. Check m\u1EA1ng / VPN / firewall.";
1634
+ case "timeout":
1635
+ return "Network ch\u1EADm > 5s. Check m\u1EA1ng r\u1ED3i retry.";
1636
+ default:
1637
+ return "L\u1ED7i kh\xF4ng x\xE1c \u0111\u1ECBnh. Check URL + gh auth status.";
1638
+ }
1639
+ }
1640
+ async function handleRemoteAccessFailureWithAccountSwitch(args) {
1641
+ let reason = args.initialReason;
1642
+ let detail = args.initialDetail;
1643
+ while (true) {
1644
+ const ghUser = getCurrentGhUser2();
1645
+ log.warn(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c ${args.url}`);
1646
+ log.dim(` L\xFD do: ${reason}${detail ? ` \u2014 ${detail.slice(0, 150)}` : ""}`);
1647
+ log.info(getReasonHint(reason, args.url, ghUser));
1648
+ if (ghUser) log.dim(` gh CLI hi\u1EC7n \u0111ang login: ${ghUser}`);
1649
+ const action = await select5({
1650
+ message: "C\xE1ch x\u1EED l\xFD?",
1651
+ choices: [
1652
+ {
1653
+ name: "Switch GitHub account (gh auth login \u2014 m\u1EDF browser)",
1654
+ value: "switch"
1655
+ },
1656
+ {
1657
+ name: "T\xF4i v\u1EEBa fix (accept invite / s\u1EEDa URL) \u2014 retry verify",
1658
+ value: "retry"
1659
+ },
1660
+ {
1661
+ name: "T\u1EA1m ng\u01B0ng init \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau",
1662
+ value: "abort"
1663
+ }
1664
+ ]
1665
+ });
1666
+ if (action === "abort") {
1667
+ throw new RemoteAccessAbortedError(
1668
+ `User ch\u1ECDn t\u1EA1m ng\u01B0ng. Fix access ${args.url} r\u1ED3i ch\u1EA1y l\u1EA1i 'avatar init'.`
1669
+ );
1670
+ }
1671
+ if (action === "switch") {
1672
+ triggerGhAuthLoginInteractive();
1673
+ }
1674
+ log.info("Verify remote l\u1EA1i...");
1675
+ const result = tryVerifyGitRemoteAccessible(args.url);
1676
+ if (result.ok) {
1677
+ log.success(`Remote accessible: ${args.url}`);
1678
+ return;
1679
+ }
1680
+ reason = result.reason ?? "unknown";
1681
+ detail = result.detail;
1682
+ }
1683
+ }
1684
+
1379
1685
  // src/lib/install-gh-cli-via-package-manager.ts
1380
- import { spawnSync as spawnSync10 } from "child_process";
1686
+ import { spawnSync as spawnSync13 } from "child_process";
1381
1687
  var INSTALL_COMMANDS = {
1382
1688
  brew: { cmd: "brew", args: ["install", "gh"] },
1383
1689
  apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
@@ -1388,7 +1694,7 @@ var INSTALL_COMMANDS = {
1388
1694
  function installGhCliViaPackageManager(pm) {
1389
1695
  const spec = INSTALL_COMMANDS[pm];
1390
1696
  log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
1391
- const r = spawnSync10(spec.cmd, spec.args, { stdio: "inherit" });
1697
+ const r = spawnSync13(spec.cmd, spec.args, { stdio: "inherit" });
1392
1698
  if (r.status !== 0) {
1393
1699
  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.`);
1394
1700
  }
@@ -1396,9 +1702,9 @@ function installGhCliViaPackageManager(pm) {
1396
1702
  }
1397
1703
 
1398
1704
  // src/lib/setup-git-credential-via-gh.ts
1399
- import { spawnSync as spawnSync11 } from "child_process";
1705
+ import { spawnSync as spawnSync14 } from "child_process";
1400
1706
  function setupGitCredentialViaGh() {
1401
- const r = spawnSync11("gh", ["auth", "setup-git"], { stdio: "ignore" });
1707
+ const r = spawnSync14("gh", ["auth", "setup-git"], { stdio: "ignore" });
1402
1708
  if (r.status !== 0) {
1403
1709
  log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
1404
1710
  return;
@@ -1407,10 +1713,10 @@ function setupGitCredentialViaGh() {
1407
1713
  }
1408
1714
 
1409
1715
  // src/lib/trigger-gh-cli-auth-login.ts
1410
- import { spawnSync as spawnSync12 } from "child_process";
1716
+ import { spawnSync as spawnSync15 } from "child_process";
1411
1717
  function triggerGhCliAuthLogin() {
1412
1718
  log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
1413
- const r = spawnSync12(
1719
+ const r = spawnSync15(
1414
1720
  "gh",
1415
1721
  ["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
1416
1722
  { stdio: "inherit" }
@@ -1421,65 +1727,91 @@ function triggerGhCliAuthLogin() {
1421
1727
  log.success("\u0110\xE3 \u0111\u0103ng nh\u1EADp GitHub");
1422
1728
  }
1423
1729
 
1424
- // src/lib/verify-git-remote-accessible.ts
1425
- import { spawnSync as spawnSync13 } from "child_process";
1426
- var TIMEOUT_MS = 5e3;
1427
- var RemoteNotAccessibleError = class extends Error {
1428
- constructor(url, reason) {
1429
- super(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c remote ${url}: ${reason}`);
1430
- this.name = "RemoteNotAccessibleError";
1431
- }
1432
- };
1433
- function verifyGitRemoteAccessible(url) {
1434
- const r = spawnSync13("git", ["ls-remote", "--exit-code", url, "HEAD"], {
1435
- stdio: "ignore",
1436
- timeout: TIMEOUT_MS
1437
- });
1438
- if (r.status === 0) return;
1439
- if (r.signal === "SIGTERM") throw new RemoteNotAccessibleError(url, "timeout 5s");
1440
- throw new RemoteNotAccessibleError(url, `git ls-remote exit ${r.status}`);
1441
- }
1442
-
1443
1730
  // src/lib/git-auth-and-install-orchestrator.ts
1444
1731
  async function ensureGitHubReady(remoteUrl) {
1445
- let state = checkGhCliAuthStatus();
1446
- if (state === "not-installed") {
1732
+ while (checkGhCliAuthStatus() === "not-installed") {
1447
1733
  log.warn("gh CLI ch\u01B0a c\xE0i. Avatar s\u1EBD t\u1EF1 c\xE0i.");
1448
1734
  const pm = detectPackageManager();
1449
1735
  if (!pm) {
1450
- throw new Error(
1451
- "Kh\xF4ng ph\xE1t hi\u1EC7n package manager (brew/apt/dnf/pacman/winget). C\xE0i gh CLI tay r\u1ED3i ch\u1EA1y l\u1EA1i: https://cli.github.com"
1452
- );
1736
+ const action = await promptRetryOrSkip({
1737
+ taskName: "Ph\xE1t hi\u1EC7n package manager",
1738
+ reason: "Kh\xF4ng t\xECm th\u1EA5y brew/apt/dnf/pacman/winget tr\xEAn m\xE1y.",
1739
+ allowSkip: false,
1740
+ hint: "C\xE0i gh CLI tay (https://cli.github.com) r\u1ED3i ch\u1ECDn Retry."
1741
+ });
1742
+ if (action === "abort") {
1743
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc c\xE0i gh CLI.");
1744
+ }
1745
+ continue;
1746
+ }
1747
+ try {
1748
+ installGhCliViaPackageManager(pm);
1749
+ } catch (err) {
1750
+ const action = await promptRetryOrSkip({
1751
+ taskName: `C\xE0i gh CLI qua ${pm}`,
1752
+ reason: err.message,
1753
+ allowSkip: false,
1754
+ hint: "C\xE0i tay (https://cli.github.com) r\u1ED3i ch\u1ECDn Retry, ho\u1EB7c Abort."
1755
+ });
1756
+ if (action === "abort") {
1757
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc c\xE0i gh CLI.");
1758
+ }
1453
1759
  }
1454
- installGhCliViaPackageManager(pm);
1455
- state = checkGhCliAuthStatus();
1456
1760
  }
1457
- if (state === "not-authenticated") {
1761
+ while (checkGhCliAuthStatus() === "not-authenticated") {
1458
1762
  log.warn("Ch\u01B0a \u0111\u0103ng nh\u1EADp GitHub.");
1459
- triggerGhCliAuthLogin();
1460
- state = checkGhCliAuthStatus();
1461
- if (state !== "authenticated") {
1462
- throw new Error("Sau gh auth login v\u1EABn ch\u01B0a authenticated. Th\u1EED l\u1EA1i.");
1763
+ try {
1764
+ triggerGhCliAuthLogin();
1765
+ } catch (err) {
1766
+ const action = await promptRetryOrSkip({
1767
+ taskName: "\u0110\u0103ng nh\u1EADp GitHub qua gh",
1768
+ reason: err.message,
1769
+ allowSkip: false,
1770
+ hint: "Th\u1EED l\u1EA1i \u2014 ch\u1ECDn c\xE1ch login kh\xE1c (browser vs token) khi gh prompt."
1771
+ });
1772
+ if (action === "abort") {
1773
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc gh auth login.");
1774
+ }
1775
+ continue;
1776
+ }
1777
+ if (checkGhCliAuthStatus() !== "authenticated") {
1778
+ const action = await promptRetryOrSkip({
1779
+ taskName: "Verify gh auth",
1780
+ reason: "Sau gh auth login v\u1EABn b\xE1o not-authenticated.",
1781
+ allowSkip: false,
1782
+ hint: "C\xF3 th\u1EC3 user cancel browser. Th\u1EED l\u1EA1i ho\u1EB7c abort."
1783
+ });
1784
+ if (action === "abort") {
1785
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc verify gh auth.");
1786
+ }
1463
1787
  }
1464
1788
  }
1465
1789
  log.success("gh CLI s\u1EB5n s\xE0ng");
1466
1790
  setupGitCredentialViaGh();
1467
1791
  if (remoteUrl) {
1468
- verifyGitRemoteAccessible(remoteUrl);
1469
- log.success(`Remote accessible: ${remoteUrl}`);
1792
+ const result = tryVerifyGitRemoteAccessible(remoteUrl);
1793
+ if (result.ok) {
1794
+ log.success(`Remote accessible: ${remoteUrl}`);
1795
+ } else {
1796
+ await handleRemoteAccessFailureWithAccountSwitch({
1797
+ url: remoteUrl,
1798
+ initialReason: result.reason ?? "unknown",
1799
+ initialDetail: result.detail
1800
+ });
1801
+ }
1470
1802
  }
1471
1803
  }
1472
1804
 
1473
1805
  // src/lib/create-workspace-remote-via-gh.ts
1474
1806
  function canCreateInNamespace(org, ghUser) {
1475
1807
  if (org.toLowerCase() === ghUser.toLowerCase()) return { ok: true };
1476
- const r = spawnSync14("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
1808
+ const r = spawnSync16("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
1477
1809
  stdio: "ignore"
1478
1810
  });
1479
1811
  if (r.status === 0) return { ok: true };
1480
- const orgCheck = spawnSync14("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
1812
+ const orgCheck = spawnSync16("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
1481
1813
  if (orgCheck.status !== 0) {
1482
- const userCheck = spawnSync14("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
1814
+ const userCheck = spawnSync16("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
1483
1815
  if (userCheck.status === 0) {
1484
1816
  return {
1485
1817
  ok: false,
@@ -1496,27 +1828,27 @@ function canCreateInNamespace(org, ghUser) {
1496
1828
  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.`
1497
1829
  };
1498
1830
  }
1499
- async function createWorkspaceRemoteViaGh(input3) {
1500
- validateRepoName(input3.workspaceName);
1501
- validateRepoVisibility(input3.visibility);
1831
+ async function createWorkspaceRemoteViaGh(input4) {
1832
+ validateRepoName(input4.workspaceName);
1833
+ validateRepoVisibility(input4.visibility);
1502
1834
  await ensureGitHubReady();
1503
1835
  const ghUser = resolveGithubUsernameDefault();
1504
- const org = input3.org ?? ghUser;
1836
+ const org = input4.org ?? ghUser;
1505
1837
  const namespaceCheck = canCreateInNamespace(org, ghUser);
1506
1838
  if (!namespaceCheck.ok) {
1507
1839
  throw new Error(`Kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi '${org}/': ${namespaceCheck.reason}`);
1508
1840
  }
1509
- const fullName = `${org}/${input3.workspaceName}`;
1510
- log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input3.visibility})...`);
1511
- const r = spawnSync14(
1841
+ const fullName = `${org}/${input4.workspaceName}`;
1842
+ log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input4.visibility})...`);
1843
+ const r = spawnSync16(
1512
1844
  "gh",
1513
1845
  [
1514
1846
  "repo",
1515
1847
  "create",
1516
1848
  fullName,
1517
- `--${input3.visibility}`,
1849
+ `--${input4.visibility}`,
1518
1850
  "--source",
1519
- input3.workspacePath,
1851
+ input4.workspacePath,
1520
1852
  "--remote",
1521
1853
  "origin",
1522
1854
  "--push"
@@ -1528,7 +1860,7 @@ async function createWorkspaceRemoteViaGh(input3) {
1528
1860
  if (stderr) process.stderr.write(`${stderr}
1529
1861
  `);
1530
1862
  throw new Error(
1531
- `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`
1863
+ `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} --${input4.visibility} --source=. --remote=origin --push`
1532
1864
  );
1533
1865
  }
1534
1866
  const sshUrl = `git@github.com:${fullName}.git`;
@@ -1539,14 +1871,14 @@ async function createWorkspaceRemoteViaGh(input3) {
1539
1871
 
1540
1872
  // src/lib/safe-bootstrap-for-dirty-folder.ts
1541
1873
  import { readdirSync } from "fs";
1542
- import { select as select3 } from "@inquirer/prompts";
1874
+ import { select as select6 } from "@inquirer/prompts";
1543
1875
  import { simpleGit as simpleGit3 } from "simple-git";
1544
1876
 
1545
1877
  // src/lib/check-folder-has-git.ts
1546
1878
  import { existsSync as existsSync2, statSync } from "fs";
1547
- import { join as join10 } from "path";
1879
+ import { join as join11 } from "path";
1548
1880
  function checkFolderHasGit(folderPath) {
1549
- const gitPath = join10(folderPath, ".git");
1881
+ const gitPath = join11(folderPath, ".git");
1550
1882
  if (!existsSync2(gitPath)) return false;
1551
1883
  const stat = statSync(gitPath);
1552
1884
  return stat.isDirectory() || stat.isFile();
@@ -1578,7 +1910,7 @@ async function createInitialGitCommit(folderPath) {
1578
1910
 
1579
1911
  // src/lib/detect-folder-tech-stack.ts
1580
1912
  import { existsSync as existsSync3 } from "fs";
1581
- import { join as join11 } from "path";
1913
+ import { join as join12 } from "path";
1582
1914
  var SIGNATURES = {
1583
1915
  node: ["package.json"],
1584
1916
  python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
@@ -1590,7 +1922,7 @@ var SIGNATURES = {
1590
1922
  function detectFolderTechStack(folderPath) {
1591
1923
  const matched = [];
1592
1924
  for (const [stack, files] of Object.entries(SIGNATURES)) {
1593
- if (files.some((f) => existsSync3(join11(folderPath, f)))) {
1925
+ if (files.some((f) => existsSync3(join12(folderPath, f)))) {
1594
1926
  matched.push(stack);
1595
1927
  }
1596
1928
  }
@@ -1599,19 +1931,19 @@ function detectFolderTechStack(folderPath) {
1599
1931
 
1600
1932
  // src/lib/gitignore-template-loader.ts
1601
1933
  import { readFileSync as readFileSync2 } from "fs";
1602
- import { dirname as dirname3, join as join12 } from "path";
1934
+ import { dirname as dirname3, join as join13 } from "path";
1603
1935
  import { fileURLToPath as fileURLToPath2 } from "url";
1604
1936
  var __dirname = dirname3(fileURLToPath2(import.meta.url));
1605
1937
  var CANDIDATE_DIRS = [
1606
- join12(__dirname, "..", "templates", "gitignore"),
1607
- join12(__dirname, "..", "..", "src", "templates", "gitignore")
1938
+ join13(__dirname, "..", "templates", "gitignore"),
1939
+ join13(__dirname, "..", "..", "src", "templates", "gitignore")
1608
1940
  ];
1609
1941
  var AVATAR_MARKER_START = "# === avatar ===";
1610
1942
  var AVATAR_MARKER_END = "# === /avatar ===";
1611
1943
  function readTemplate(stack) {
1612
1944
  for (const dir of CANDIDATE_DIRS) {
1613
1945
  try {
1614
- return readFileSync2(join12(dir, `${stack}.txt`), "utf8");
1946
+ return readFileSync2(join13(dir, `${stack}.txt`), "utf8");
1615
1947
  } catch {
1616
1948
  }
1617
1949
  }
@@ -1626,9 +1958,9 @@ ${readTemplate(s).trim()}`);
1626
1958
 
1627
1959
  // src/lib/write-or-merge-gitignore.ts
1628
1960
  import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
1629
- import { join as join13 } from "path";
1961
+ import { join as join14 } from "path";
1630
1962
  function writeOrMergeGitignore(folderPath, avatarBlock) {
1631
- const path = join13(folderPath, ".gitignore");
1963
+ const path = join14(folderPath, ".gitignore");
1632
1964
  if (!existsSync4(path)) {
1633
1965
  writeFileSync(path, avatarBlock, "utf8");
1634
1966
  return;
@@ -1670,7 +2002,7 @@ async function promptBootstrapStrategy(state, opts) {
1670
2002
  if (opts.presetStrategy) return opts.presetStrategy;
1671
2003
  if (opts.autoYes) return "stash";
1672
2004
  if (state === "empty" || state === "clean") return "commit-all";
1673
- return await select3({
2005
+ return await select6({
1674
2006
  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:",
1675
2007
  choices: [
1676
2008
  {
@@ -1803,145 +2135,6 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
1803
2135
  await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
1804
2136
  }
1805
2137
 
1806
- // src/lib/team-pack-submodule-manager.ts
1807
- import { join as join14 } from "path";
1808
-
1809
- // src/lib/check-team-pack-access-with-retry-loop.ts
1810
- import { spawnSync as spawnSync15 } from "child_process";
1811
- import { confirm as confirm2, select as select4 } from "@inquirer/prompts";
1812
- function parseRepoSlugFromGitUrl(url) {
1813
- const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
1814
- if (httpsMatch) return httpsMatch[1];
1815
- return null;
1816
- }
1817
- function checkRepoAccess(repoSlug) {
1818
- const r = spawnSync15("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
1819
- return r.status === 0;
1820
- }
1821
- function getCurrentGhUser() {
1822
- const r = spawnSync15("gh", ["api", "user", "--jq", ".login"], {
1823
- encoding: "utf8",
1824
- stdio: ["ignore", "pipe", "pipe"]
1825
- });
1826
- if (r.status !== 0) return null;
1827
- return r.stdout.trim() || null;
1828
- }
1829
- async function copyInfoToClipboardWithConsent(info) {
1830
- const ok = await confirm2({
1831
- message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
1832
- default: true
1833
- });
1834
- if (!ok) return;
1835
- try {
1836
- const { default: clipboardy } = await import("clipboardy");
1837
- await clipboardy.write(info);
1838
- log.success("\u0110\xE3 copy v\xE0o clipboard");
1839
- } catch (err) {
1840
- log.dim(`Copy clipboard fail: ${err.message}`);
1841
- }
1842
- }
1843
- function buildAccessRequestInfo(ghUser, ssoEmail) {
1844
- const lines = [
1845
- "Request access team-ai-pack (NAL)",
1846
- "",
1847
- `GitHub username: ${ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)"}`,
1848
- `NAL email: ${ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)"}`,
1849
- `Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
1850
- ];
1851
- return lines.join("\n");
1852
- }
1853
- async function ensureTeamPackAccessWithRetry(args) {
1854
- if (checkRepoAccess(args.repoSlug)) return true;
1855
- const ghUser = getCurrentGhUser();
1856
- const info = buildAccessRequestInfo(ghUser, args.ssoEmail ?? null);
1857
- log.warn(`B\u1EA1n ch\u01B0a c\xF3 quy\u1EC1n access v\xE0o b\u1ED9 package ${args.repoSlug}.`);
1858
- log.info("Li\xEAn h\u1EC7 admin (Luke @nal.vn) \u0111\u1EC3 \u0111\u01B0\u1EE3c add v\xE0o org nalvn.");
1859
- log.plain("");
1860
- log.plain(info);
1861
- log.plain("");
1862
- await copyInfoToClipboardWithConsent(info);
1863
- while (true) {
1864
- const action = await select4({
1865
- message: "Ti\u1EBFp t\u1EE5c?",
1866
- choices: [
1867
- { name: "\u0110\xE3 \u0111\u01B0\u1EE3c add \u2014 ki\u1EC3m tra l\u1EA1i v\xE0 ti\u1EBFp t\u1EE5c", value: "retry" },
1868
- { name: "T\u1EA1m ng\u01B0ng \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau", value: "abort" }
1869
- ]
1870
- });
1871
- if (action === "abort") {
1872
- log.dim("T\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\xE3 accept invite t\u1EEB GitHub.");
1873
- return false;
1874
- }
1875
- log.info("Ki\u1EC3m tra access...");
1876
- if (checkRepoAccess(args.repoSlug)) {
1877
- log.success("\u0110\xE3 c\xF3 access \u2014 ti\u1EBFp t\u1EE5c.");
1878
- return true;
1879
- }
1880
- 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).");
1881
- }
1882
- }
1883
-
1884
- // src/lib/resolve-team-pack-repo-url.ts
1885
- var ORG_DEFAULT = "https://github.com/nalvn/team-ai-pack.git";
1886
- function resolveTeamPackRepoUrl() {
1887
- if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
1888
- return process.env.AVATAR_TEAM_PACK_REPO_URL;
1889
- }
1890
- return ORG_DEFAULT;
1891
- }
1892
-
1893
- // src/lib/team-pack-submodule-manager.ts
1894
- var TEAM_PACK_REPO_URL = resolveTeamPackRepoUrl();
1895
- var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
1896
- var TeamPackAccessAbortedError = class extends Error {
1897
- constructor(message) {
1898
- super(message);
1899
- this.name = "TeamPackAccessAbortedError";
1900
- }
1901
- };
1902
- async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
1903
- const url = resolveTeamPackRepoUrl();
1904
- const repoSlug = parseRepoSlugFromGitUrl(url);
1905
- if (repoSlug) {
1906
- const hasAccess = await ensureTeamPackAccessWithRetry({ repoSlug, ssoEmail });
1907
- if (!hasAccess) {
1908
- throw new TeamPackAccessAbortedError(
1909
- "User ch\u1ECDn t\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\u01B0\u1EE3c add v\xE0o org."
1910
- );
1911
- }
1912
- }
1913
- try {
1914
- await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
1915
- } catch (err) {
1916
- const msg = err instanceof Error ? err.message : String(err);
1917
- if (msg.includes("Repository not found") || msg.includes("not found")) {
1918
- log.error(
1919
- `Repo team-ai-pack kh\xF4ng t\u1ED3n t\u1EA1i: ${url}
1920
- C\xE1ch fix:
1921
- 1. T\u1EA1o repo: gh repo create <owner>/team-ai-pack --private --add-readme
1922
- 2. Ho\u1EB7c override URL: export AVATAR_TEAM_PACK_REPO_URL=<url-repo-c\u1EE7a-b\u1EA1n>
1923
- 3. Ho\u1EB7c d\xF9ng flag --skip-team-pack`
1924
- );
1925
- }
1926
- throw err;
1927
- }
1928
- let target = tag ?? null;
1929
- if (!target) {
1930
- target = await latestTag(join14(projectRoot, TEAM_PACK_RELATIVE_PATH));
1931
- }
1932
- if (target) {
1933
- await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
1934
- }
1935
- return { pinnedTag: target };
1936
- }
1937
- async function readPinnedPackVersion(projectRoot) {
1938
- const submoduleRoot = join14(projectRoot, TEAM_PACK_RELATIVE_PATH);
1939
- const tag = await latestTag(submoduleRoot);
1940
- if (tag) return tag;
1941
- const sha = await currentCommitSha(submoduleRoot);
1942
- return sha.slice(0, 7);
1943
- }
1944
-
1945
2138
  // src/commands/init-conflict-detection-helpers.ts
1946
2139
  import { readdir } from "fs/promises";
1947
2140
  import { join as join15 } from "path";
@@ -2200,6 +2393,14 @@ function registerInitCommand(program2) {
2200
2393
  log.dim(err.message);
2201
2394
  process.exit(0);
2202
2395
  }
2396
+ if (err instanceof RemoteAccessAbortedError) {
2397
+ log.dim(err.message);
2398
+ process.exit(0);
2399
+ }
2400
+ if (err instanceof UserAbortedRecoveryError) {
2401
+ log.dim(err.message);
2402
+ process.exit(0);
2403
+ }
2203
2404
  log.error(err instanceof Error ? err.message : String(err));
2204
2405
  process.exit(1);
2205
2406
  }
@@ -2211,13 +2412,26 @@ async function runInit(opts) {
2211
2412
  log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
2212
2413
  }
2213
2414
  let userConfig = await readUserConfig();
2214
- if (!userConfig || isTokenExpired(userConfig)) {
2415
+ while (!userConfig || isTokenExpired(userConfig)) {
2215
2416
  log.info("Ch\u01B0a \u0111\u0103ng nh\u1EADp \u2014 ch\u1EA1y login flow tr\u01B0\u1EDBc khi init...");
2216
- await runLogin({});
2417
+ try {
2418
+ await runLogin({});
2419
+ } catch (err) {
2420
+ log.warn(`Login fail: ${err.message}`);
2421
+ }
2217
2422
  userConfig = await readUserConfig();
2218
- if (!userConfig || isTokenExpired(userConfig)) {
2219
- log.error("Login kh\xF4ng ho\xE0n t\u1EA5t. Ch\u1EA1y 'avatar login' tay r\u1ED3i init l\u1EA1i.");
2220
- process.exit(1);
2423
+ if (userConfig && !isTokenExpired(userConfig)) break;
2424
+ const action = await promptRetryOrSkip({
2425
+ taskName: "\u0110\u0103ng nh\u1EADp SSO Google",
2426
+ reason: "Token ch\u01B0a \u0111\u01B0\u1EE3c t\u1EA1o ho\u1EB7c \u0111\xE3 h\u1EBFt h\u1EA1n.",
2427
+ allowSkip: false,
2428
+ // Login bắt buộc, không skip được.
2429
+ hint: "\u0110\u1EA3m b\u1EA3o b\u1EA1n ch\u1ECDn 'Allow' tr\xEAn browser v\xE0 d\xF9ng email @nal.vn."
2430
+ });
2431
+ if (action === "abort") {
2432
+ throw new UserAbortedRecoveryError(
2433
+ "User abort t\u1EA1i b\u01B0\u1EDBc login. Ch\u1EA1y 'avatar login' tay r\u1ED3i 'avatar init' l\u1EA1i."
2434
+ );
2221
2435
  }
2222
2436
  }
2223
2437
  const status = opts.projectStatus ?? await promptProjectStatus();
@@ -2234,7 +2448,7 @@ async function runInit(opts) {
2234
2448
  }
2235
2449
  }
2236
2450
  async function promptProjectStatus() {
2237
- return await select5({
2451
+ return await select7({
2238
2452
  message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
2239
2453
  choices: [
2240
2454
  { name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
@@ -2244,14 +2458,14 @@ async function promptProjectStatus() {
2244
2458
  });
2245
2459
  }
2246
2460
  async function runInitFromExistingRemote(opts, ownerEmail) {
2247
- const remoteUrl = opts.clientRepo ?? await input2({
2461
+ const remoteUrl = opts.clientRepo ?? await input3({
2248
2462
  message: "URL git c\u1EE7a repo:",
2249
2463
  validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
2250
2464
  });
2251
2465
  await ensureGitHubReady(remoteUrl);
2252
2466
  const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
2253
2467
  const inferredName = inferWorkspaceName(remoteUrl);
2254
- const workspaceName = opts.workspaceName ?? await input2({ message: "T\xEAn workspace:", default: inferredName });
2468
+ const workspaceName = opts.workspaceName ?? await input3({ message: "T\xEAn workspace:", default: inferredName });
2255
2469
  const workspaceParent = resolve(opts.workspaceParent ?? ".");
2256
2470
  const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
2257
2471
  await scaffoldWorkspaceWithSrcSubmodule({
@@ -2273,7 +2487,7 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
2273
2487
  }
2274
2488
  async function runInitFromExistingFolder(opts, ownerEmail) {
2275
2489
  const folderPath = resolve(
2276
- opts.folderPath ?? await input2({
2490
+ opts.folderPath ?? await input3({
2277
2491
  message: "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3:",
2278
2492
  validate: (v) => v.length > 0 ? true : "Path b\u1EAFt bu\u1ED9c"
2279
2493
  })
@@ -2285,7 +2499,7 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
2285
2499
  const remoteUrl = await getOrCreateOriginRemote(folderPath, opts);
2286
2500
  const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
2287
2501
  const inferredName = opts.workspaceName ?? `${basename(folderPath)}-avatar-workspace`;
2288
- const workspaceName = opts.workspaceName ?? await input2({ message: "T\xEAn workspace:", default: inferredName });
2502
+ const workspaceName = opts.workspaceName ?? await input3({ message: "T\xEAn workspace:", default: inferredName });
2289
2503
  const workspaceParent = resolve(opts.workspaceParent ?? ".");
2290
2504
  const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
2291
2505
  await scaffoldWorkspaceWithSrcSubmodule({
@@ -2308,11 +2522,11 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
2308
2522
  }
2309
2523
  async function runInitFromScratch(opts, ownerEmail) {
2310
2524
  await ensureGitHubReady();
2311
- const projectName = opts.workspaceName ?? await input2({
2525
+ const projectName = opts.workspaceName ?? await input3({
2312
2526
  message: "T\xEAn d\u1EF1 \xE1n:",
2313
2527
  validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
2314
2528
  });
2315
- const visibility = opts.repoVisibility ?? await select5({
2529
+ const visibility = opts.repoVisibility ?? await select7({
2316
2530
  message: "Visibility?",
2317
2531
  choices: [
2318
2532
  { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
@@ -2340,7 +2554,10 @@ async function runInitFromScratch(opts, ownerEmail) {
2340
2554
  await git(workspacePath).subModule(["add", urls.sshUrl, "src"]);
2341
2555
  let pinnedTag = "HEAD";
2342
2556
  if (!opts.skipTeamPack) {
2343
- const result = await addTeamPackSubmodule(workspacePath, opts.packVersion);
2557
+ const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
2558
+ workspacePath,
2559
+ opts.packVersion
2560
+ );
2344
2561
  pinnedTag = result.pinnedTag ?? "HEAD";
2345
2562
  sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
2346
2563
  } else {
@@ -2381,14 +2598,14 @@ async function getOrCreateOriginRemote(folderPath, opts) {
2381
2598
  return void 0;
2382
2599
  }
2383
2600
  await ensureGitHubReady();
2384
- const visibility = opts.repoVisibility ?? await select5({
2601
+ const visibility = opts.repoVisibility ?? await select7({
2385
2602
  message: "Visibility?",
2386
2603
  choices: [
2387
2604
  { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
2388
2605
  { name: "public", value: "public" }
2389
2606
  ]
2390
2607
  });
2391
- const repoName = await input2({
2608
+ const repoName = await input3({
2392
2609
  message: "T\xEAn repo:",
2393
2610
  default: basename(folderPath)
2394
2611
  });
@@ -2410,7 +2627,10 @@ async function scaffoldWorkspaceWithSrcSubmodule(args) {
2410
2627
  await git(args.workspacePath).subModule(["add", args.srcRemoteUrl, "src"]);
2411
2628
  let pinnedTag = "HEAD";
2412
2629
  if (!args.skipTeamPack) {
2413
- const result = await addTeamPackSubmodule(args.workspacePath, args.packVersion);
2630
+ const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
2631
+ args.workspacePath,
2632
+ args.packVersion
2633
+ );
2414
2634
  pinnedTag = result.pinnedTag ?? "HEAD";
2415
2635
  sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
2416
2636
  } else {
@@ -2478,43 +2698,79 @@ async function maybeCreateWorkspaceRemote(args) {
2478
2698
  });
2479
2699
  }
2480
2700
  if (!shouldCreate) return;
2481
- const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select5({
2701
+ const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select7({
2482
2702
  message: "Workspace visibility?",
2483
2703
  choices: [
2484
2704
  { name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
2485
2705
  { name: "public", value: "public" }
2486
2706
  ]
2487
2707
  }));
2488
- try {
2489
- await createWorkspaceRemoteViaGh({
2490
- workspacePath: args.workspacePath,
2491
- workspaceName: args.workspaceName,
2492
- visibility,
2493
- org: args.repoOrg
2494
- });
2495
- } catch (err) {
2496
- log.warn(err instanceof Error ? err.message : String(err));
2497
- log.warn("Workspace v\u1EABn s\u1EB5n s\xE0ng local-only. Setup remote sau khi c\u1EA7n.");
2708
+ while (true) {
2709
+ try {
2710
+ await createWorkspaceRemoteViaGh({
2711
+ workspacePath: args.workspacePath,
2712
+ workspaceName: args.workspaceName,
2713
+ visibility,
2714
+ org: args.repoOrg
2715
+ });
2716
+ return;
2717
+ } catch (err) {
2718
+ const action = await promptRetryOrSkip({
2719
+ taskName: "T\u1EA1o workspace remote tr\xEAn GitHub",
2720
+ reason: err instanceof Error ? err.message : String(err),
2721
+ allowSkip: true,
2722
+ // Workspace remote OPTIONAL — skip OK, workspace local vẫn dùng được.
2723
+ hint: "Tip: sai org? Pass --repo-org=<your-gh-user>. Ho\u1EB7c switch gh: gh auth login."
2724
+ });
2725
+ if (action === "abort") {
2726
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc t\u1EA1o workspace remote.");
2727
+ }
2728
+ if (action === "skip") {
2729
+ log.warn("Workspace v\u1EABn s\u1EB5n s\xE0ng local-only. Setup remote sau khi c\u1EA7n.");
2730
+ return;
2731
+ }
2732
+ }
2498
2733
  }
2499
2734
  }
2500
2735
  async function resolveWorkspacePath(parent, desiredName, force) {
2501
2736
  const desired = join16(parent, desiredName);
2502
2737
  if (await isEmptyOrMissing(desired)) return desired;
2503
- const alternative = await findAlternativeWorkspaceName(parent, desiredName);
2504
- if (!alternative) {
2505
- throw new Error(`Kh\xF4ng t\xECm \u0111\u01B0\u1EE3c workspace path kh\u1EA3 d\u1EE5ng trong ${parent}`);
2506
- }
2507
2738
  log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
2508
- if (force) {
2509
- log.info(`--force: d\xF9ng ${alternative}`);
2510
- return alternative;
2739
+ while (true) {
2740
+ const alternative = await findAlternativeWorkspaceName(parent, desiredName);
2741
+ if (force && alternative) {
2742
+ log.info(`--force: d\xF9ng ${alternative}`);
2743
+ return alternative;
2744
+ }
2745
+ const choices = [];
2746
+ if (alternative) {
2747
+ choices.push({ name: `D\xF9ng "${alternative}" (suggest)`, value: "use-alt" });
2748
+ }
2749
+ choices.push({ name: "Nh\u1EADp t\xEAn workspace kh\xE1c (manual)", value: "manual" });
2750
+ choices.push({ name: "T\u1EA1m ng\u01B0ng init", value: "abort" });
2751
+ const action = await select7({
2752
+ message: "C\xE1ch x\u1EED l\xFD workspace path conflict?",
2753
+ choices
2754
+ });
2755
+ if (action === "abort") {
2756
+ throw new UserAbortedRecoveryError(
2757
+ "User abort t\u1EA1i b\u01B0\u1EDBc resolve workspace path. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c."
2758
+ );
2759
+ }
2760
+ if (action === "use-alt" && alternative) {
2761
+ return alternative;
2762
+ }
2763
+ const newName = await input3({
2764
+ message: "T\xEAn workspace m\u1EDBi:",
2765
+ validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
2766
+ });
2767
+ const newPath = join16(parent, newName.trim());
2768
+ if (await isEmptyOrMissing(newPath)) return newPath;
2769
+ log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
2511
2770
  }
2512
- const useAlt = await confirm3({ message: `D\xF9ng "${alternative}" thay th\u1EBF?`, default: true });
2513
- if (!useAlt) throw new Error("H\u1EE7y init. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c.");
2514
- return alternative;
2515
2771
  }
2516
2772
  async function promptTeamOwner(currentUserEmail) {
2517
- return await input2({ message: "Team owner email:", default: currentUserEmail });
2773
+ return await input3({ message: "Team owner email:", default: currentUserEmail });
2518
2774
  }
2519
2775
  async function maybeCommitWorkspace(workspacePath, skipCommit) {
2520
2776
  if (skipCommit) {
@@ -2839,7 +3095,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
2839
3095
  }
2840
3096
 
2841
3097
  // src/commands/uninstall.ts
2842
- var CLI_VERSION = "1.2.3";
3098
+ var CLI_VERSION = "1.2.5";
2843
3099
  function registerUninstallCommand(program2) {
2844
3100
  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) => {
2845
3101
  try {
@@ -2921,7 +3177,7 @@ function printUninstallSuccessBox(backupPath) {
2921
3177
  }
2922
3178
 
2923
3179
  // src/index.ts
2924
- var CLI_VERSION2 = "1.2.3";
3180
+ var CLI_VERSION2 = "1.2.5";
2925
3181
  var program = new Command();
2926
3182
  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(
2927
3183
  "beforeAll",