@nalvietnam/avatar-cli 1.2.4 → 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 select6 } 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 spawnSync15 } 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
  });
@@ -1377,11 +1567,11 @@ function detectPackageManager() {
1377
1567
  }
1378
1568
 
1379
1569
  // 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";
1570
+ import { spawnSync as spawnSync12 } from "child_process";
1571
+ import { select as select5 } from "@inquirer/prompts";
1382
1572
 
1383
1573
  // src/lib/verify-git-remote-accessible.ts
1384
- import { spawnSync as spawnSync10 } from "child_process";
1574
+ import { spawnSync as spawnSync11 } from "child_process";
1385
1575
  var TIMEOUT_MS = 5e3;
1386
1576
  function classifyRemoteError(stderr) {
1387
1577
  const text = stderr.toLowerCase();
@@ -1397,7 +1587,7 @@ function classifyRemoteError(stderr) {
1397
1587
  return "unknown";
1398
1588
  }
1399
1589
  function tryVerifyGitRemoteAccessible(url) {
1400
- const r = spawnSync10("git", ["ls-remote", "--exit-code", url, "HEAD"], {
1590
+ const r = spawnSync11("git", ["ls-remote", "--exit-code", url, "HEAD"], {
1401
1591
  encoding: "utf8",
1402
1592
  timeout: TIMEOUT_MS,
1403
1593
  stdio: ["ignore", "pipe", "pipe"]
@@ -1418,8 +1608,8 @@ var RemoteAccessAbortedError = class extends Error {
1418
1608
  this.name = "RemoteAccessAbortedError";
1419
1609
  }
1420
1610
  };
1421
- function getCurrentGhUser() {
1422
- const r = spawnSync11("gh", ["api", "user", "--jq", ".login"], {
1611
+ function getCurrentGhUser2() {
1612
+ const r = spawnSync12("gh", ["api", "user", "--jq", ".login"], {
1423
1613
  encoding: "utf8",
1424
1614
  stdio: ["ignore", "pipe", "pipe"]
1425
1615
  });
@@ -1428,7 +1618,7 @@ function getCurrentGhUser() {
1428
1618
  }
1429
1619
  function triggerGhAuthLoginInteractive() {
1430
1620
  log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
1431
- const r = spawnSync11("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1621
+ const r = spawnSync12("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1432
1622
  if (r.status !== 0) {
1433
1623
  log.warn(`gh auth login exit ${r.status}. B\u1EA1n c\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
1434
1624
  }
@@ -1451,12 +1641,12 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
1451
1641
  let reason = args.initialReason;
1452
1642
  let detail = args.initialDetail;
1453
1643
  while (true) {
1454
- const ghUser = getCurrentGhUser();
1644
+ const ghUser = getCurrentGhUser2();
1455
1645
  log.warn(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c ${args.url}`);
1456
1646
  log.dim(` L\xFD do: ${reason}${detail ? ` \u2014 ${detail.slice(0, 150)}` : ""}`);
1457
1647
  log.info(getReasonHint(reason, args.url, ghUser));
1458
1648
  if (ghUser) log.dim(` gh CLI hi\u1EC7n \u0111ang login: ${ghUser}`);
1459
- const action = await select3({
1649
+ const action = await select5({
1460
1650
  message: "C\xE1ch x\u1EED l\xFD?",
1461
1651
  choices: [
1462
1652
  {
@@ -1493,7 +1683,7 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
1493
1683
  }
1494
1684
 
1495
1685
  // src/lib/install-gh-cli-via-package-manager.ts
1496
- import { spawnSync as spawnSync12 } from "child_process";
1686
+ import { spawnSync as spawnSync13 } from "child_process";
1497
1687
  var INSTALL_COMMANDS = {
1498
1688
  brew: { cmd: "brew", args: ["install", "gh"] },
1499
1689
  apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
@@ -1504,7 +1694,7 @@ var INSTALL_COMMANDS = {
1504
1694
  function installGhCliViaPackageManager(pm) {
1505
1695
  const spec = INSTALL_COMMANDS[pm];
1506
1696
  log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
1507
- const r = spawnSync12(spec.cmd, spec.args, { stdio: "inherit" });
1697
+ const r = spawnSync13(spec.cmd, spec.args, { stdio: "inherit" });
1508
1698
  if (r.status !== 0) {
1509
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.`);
1510
1700
  }
@@ -1512,9 +1702,9 @@ function installGhCliViaPackageManager(pm) {
1512
1702
  }
1513
1703
 
1514
1704
  // src/lib/setup-git-credential-via-gh.ts
1515
- import { spawnSync as spawnSync13 } from "child_process";
1705
+ import { spawnSync as spawnSync14 } from "child_process";
1516
1706
  function setupGitCredentialViaGh() {
1517
- const r = spawnSync13("gh", ["auth", "setup-git"], { stdio: "ignore" });
1707
+ const r = spawnSync14("gh", ["auth", "setup-git"], { stdio: "ignore" });
1518
1708
  if (r.status !== 0) {
1519
1709
  log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
1520
1710
  return;
@@ -1523,10 +1713,10 @@ function setupGitCredentialViaGh() {
1523
1713
  }
1524
1714
 
1525
1715
  // src/lib/trigger-gh-cli-auth-login.ts
1526
- import { spawnSync as spawnSync14 } from "child_process";
1716
+ import { spawnSync as spawnSync15 } from "child_process";
1527
1717
  function triggerGhCliAuthLogin() {
1528
1718
  log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
1529
- const r = spawnSync14(
1719
+ const r = spawnSync15(
1530
1720
  "gh",
1531
1721
  ["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
1532
1722
  { stdio: "inherit" }
@@ -1539,24 +1729,61 @@ function triggerGhCliAuthLogin() {
1539
1729
 
1540
1730
  // src/lib/git-auth-and-install-orchestrator.ts
1541
1731
  async function ensureGitHubReady(remoteUrl) {
1542
- let state = checkGhCliAuthStatus();
1543
- if (state === "not-installed") {
1732
+ while (checkGhCliAuthStatus() === "not-installed") {
1544
1733
  log.warn("gh CLI ch\u01B0a c\xE0i. Avatar s\u1EBD t\u1EF1 c\xE0i.");
1545
1734
  const pm = detectPackageManager();
1546
1735
  if (!pm) {
1547
- throw new Error(
1548
- "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"
1549
- );
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
+ }
1550
1759
  }
1551
- installGhCliViaPackageManager(pm);
1552
- state = checkGhCliAuthStatus();
1553
1760
  }
1554
- if (state === "not-authenticated") {
1761
+ while (checkGhCliAuthStatus() === "not-authenticated") {
1555
1762
  log.warn("Ch\u01B0a \u0111\u0103ng nh\u1EADp GitHub.");
1556
- triggerGhCliAuthLogin();
1557
- state = checkGhCliAuthStatus();
1558
- if (state !== "authenticated") {
1559
- 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
+ }
1560
1787
  }
1561
1788
  }
1562
1789
  log.success("gh CLI s\u1EB5n s\xE0ng");
@@ -1578,13 +1805,13 @@ async function ensureGitHubReady(remoteUrl) {
1578
1805
  // src/lib/create-workspace-remote-via-gh.ts
1579
1806
  function canCreateInNamespace(org, ghUser) {
1580
1807
  if (org.toLowerCase() === ghUser.toLowerCase()) return { ok: true };
1581
- const r = spawnSync15("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
1808
+ const r = spawnSync16("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
1582
1809
  stdio: "ignore"
1583
1810
  });
1584
1811
  if (r.status === 0) return { ok: true };
1585
- const orgCheck = spawnSync15("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
1812
+ const orgCheck = spawnSync16("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
1586
1813
  if (orgCheck.status !== 0) {
1587
- const userCheck = spawnSync15("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
1814
+ const userCheck = spawnSync16("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
1588
1815
  if (userCheck.status === 0) {
1589
1816
  return {
1590
1817
  ok: false,
@@ -1601,27 +1828,27 @@ function canCreateInNamespace(org, ghUser) {
1601
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.`
1602
1829
  };
1603
1830
  }
1604
- async function createWorkspaceRemoteViaGh(input3) {
1605
- validateRepoName(input3.workspaceName);
1606
- validateRepoVisibility(input3.visibility);
1831
+ async function createWorkspaceRemoteViaGh(input4) {
1832
+ validateRepoName(input4.workspaceName);
1833
+ validateRepoVisibility(input4.visibility);
1607
1834
  await ensureGitHubReady();
1608
1835
  const ghUser = resolveGithubUsernameDefault();
1609
- const org = input3.org ?? ghUser;
1836
+ const org = input4.org ?? ghUser;
1610
1837
  const namespaceCheck = canCreateInNamespace(org, ghUser);
1611
1838
  if (!namespaceCheck.ok) {
1612
1839
  throw new Error(`Kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi '${org}/': ${namespaceCheck.reason}`);
1613
1840
  }
1614
- const fullName = `${org}/${input3.workspaceName}`;
1615
- log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input3.visibility})...`);
1616
- const r = spawnSync15(
1841
+ const fullName = `${org}/${input4.workspaceName}`;
1842
+ log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input4.visibility})...`);
1843
+ const r = spawnSync16(
1617
1844
  "gh",
1618
1845
  [
1619
1846
  "repo",
1620
1847
  "create",
1621
1848
  fullName,
1622
- `--${input3.visibility}`,
1849
+ `--${input4.visibility}`,
1623
1850
  "--source",
1624
- input3.workspacePath,
1851
+ input4.workspacePath,
1625
1852
  "--remote",
1626
1853
  "origin",
1627
1854
  "--push"
@@ -1633,7 +1860,7 @@ async function createWorkspaceRemoteViaGh(input3) {
1633
1860
  if (stderr) process.stderr.write(`${stderr}
1634
1861
  `);
1635
1862
  throw new Error(
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`
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`
1637
1864
  );
1638
1865
  }
1639
1866
  const sshUrl = `git@github.com:${fullName}.git`;
@@ -1644,14 +1871,14 @@ async function createWorkspaceRemoteViaGh(input3) {
1644
1871
 
1645
1872
  // src/lib/safe-bootstrap-for-dirty-folder.ts
1646
1873
  import { readdirSync } from "fs";
1647
- import { select as select4 } from "@inquirer/prompts";
1874
+ import { select as select6 } from "@inquirer/prompts";
1648
1875
  import { simpleGit as simpleGit3 } from "simple-git";
1649
1876
 
1650
1877
  // src/lib/check-folder-has-git.ts
1651
1878
  import { existsSync as existsSync2, statSync } from "fs";
1652
- import { join as join10 } from "path";
1879
+ import { join as join11 } from "path";
1653
1880
  function checkFolderHasGit(folderPath) {
1654
- const gitPath = join10(folderPath, ".git");
1881
+ const gitPath = join11(folderPath, ".git");
1655
1882
  if (!existsSync2(gitPath)) return false;
1656
1883
  const stat = statSync(gitPath);
1657
1884
  return stat.isDirectory() || stat.isFile();
@@ -1683,7 +1910,7 @@ async function createInitialGitCommit(folderPath) {
1683
1910
 
1684
1911
  // src/lib/detect-folder-tech-stack.ts
1685
1912
  import { existsSync as existsSync3 } from "fs";
1686
- import { join as join11 } from "path";
1913
+ import { join as join12 } from "path";
1687
1914
  var SIGNATURES = {
1688
1915
  node: ["package.json"],
1689
1916
  python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
@@ -1695,7 +1922,7 @@ var SIGNATURES = {
1695
1922
  function detectFolderTechStack(folderPath) {
1696
1923
  const matched = [];
1697
1924
  for (const [stack, files] of Object.entries(SIGNATURES)) {
1698
- if (files.some((f) => existsSync3(join11(folderPath, f)))) {
1925
+ if (files.some((f) => existsSync3(join12(folderPath, f)))) {
1699
1926
  matched.push(stack);
1700
1927
  }
1701
1928
  }
@@ -1704,19 +1931,19 @@ function detectFolderTechStack(folderPath) {
1704
1931
 
1705
1932
  // src/lib/gitignore-template-loader.ts
1706
1933
  import { readFileSync as readFileSync2 } from "fs";
1707
- import { dirname as dirname3, join as join12 } from "path";
1934
+ import { dirname as dirname3, join as join13 } from "path";
1708
1935
  import { fileURLToPath as fileURLToPath2 } from "url";
1709
1936
  var __dirname = dirname3(fileURLToPath2(import.meta.url));
1710
1937
  var CANDIDATE_DIRS = [
1711
- join12(__dirname, "..", "templates", "gitignore"),
1712
- join12(__dirname, "..", "..", "src", "templates", "gitignore")
1938
+ join13(__dirname, "..", "templates", "gitignore"),
1939
+ join13(__dirname, "..", "..", "src", "templates", "gitignore")
1713
1940
  ];
1714
1941
  var AVATAR_MARKER_START = "# === avatar ===";
1715
1942
  var AVATAR_MARKER_END = "# === /avatar ===";
1716
1943
  function readTemplate(stack) {
1717
1944
  for (const dir of CANDIDATE_DIRS) {
1718
1945
  try {
1719
- return readFileSync2(join12(dir, `${stack}.txt`), "utf8");
1946
+ return readFileSync2(join13(dir, `${stack}.txt`), "utf8");
1720
1947
  } catch {
1721
1948
  }
1722
1949
  }
@@ -1731,9 +1958,9 @@ ${readTemplate(s).trim()}`);
1731
1958
 
1732
1959
  // src/lib/write-or-merge-gitignore.ts
1733
1960
  import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
1734
- import { join as join13 } from "path";
1961
+ import { join as join14 } from "path";
1735
1962
  function writeOrMergeGitignore(folderPath, avatarBlock) {
1736
- const path = join13(folderPath, ".gitignore");
1963
+ const path = join14(folderPath, ".gitignore");
1737
1964
  if (!existsSync4(path)) {
1738
1965
  writeFileSync(path, avatarBlock, "utf8");
1739
1966
  return;
@@ -1775,7 +2002,7 @@ async function promptBootstrapStrategy(state, opts) {
1775
2002
  if (opts.presetStrategy) return opts.presetStrategy;
1776
2003
  if (opts.autoYes) return "stash";
1777
2004
  if (state === "empty" || state === "clean") return "commit-all";
1778
- return await select4({
2005
+ return await select6({
1779
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:",
1780
2007
  choices: [
1781
2008
  {
@@ -1908,145 +2135,6 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
1908
2135
  await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
1909
2136
  }
1910
2137
 
1911
- // src/lib/team-pack-submodule-manager.ts
1912
- import { join as join14 } from "path";
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
-
1989
- // src/lib/resolve-team-pack-repo-url.ts
1990
- var ORG_DEFAULT = "https://github.com/nalvn/team-ai-pack.git";
1991
- function resolveTeamPackRepoUrl() {
1992
- if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
1993
- return process.env.AVATAR_TEAM_PACK_REPO_URL;
1994
- }
1995
- return ORG_DEFAULT;
1996
- }
1997
-
1998
- // src/lib/team-pack-submodule-manager.ts
1999
- var TEAM_PACK_REPO_URL = resolveTeamPackRepoUrl();
2000
- var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
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) {
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
- }
2018
- try {
2019
- await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
2020
- } catch (err) {
2021
- const msg = err instanceof Error ? err.message : String(err);
2022
- if (msg.includes("Repository not found") || msg.includes("not found")) {
2023
- log.error(
2024
- `Repo team-ai-pack kh\xF4ng t\u1ED3n t\u1EA1i: ${url}
2025
- C\xE1ch fix:
2026
- 1. T\u1EA1o repo: gh repo create <owner>/team-ai-pack --private --add-readme
2027
- 2. Ho\u1EB7c override URL: export AVATAR_TEAM_PACK_REPO_URL=<url-repo-c\u1EE7a-b\u1EA1n>
2028
- 3. Ho\u1EB7c d\xF9ng flag --skip-team-pack`
2029
- );
2030
- }
2031
- throw err;
2032
- }
2033
- let target = tag ?? null;
2034
- if (!target) {
2035
- target = await latestTag(join14(projectRoot, TEAM_PACK_RELATIVE_PATH));
2036
- }
2037
- if (target) {
2038
- await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
2039
- }
2040
- return { pinnedTag: target };
2041
- }
2042
- async function readPinnedPackVersion(projectRoot) {
2043
- const submoduleRoot = join14(projectRoot, TEAM_PACK_RELATIVE_PATH);
2044
- const tag = await latestTag(submoduleRoot);
2045
- if (tag) return tag;
2046
- const sha = await currentCommitSha(submoduleRoot);
2047
- return sha.slice(0, 7);
2048
- }
2049
-
2050
2138
  // src/commands/init-conflict-detection-helpers.ts
2051
2139
  import { readdir } from "fs/promises";
2052
2140
  import { join as join15 } from "path";
@@ -2309,6 +2397,10 @@ function registerInitCommand(program2) {
2309
2397
  log.dim(err.message);
2310
2398
  process.exit(0);
2311
2399
  }
2400
+ if (err instanceof UserAbortedRecoveryError) {
2401
+ log.dim(err.message);
2402
+ process.exit(0);
2403
+ }
2312
2404
  log.error(err instanceof Error ? err.message : String(err));
2313
2405
  process.exit(1);
2314
2406
  }
@@ -2320,13 +2412,26 @@ async function runInit(opts) {
2320
2412
  log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
2321
2413
  }
2322
2414
  let userConfig = await readUserConfig();
2323
- if (!userConfig || isTokenExpired(userConfig)) {
2415
+ while (!userConfig || isTokenExpired(userConfig)) {
2324
2416
  log.info("Ch\u01B0a \u0111\u0103ng nh\u1EADp \u2014 ch\u1EA1y login flow tr\u01B0\u1EDBc khi init...");
2325
- await runLogin({});
2417
+ try {
2418
+ await runLogin({});
2419
+ } catch (err) {
2420
+ log.warn(`Login fail: ${err.message}`);
2421
+ }
2326
2422
  userConfig = await readUserConfig();
2327
- if (!userConfig || isTokenExpired(userConfig)) {
2328
- log.error("Login kh\xF4ng ho\xE0n t\u1EA5t. Ch\u1EA1y 'avatar login' tay r\u1ED3i init l\u1EA1i.");
2329
- 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
+ );
2330
2435
  }
2331
2436
  }
2332
2437
  const status = opts.projectStatus ?? await promptProjectStatus();
@@ -2343,7 +2448,7 @@ async function runInit(opts) {
2343
2448
  }
2344
2449
  }
2345
2450
  async function promptProjectStatus() {
2346
- return await select6({
2451
+ return await select7({
2347
2452
  message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
2348
2453
  choices: [
2349
2454
  { name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
@@ -2353,14 +2458,14 @@ async function promptProjectStatus() {
2353
2458
  });
2354
2459
  }
2355
2460
  async function runInitFromExistingRemote(opts, ownerEmail) {
2356
- const remoteUrl = opts.clientRepo ?? await input2({
2461
+ const remoteUrl = opts.clientRepo ?? await input3({
2357
2462
  message: "URL git c\u1EE7a repo:",
2358
2463
  validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
2359
2464
  });
2360
2465
  await ensureGitHubReady(remoteUrl);
2361
2466
  const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
2362
2467
  const inferredName = inferWorkspaceName(remoteUrl);
2363
- 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 });
2364
2469
  const workspaceParent = resolve(opts.workspaceParent ?? ".");
2365
2470
  const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
2366
2471
  await scaffoldWorkspaceWithSrcSubmodule({
@@ -2382,7 +2487,7 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
2382
2487
  }
2383
2488
  async function runInitFromExistingFolder(opts, ownerEmail) {
2384
2489
  const folderPath = resolve(
2385
- opts.folderPath ?? await input2({
2490
+ opts.folderPath ?? await input3({
2386
2491
  message: "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3:",
2387
2492
  validate: (v) => v.length > 0 ? true : "Path b\u1EAFt bu\u1ED9c"
2388
2493
  })
@@ -2394,7 +2499,7 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
2394
2499
  const remoteUrl = await getOrCreateOriginRemote(folderPath, opts);
2395
2500
  const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
2396
2501
  const inferredName = opts.workspaceName ?? `${basename(folderPath)}-avatar-workspace`;
2397
- 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 });
2398
2503
  const workspaceParent = resolve(opts.workspaceParent ?? ".");
2399
2504
  const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
2400
2505
  await scaffoldWorkspaceWithSrcSubmodule({
@@ -2417,11 +2522,11 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
2417
2522
  }
2418
2523
  async function runInitFromScratch(opts, ownerEmail) {
2419
2524
  await ensureGitHubReady();
2420
- const projectName = opts.workspaceName ?? await input2({
2525
+ const projectName = opts.workspaceName ?? await input3({
2421
2526
  message: "T\xEAn d\u1EF1 \xE1n:",
2422
2527
  validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
2423
2528
  });
2424
- const visibility = opts.repoVisibility ?? await select6({
2529
+ const visibility = opts.repoVisibility ?? await select7({
2425
2530
  message: "Visibility?",
2426
2531
  choices: [
2427
2532
  { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
@@ -2449,7 +2554,10 @@ async function runInitFromScratch(opts, ownerEmail) {
2449
2554
  await git(workspacePath).subModule(["add", urls.sshUrl, "src"]);
2450
2555
  let pinnedTag = "HEAD";
2451
2556
  if (!opts.skipTeamPack) {
2452
- const result = await addTeamPackSubmodule(workspacePath, opts.packVersion);
2557
+ const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
2558
+ workspacePath,
2559
+ opts.packVersion
2560
+ );
2453
2561
  pinnedTag = result.pinnedTag ?? "HEAD";
2454
2562
  sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
2455
2563
  } else {
@@ -2490,14 +2598,14 @@ async function getOrCreateOriginRemote(folderPath, opts) {
2490
2598
  return void 0;
2491
2599
  }
2492
2600
  await ensureGitHubReady();
2493
- const visibility = opts.repoVisibility ?? await select6({
2601
+ const visibility = opts.repoVisibility ?? await select7({
2494
2602
  message: "Visibility?",
2495
2603
  choices: [
2496
2604
  { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
2497
2605
  { name: "public", value: "public" }
2498
2606
  ]
2499
2607
  });
2500
- const repoName = await input2({
2608
+ const repoName = await input3({
2501
2609
  message: "T\xEAn repo:",
2502
2610
  default: basename(folderPath)
2503
2611
  });
@@ -2519,7 +2627,10 @@ async function scaffoldWorkspaceWithSrcSubmodule(args) {
2519
2627
  await git(args.workspacePath).subModule(["add", args.srcRemoteUrl, "src"]);
2520
2628
  let pinnedTag = "HEAD";
2521
2629
  if (!args.skipTeamPack) {
2522
- const result = await addTeamPackSubmodule(args.workspacePath, args.packVersion);
2630
+ const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
2631
+ args.workspacePath,
2632
+ args.packVersion
2633
+ );
2523
2634
  pinnedTag = result.pinnedTag ?? "HEAD";
2524
2635
  sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
2525
2636
  } else {
@@ -2587,43 +2698,79 @@ async function maybeCreateWorkspaceRemote(args) {
2587
2698
  });
2588
2699
  }
2589
2700
  if (!shouldCreate) return;
2590
- const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select6({
2701
+ const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select7({
2591
2702
  message: "Workspace visibility?",
2592
2703
  choices: [
2593
2704
  { name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
2594
2705
  { name: "public", value: "public" }
2595
2706
  ]
2596
2707
  }));
2597
- try {
2598
- await createWorkspaceRemoteViaGh({
2599
- workspacePath: args.workspacePath,
2600
- workspaceName: args.workspaceName,
2601
- visibility,
2602
- org: args.repoOrg
2603
- });
2604
- } catch (err) {
2605
- log.warn(err instanceof Error ? err.message : String(err));
2606
- 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
+ }
2607
2733
  }
2608
2734
  }
2609
2735
  async function resolveWorkspacePath(parent, desiredName, force) {
2610
2736
  const desired = join16(parent, desiredName);
2611
2737
  if (await isEmptyOrMissing(desired)) return desired;
2612
- const alternative = await findAlternativeWorkspaceName(parent, desiredName);
2613
- if (!alternative) {
2614
- throw new Error(`Kh\xF4ng t\xECm \u0111\u01B0\u1EE3c workspace path kh\u1EA3 d\u1EE5ng trong ${parent}`);
2615
- }
2616
2738
  log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
2617
- if (force) {
2618
- log.info(`--force: d\xF9ng ${alternative}`);
2619
- 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.`);
2620
2770
  }
2621
- const useAlt = await confirm3({ message: `D\xF9ng "${alternative}" thay th\u1EBF?`, default: true });
2622
- if (!useAlt) throw new Error("H\u1EE7y init. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c.");
2623
- return alternative;
2624
2771
  }
2625
2772
  async function promptTeamOwner(currentUserEmail) {
2626
- return await input2({ message: "Team owner email:", default: currentUserEmail });
2773
+ return await input3({ message: "Team owner email:", default: currentUserEmail });
2627
2774
  }
2628
2775
  async function maybeCommitWorkspace(workspacePath, skipCommit) {
2629
2776
  if (skipCommit) {
@@ -2948,7 +3095,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
2948
3095
  }
2949
3096
 
2950
3097
  // src/commands/uninstall.ts
2951
- var CLI_VERSION = "1.2.4";
3098
+ var CLI_VERSION = "1.2.5";
2952
3099
  function registerUninstallCommand(program2) {
2953
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) => {
2954
3101
  try {
@@ -3030,7 +3177,7 @@ function printUninstallSuccessBox(backupPath) {
3030
3177
  }
3031
3178
 
3032
3179
  // src/index.ts
3033
- var CLI_VERSION2 = "1.2.4";
3180
+ var CLI_VERSION2 = "1.2.5";
3034
3181
  var program = new Command();
3035
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(
3036
3183
  "beforeAll",