@nalvietnam/avatar-cli 1.2.4 → 1.2.6

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,8 +1194,240 @@ 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";
1198
- import boxen3 from "boxen";
1197
+ import { confirm as confirm3, input as input3, select as select7 } from "@inquirer/prompts";
1198
+ import boxen4 from "boxen";
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
+ import boxen2 from "boxen";
1231
+ function parseRepoSlugFromGitUrl(url) {
1232
+ const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
1233
+ if (httpsMatch) return httpsMatch[1];
1234
+ return null;
1235
+ }
1236
+ function checkRepoAccess(repoSlug) {
1237
+ const r = spawnSync6("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
1238
+ return r.status === 0;
1239
+ }
1240
+ function getCurrentGhUser() {
1241
+ const r = spawnSync6("gh", ["api", "user", "--jq", ".login"], {
1242
+ encoding: "utf8",
1243
+ stdio: ["ignore", "pipe", "pipe"]
1244
+ });
1245
+ if (r.status !== 0) return null;
1246
+ return r.stdout.trim() || null;
1247
+ }
1248
+ function triggerGhAuthLoginInteractive() {
1249
+ log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
1250
+ const r = spawnSync6("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1251
+ if (r.status !== 0) {
1252
+ log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
1253
+ }
1254
+ }
1255
+ async function copyInfoToClipboardWithConsent(info) {
1256
+ const ok = await confirm2({
1257
+ message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
1258
+ default: true
1259
+ });
1260
+ if (!ok) return;
1261
+ try {
1262
+ const { default: clipboardy } = await import("clipboardy");
1263
+ await clipboardy.write(info);
1264
+ log.success("\u0110\xE3 copy v\xE0o clipboard");
1265
+ } catch (err) {
1266
+ log.dim(`Copy clipboard fail: ${err.message}`);
1267
+ }
1268
+ }
1269
+ function printAccessWarningBox(repoSlug, ghUser, ssoEmail) {
1270
+ const lines = [
1271
+ `${chalk.red("\u26D4 KH\xD4NG C\xD3 QUY\u1EC0N ACCESS")}`,
1272
+ "",
1273
+ `Repo: ${chalk.bold(repoSlug)}`,
1274
+ "",
1275
+ "B\u1EA1n c\u1EA7n \u0111\u01B0\u1EE3c admin add v\xE0o org \u0111\u1EC3 pull team-ai-pack.",
1276
+ "",
1277
+ `${chalk.dim("Th\xF4ng tin g\u1EEDi admin:")}`,
1278
+ ` GitHub username: ${chalk.cyan(ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)")}`,
1279
+ ` NAL email: ${chalk.cyan(ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)")}`,
1280
+ ` Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
1281
+ "",
1282
+ `${chalk.dim("Li\xEAn h\u1EC7:")} luke@nal.vn (Slack #avatar-setup)`
1283
+ ];
1284
+ process.stdout.write(
1285
+ `${boxen2(lines.join("\n"), { padding: 1, borderColor: "red", borderStyle: "round" })}
1286
+ `
1287
+ );
1288
+ }
1289
+ function buildAccessRequestInfo(repoSlug, ghUser, ssoEmail) {
1290
+ return [
1291
+ `Request access ${repoSlug} (NAL)`,
1292
+ "",
1293
+ `GitHub username: ${ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)"}`,
1294
+ `NAL email: ${ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)"}`,
1295
+ `Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
1296
+ ].join("\n");
1297
+ }
1298
+ async function ensureTeamPackAccessWithRetry(args) {
1299
+ if (checkRepoAccess(args.repoSlug)) return true;
1300
+ const initialGhUser = getCurrentGhUser();
1301
+ printAccessWarningBox(args.repoSlug, initialGhUser, args.ssoEmail ?? null);
1302
+ await copyInfoToClipboardWithConsent(
1303
+ buildAccessRequestInfo(args.repoSlug, initialGhUser, args.ssoEmail ?? null)
1304
+ );
1305
+ while (true) {
1306
+ const ghUser = getCurrentGhUser();
1307
+ const ghUserDisplay = ghUser ?? "(ch\u01B0a gh auth)";
1308
+ const action = await select4({
1309
+ message: "C\xE1ch x\u1EED l\xFD?",
1310
+ choices: [
1311
+ {
1312
+ name: `\u0110\xE3 \u0111\u01B0\u1EE3c grant access v\u1EDBi GitHub username '${ghUserDisplay}' \u2014 ki\u1EC3m tra l\u1EA1i`,
1313
+ value: "retry-same"
1314
+ },
1315
+ {
1316
+ name: "\u0110\xE3 grant v\u1EDBi GitHub account kh\xE1c \u2014 switch gh (m\u1EDF browser)",
1317
+ value: "switch-account"
1318
+ },
1319
+ {
1320
+ name: "T\u1EA1m ng\u01B0ng \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau",
1321
+ value: "abort"
1322
+ }
1323
+ ]
1324
+ });
1325
+ if (action === "abort") {
1326
+ log.dim("T\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\xE3 accept invite t\u1EEB GitHub.");
1327
+ return false;
1328
+ }
1329
+ if (action === "switch-account") {
1330
+ triggerGhAuthLoginInteractive();
1331
+ }
1332
+ log.info("Ki\u1EC3m tra access...");
1333
+ if (checkRepoAccess(args.repoSlug)) {
1334
+ const finalUser = getCurrentGhUser();
1335
+ log.success(`\u0110\xE3 c\xF3 access v\u1EDBi '${finalUser ?? "(unknown)"}' \u2014 ti\u1EBFp t\u1EE5c.`);
1336
+ return true;
1337
+ }
1338
+ log.warn(
1339
+ `V\u1EABn ch\u01B0a c\xF3 access v\u1EDBi account '${ghUser ?? "(unknown)"}'. \u0110\u1EA3m b\u1EA3o \u0111\xE3 accept email invite ho\u1EB7c switch \u0111\xFAng account.`
1340
+ );
1341
+ }
1342
+ }
1343
+
1344
+ // src/lib/resolve-team-pack-repo-url.ts
1345
+ var ORG_DEFAULT = "https://github.com/nalvn/team-ai-pack.git";
1346
+ function resolveTeamPackRepoUrl() {
1347
+ if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
1348
+ return process.env.AVATAR_TEAM_PACK_REPO_URL;
1349
+ }
1350
+ return ORG_DEFAULT;
1351
+ }
1352
+
1353
+ // src/lib/team-pack-submodule-manager.ts
1354
+ var TEAM_PACK_REPO_URL = resolveTeamPackRepoUrl();
1355
+ var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
1356
+ var TeamPackAccessAbortedError = class extends Error {
1357
+ constructor(message) {
1358
+ super(message);
1359
+ this.name = "TeamPackAccessAbortedError";
1360
+ }
1361
+ };
1362
+ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
1363
+ const url = resolveTeamPackRepoUrl();
1364
+ const repoSlug = parseRepoSlugFromGitUrl(url);
1365
+ if (repoSlug) {
1366
+ const hasAccess = await ensureTeamPackAccessWithRetry({ repoSlug, ssoEmail });
1367
+ if (!hasAccess) {
1368
+ throw new TeamPackAccessAbortedError(
1369
+ "User ch\u1ECDn t\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\u01B0\u1EE3c add v\xE0o org."
1370
+ );
1371
+ }
1372
+ }
1373
+ try {
1374
+ await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
1375
+ } catch (err) {
1376
+ const msg = err instanceof Error ? err.message : String(err);
1377
+ if (msg.includes("Repository not found") || msg.includes("not found")) {
1378
+ log.error(
1379
+ `Repo team-ai-pack kh\xF4ng t\u1ED3n t\u1EA1i: ${url}
1380
+ C\xE1ch fix:
1381
+ 1. T\u1EA1o repo: gh repo create <owner>/team-ai-pack --private --add-readme
1382
+ 2. Ho\u1EB7c override URL: export AVATAR_TEAM_PACK_REPO_URL=<url-repo-c\u1EE7a-b\u1EA1n>
1383
+ 3. Ho\u1EB7c d\xF9ng flag --skip-team-pack`
1384
+ );
1385
+ }
1386
+ throw err;
1387
+ }
1388
+ let target = tag ?? null;
1389
+ if (!target) {
1390
+ target = await latestTag(join10(projectRoot, TEAM_PACK_RELATIVE_PATH));
1391
+ }
1392
+ if (target) {
1393
+ await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
1394
+ }
1395
+ return { pinnedTag: target };
1396
+ }
1397
+ async function readPinnedPackVersion(projectRoot) {
1398
+ const submoduleRoot = join10(projectRoot, TEAM_PACK_RELATIVE_PATH);
1399
+ const tag = await latestTag(submoduleRoot);
1400
+ if (tag) return tag;
1401
+ const sha = await currentCommitSha(submoduleRoot);
1402
+ return sha.slice(0, 7);
1403
+ }
1404
+
1405
+ // src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
1406
+ async function addTeamPackSubmoduleWithRetryOnNetworkFail(projectRoot, tag, ssoEmail) {
1407
+ while (true) {
1408
+ try {
1409
+ const result = await addTeamPackSubmodule(projectRoot, tag, ssoEmail);
1410
+ return { pinnedTag: result.pinnedTag, skipped: false };
1411
+ } catch (err) {
1412
+ if (err instanceof TeamPackAccessAbortedError) throw err;
1413
+ const action = await promptRetryOrSkip({
1414
+ taskName: "Pull team-ai-pack submodule",
1415
+ reason: err instanceof Error ? err.message : String(err),
1416
+ allowSkip: true,
1417
+ hint: "Network glitch? Retry th\u01B0\u1EDDng work. N\u1EBFu skip, d\xF9ng `avatar sync` sau \u0111\u1EC3 pull pack."
1418
+ });
1419
+ if (action === "abort") {
1420
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc pull team-ai-pack.");
1421
+ }
1422
+ if (action === "skip") {
1423
+ log.warn(
1424
+ "Skip team-ai-pack. Workspace d\xF9ng \u0111\u01B0\u1EE3c nh\u01B0ng kh\xF4ng c\xF3 shared knowledge. Pull sau qua `avatar sync`."
1425
+ );
1426
+ return { pinnedTag: null, skipped: true };
1427
+ }
1428
+ }
1429
+ }
1430
+ }
1199
1431
 
1200
1432
  // src/lib/avatar-ascii-banner.ts
1201
1433
  import chalk2 from "chalk";
@@ -1259,27 +1491,27 @@ ${renderAvatarBanner(opts)}
1259
1491
  }
1260
1492
 
1261
1493
  // src/lib/execute-gh-repo-create.ts
1262
- import { spawnSync as spawnSync6 } from "child_process";
1494
+ import { spawnSync as spawnSync7 } from "child_process";
1263
1495
  var RepoAlreadyExistsError = class extends Error {
1264
1496
  constructor(fullName) {
1265
1497
  super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
1266
1498
  this.name = "RepoAlreadyExistsError";
1267
1499
  }
1268
1500
  };
1269
- function executeGhRepoCreate(input3) {
1270
- const fullName = `${input3.org}/${input3.name}`;
1501
+ function executeGhRepoCreate(input4) {
1502
+ const fullName = `${input4.org}/${input4.name}`;
1271
1503
  const args = [
1272
1504
  "repo",
1273
1505
  "create",
1274
1506
  fullName,
1275
- `--${input3.visibility}`,
1507
+ `--${input4.visibility}`,
1276
1508
  "--source",
1277
- input3.folder,
1509
+ input4.folder,
1278
1510
  "--remote",
1279
1511
  "origin",
1280
1512
  "--push"
1281
1513
  ];
1282
- const r = spawnSync6("gh", args, { stdio: "inherit" });
1514
+ const r = spawnSync7("gh", args, { stdio: "inherit" });
1283
1515
  if (r.status !== 0) {
1284
1516
  if (r.status === 1) {
1285
1517
  throw new RepoAlreadyExistsError(fullName);
@@ -1293,9 +1525,9 @@ function executeGhRepoCreate(input3) {
1293
1525
  }
1294
1526
 
1295
1527
  // src/lib/resolve-github-username-default.ts
1296
- import { spawnSync as spawnSync7 } from "child_process";
1528
+ import { spawnSync as spawnSync8 } from "child_process";
1297
1529
  function resolveGithubUsernameDefault() {
1298
- const r = spawnSync7("gh", ["api", "user", "--jq", ".login"], {
1530
+ const r = spawnSync8("gh", ["api", "user", "--jq", ".login"], {
1299
1531
  encoding: "utf8",
1300
1532
  stdio: ["ignore", "pipe", "pipe"]
1301
1533
  });
@@ -1327,28 +1559,28 @@ function validateRepoVisibility(v) {
1327
1559
  }
1328
1560
 
1329
1561
  // 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})...`);
1562
+ function createGithubRemoteFromFolder(input4) {
1563
+ validateRepoName(input4.name);
1564
+ validateRepoVisibility(input4.visibility);
1565
+ const org = input4.org ?? resolveGithubUsernameDefault();
1566
+ log.info(`T\u1EA1o GitHub repo ${org}/${input4.name} (${input4.visibility})...`);
1335
1567
  const urls = executeGhRepoCreate({
1336
- folder: input3.folder,
1568
+ folder: input4.folder,
1337
1569
  org,
1338
- name: input3.name,
1339
- visibility: input3.visibility
1570
+ name: input4.name,
1571
+ visibility: input4.visibility
1340
1572
  });
1341
1573
  log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
1342
1574
  return urls;
1343
1575
  }
1344
1576
 
1345
1577
  // src/lib/create-workspace-remote-via-gh.ts
1346
- import { spawnSync as spawnSync15 } from "child_process";
1578
+ import { spawnSync as spawnSync16 } from "child_process";
1347
1579
 
1348
1580
  // src/lib/check-gh-cli-auth-status.ts
1349
- import { spawnSync as spawnSync8 } from "child_process";
1581
+ import { spawnSync as spawnSync9 } from "child_process";
1350
1582
  function checkGhCliAuthStatus() {
1351
- const r = spawnSync8("gh", ["auth", "status"], { stdio: "ignore" });
1583
+ const r = spawnSync9("gh", ["auth", "status"], { stdio: "ignore" });
1352
1584
  if (r.error && r.error.code === "ENOENT") {
1353
1585
  return "not-installed";
1354
1586
  }
@@ -1356,12 +1588,12 @@ function checkGhCliAuthStatus() {
1356
1588
  }
1357
1589
 
1358
1590
  // src/lib/detect-package-manager.ts
1359
- import { spawnSync as spawnSync9 } from "child_process";
1591
+ import { spawnSync as spawnSync10 } from "child_process";
1360
1592
  function hasBinary(name) {
1361
1593
  const platform2 = detectHostPlatform();
1362
1594
  const probe = platform2 === "win32" ? "where" : "command";
1363
1595
  const args = platform2 === "win32" ? [name] : ["-v", name];
1364
- const r = spawnSync9(probe, args, {
1596
+ const r = spawnSync10(probe, args, {
1365
1597
  shell: platform2 !== "win32",
1366
1598
  stdio: "ignore"
1367
1599
  });
@@ -1377,11 +1609,11 @@ function detectPackageManager() {
1377
1609
  }
1378
1610
 
1379
1611
  // 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";
1612
+ import { spawnSync as spawnSync12 } from "child_process";
1613
+ import { select as select5 } from "@inquirer/prompts";
1382
1614
 
1383
1615
  // src/lib/verify-git-remote-accessible.ts
1384
- import { spawnSync as spawnSync10 } from "child_process";
1616
+ import { spawnSync as spawnSync11 } from "child_process";
1385
1617
  var TIMEOUT_MS = 5e3;
1386
1618
  function classifyRemoteError(stderr) {
1387
1619
  const text = stderr.toLowerCase();
@@ -1397,7 +1629,7 @@ function classifyRemoteError(stderr) {
1397
1629
  return "unknown";
1398
1630
  }
1399
1631
  function tryVerifyGitRemoteAccessible(url) {
1400
- const r = spawnSync10("git", ["ls-remote", "--exit-code", url, "HEAD"], {
1632
+ const r = spawnSync11("git", ["ls-remote", "--exit-code", url, "HEAD"], {
1401
1633
  encoding: "utf8",
1402
1634
  timeout: TIMEOUT_MS,
1403
1635
  stdio: ["ignore", "pipe", "pipe"]
@@ -1418,17 +1650,17 @@ var RemoteAccessAbortedError = class extends Error {
1418
1650
  this.name = "RemoteAccessAbortedError";
1419
1651
  }
1420
1652
  };
1421
- function getCurrentGhUser() {
1422
- const r = spawnSync11("gh", ["api", "user", "--jq", ".login"], {
1653
+ function getCurrentGhUser2() {
1654
+ const r = spawnSync12("gh", ["api", "user", "--jq", ".login"], {
1423
1655
  encoding: "utf8",
1424
1656
  stdio: ["ignore", "pipe", "pipe"]
1425
1657
  });
1426
1658
  if (r.status !== 0) return null;
1427
1659
  return r.stdout.trim() || null;
1428
1660
  }
1429
- function triggerGhAuthLoginInteractive() {
1661
+ function triggerGhAuthLoginInteractive2() {
1430
1662
  log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
1431
- const r = spawnSync11("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1663
+ const r = spawnSync12("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1432
1664
  if (r.status !== 0) {
1433
1665
  log.warn(`gh auth login exit ${r.status}. B\u1EA1n c\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
1434
1666
  }
@@ -1451,12 +1683,12 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
1451
1683
  let reason = args.initialReason;
1452
1684
  let detail = args.initialDetail;
1453
1685
  while (true) {
1454
- const ghUser = getCurrentGhUser();
1686
+ const ghUser = getCurrentGhUser2();
1455
1687
  log.warn(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c ${args.url}`);
1456
1688
  log.dim(` L\xFD do: ${reason}${detail ? ` \u2014 ${detail.slice(0, 150)}` : ""}`);
1457
1689
  log.info(getReasonHint(reason, args.url, ghUser));
1458
1690
  if (ghUser) log.dim(` gh CLI hi\u1EC7n \u0111ang login: ${ghUser}`);
1459
- const action = await select3({
1691
+ const action = await select5({
1460
1692
  message: "C\xE1ch x\u1EED l\xFD?",
1461
1693
  choices: [
1462
1694
  {
@@ -1479,7 +1711,7 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
1479
1711
  );
1480
1712
  }
1481
1713
  if (action === "switch") {
1482
- triggerGhAuthLoginInteractive();
1714
+ triggerGhAuthLoginInteractive2();
1483
1715
  }
1484
1716
  log.info("Verify remote l\u1EA1i...");
1485
1717
  const result = tryVerifyGitRemoteAccessible(args.url);
@@ -1493,7 +1725,7 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
1493
1725
  }
1494
1726
 
1495
1727
  // src/lib/install-gh-cli-via-package-manager.ts
1496
- import { spawnSync as spawnSync12 } from "child_process";
1728
+ import { spawnSync as spawnSync13 } from "child_process";
1497
1729
  var INSTALL_COMMANDS = {
1498
1730
  brew: { cmd: "brew", args: ["install", "gh"] },
1499
1731
  apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
@@ -1504,7 +1736,7 @@ var INSTALL_COMMANDS = {
1504
1736
  function installGhCliViaPackageManager(pm) {
1505
1737
  const spec = INSTALL_COMMANDS[pm];
1506
1738
  log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
1507
- const r = spawnSync12(spec.cmd, spec.args, { stdio: "inherit" });
1739
+ const r = spawnSync13(spec.cmd, spec.args, { stdio: "inherit" });
1508
1740
  if (r.status !== 0) {
1509
1741
  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
1742
  }
@@ -1512,9 +1744,9 @@ function installGhCliViaPackageManager(pm) {
1512
1744
  }
1513
1745
 
1514
1746
  // src/lib/setup-git-credential-via-gh.ts
1515
- import { spawnSync as spawnSync13 } from "child_process";
1747
+ import { spawnSync as spawnSync14 } from "child_process";
1516
1748
  function setupGitCredentialViaGh() {
1517
- const r = spawnSync13("gh", ["auth", "setup-git"], { stdio: "ignore" });
1749
+ const r = spawnSync14("gh", ["auth", "setup-git"], { stdio: "ignore" });
1518
1750
  if (r.status !== 0) {
1519
1751
  log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
1520
1752
  return;
@@ -1523,10 +1755,10 @@ function setupGitCredentialViaGh() {
1523
1755
  }
1524
1756
 
1525
1757
  // src/lib/trigger-gh-cli-auth-login.ts
1526
- import { spawnSync as spawnSync14 } from "child_process";
1758
+ import { spawnSync as spawnSync15 } from "child_process";
1527
1759
  function triggerGhCliAuthLogin() {
1528
1760
  log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
1529
- const r = spawnSync14(
1761
+ const r = spawnSync15(
1530
1762
  "gh",
1531
1763
  ["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
1532
1764
  { stdio: "inherit" }
@@ -1539,24 +1771,61 @@ function triggerGhCliAuthLogin() {
1539
1771
 
1540
1772
  // src/lib/git-auth-and-install-orchestrator.ts
1541
1773
  async function ensureGitHubReady(remoteUrl) {
1542
- let state = checkGhCliAuthStatus();
1543
- if (state === "not-installed") {
1774
+ while (checkGhCliAuthStatus() === "not-installed") {
1544
1775
  log.warn("gh CLI ch\u01B0a c\xE0i. Avatar s\u1EBD t\u1EF1 c\xE0i.");
1545
1776
  const pm = detectPackageManager();
1546
1777
  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
- );
1778
+ const action = await promptRetryOrSkip({
1779
+ taskName: "Ph\xE1t hi\u1EC7n package manager",
1780
+ reason: "Kh\xF4ng t\xECm th\u1EA5y brew/apt/dnf/pacman/winget tr\xEAn m\xE1y.",
1781
+ allowSkip: false,
1782
+ hint: "C\xE0i gh CLI tay (https://cli.github.com) r\u1ED3i ch\u1ECDn Retry."
1783
+ });
1784
+ if (action === "abort") {
1785
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc c\xE0i gh CLI.");
1786
+ }
1787
+ continue;
1788
+ }
1789
+ try {
1790
+ installGhCliViaPackageManager(pm);
1791
+ } catch (err) {
1792
+ const action = await promptRetryOrSkip({
1793
+ taskName: `C\xE0i gh CLI qua ${pm}`,
1794
+ reason: err.message,
1795
+ allowSkip: false,
1796
+ hint: "C\xE0i tay (https://cli.github.com) r\u1ED3i ch\u1ECDn Retry, ho\u1EB7c Abort."
1797
+ });
1798
+ if (action === "abort") {
1799
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc c\xE0i gh CLI.");
1800
+ }
1550
1801
  }
1551
- installGhCliViaPackageManager(pm);
1552
- state = checkGhCliAuthStatus();
1553
1802
  }
1554
- if (state === "not-authenticated") {
1803
+ while (checkGhCliAuthStatus() === "not-authenticated") {
1555
1804
  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.");
1805
+ try {
1806
+ triggerGhCliAuthLogin();
1807
+ } catch (err) {
1808
+ const action = await promptRetryOrSkip({
1809
+ taskName: "\u0110\u0103ng nh\u1EADp GitHub qua gh",
1810
+ reason: err.message,
1811
+ allowSkip: false,
1812
+ hint: "Th\u1EED l\u1EA1i \u2014 ch\u1ECDn c\xE1ch login kh\xE1c (browser vs token) khi gh prompt."
1813
+ });
1814
+ if (action === "abort") {
1815
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc gh auth login.");
1816
+ }
1817
+ continue;
1818
+ }
1819
+ if (checkGhCliAuthStatus() !== "authenticated") {
1820
+ const action = await promptRetryOrSkip({
1821
+ taskName: "Verify gh auth",
1822
+ reason: "Sau gh auth login v\u1EABn b\xE1o not-authenticated.",
1823
+ allowSkip: false,
1824
+ hint: "C\xF3 th\u1EC3 user cancel browser. Th\u1EED l\u1EA1i ho\u1EB7c abort."
1825
+ });
1826
+ if (action === "abort") {
1827
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc verify gh auth.");
1828
+ }
1560
1829
  }
1561
1830
  }
1562
1831
  log.success("gh CLI s\u1EB5n s\xE0ng");
@@ -1578,13 +1847,13 @@ async function ensureGitHubReady(remoteUrl) {
1578
1847
  // src/lib/create-workspace-remote-via-gh.ts
1579
1848
  function canCreateInNamespace(org, ghUser) {
1580
1849
  if (org.toLowerCase() === ghUser.toLowerCase()) return { ok: true };
1581
- const r = spawnSync15("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
1850
+ const r = spawnSync16("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
1582
1851
  stdio: "ignore"
1583
1852
  });
1584
1853
  if (r.status === 0) return { ok: true };
1585
- const orgCheck = spawnSync15("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
1854
+ const orgCheck = spawnSync16("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
1586
1855
  if (orgCheck.status !== 0) {
1587
- const userCheck = spawnSync15("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
1856
+ const userCheck = spawnSync16("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
1588
1857
  if (userCheck.status === 0) {
1589
1858
  return {
1590
1859
  ok: false,
@@ -1601,27 +1870,27 @@ function canCreateInNamespace(org, ghUser) {
1601
1870
  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
1871
  };
1603
1872
  }
1604
- async function createWorkspaceRemoteViaGh(input3) {
1605
- validateRepoName(input3.workspaceName);
1606
- validateRepoVisibility(input3.visibility);
1873
+ async function createWorkspaceRemoteViaGh(input4) {
1874
+ validateRepoName(input4.workspaceName);
1875
+ validateRepoVisibility(input4.visibility);
1607
1876
  await ensureGitHubReady();
1608
1877
  const ghUser = resolveGithubUsernameDefault();
1609
- const org = input3.org ?? ghUser;
1878
+ const org = input4.org ?? ghUser;
1610
1879
  const namespaceCheck = canCreateInNamespace(org, ghUser);
1611
1880
  if (!namespaceCheck.ok) {
1612
1881
  throw new Error(`Kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi '${org}/': ${namespaceCheck.reason}`);
1613
1882
  }
1614
- const fullName = `${org}/${input3.workspaceName}`;
1615
- log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input3.visibility})...`);
1616
- const r = spawnSync15(
1883
+ const fullName = `${org}/${input4.workspaceName}`;
1884
+ log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input4.visibility})...`);
1885
+ const r = spawnSync16(
1617
1886
  "gh",
1618
1887
  [
1619
1888
  "repo",
1620
1889
  "create",
1621
1890
  fullName,
1622
- `--${input3.visibility}`,
1891
+ `--${input4.visibility}`,
1623
1892
  "--source",
1624
- input3.workspacePath,
1893
+ input4.workspacePath,
1625
1894
  "--remote",
1626
1895
  "origin",
1627
1896
  "--push"
@@ -1633,7 +1902,7 @@ async function createWorkspaceRemoteViaGh(input3) {
1633
1902
  if (stderr) process.stderr.write(`${stderr}
1634
1903
  `);
1635
1904
  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`
1905
+ `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
1906
  );
1638
1907
  }
1639
1908
  const sshUrl = `git@github.com:${fullName}.git`;
@@ -1644,14 +1913,14 @@ async function createWorkspaceRemoteViaGh(input3) {
1644
1913
 
1645
1914
  // src/lib/safe-bootstrap-for-dirty-folder.ts
1646
1915
  import { readdirSync } from "fs";
1647
- import { select as select4 } from "@inquirer/prompts";
1916
+ import { select as select6 } from "@inquirer/prompts";
1648
1917
  import { simpleGit as simpleGit3 } from "simple-git";
1649
1918
 
1650
1919
  // src/lib/check-folder-has-git.ts
1651
1920
  import { existsSync as existsSync2, statSync } from "fs";
1652
- import { join as join10 } from "path";
1921
+ import { join as join11 } from "path";
1653
1922
  function checkFolderHasGit(folderPath) {
1654
- const gitPath = join10(folderPath, ".git");
1923
+ const gitPath = join11(folderPath, ".git");
1655
1924
  if (!existsSync2(gitPath)) return false;
1656
1925
  const stat = statSync(gitPath);
1657
1926
  return stat.isDirectory() || stat.isFile();
@@ -1683,7 +1952,7 @@ async function createInitialGitCommit(folderPath) {
1683
1952
 
1684
1953
  // src/lib/detect-folder-tech-stack.ts
1685
1954
  import { existsSync as existsSync3 } from "fs";
1686
- import { join as join11 } from "path";
1955
+ import { join as join12 } from "path";
1687
1956
  var SIGNATURES = {
1688
1957
  node: ["package.json"],
1689
1958
  python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
@@ -1695,7 +1964,7 @@ var SIGNATURES = {
1695
1964
  function detectFolderTechStack(folderPath) {
1696
1965
  const matched = [];
1697
1966
  for (const [stack, files] of Object.entries(SIGNATURES)) {
1698
- if (files.some((f) => existsSync3(join11(folderPath, f)))) {
1967
+ if (files.some((f) => existsSync3(join12(folderPath, f)))) {
1699
1968
  matched.push(stack);
1700
1969
  }
1701
1970
  }
@@ -1704,19 +1973,19 @@ function detectFolderTechStack(folderPath) {
1704
1973
 
1705
1974
  // src/lib/gitignore-template-loader.ts
1706
1975
  import { readFileSync as readFileSync2 } from "fs";
1707
- import { dirname as dirname3, join as join12 } from "path";
1976
+ import { dirname as dirname3, join as join13 } from "path";
1708
1977
  import { fileURLToPath as fileURLToPath2 } from "url";
1709
1978
  var __dirname = dirname3(fileURLToPath2(import.meta.url));
1710
1979
  var CANDIDATE_DIRS = [
1711
- join12(__dirname, "..", "templates", "gitignore"),
1712
- join12(__dirname, "..", "..", "src", "templates", "gitignore")
1980
+ join13(__dirname, "..", "templates", "gitignore"),
1981
+ join13(__dirname, "..", "..", "src", "templates", "gitignore")
1713
1982
  ];
1714
1983
  var AVATAR_MARKER_START = "# === avatar ===";
1715
1984
  var AVATAR_MARKER_END = "# === /avatar ===";
1716
1985
  function readTemplate(stack) {
1717
1986
  for (const dir of CANDIDATE_DIRS) {
1718
1987
  try {
1719
- return readFileSync2(join12(dir, `${stack}.txt`), "utf8");
1988
+ return readFileSync2(join13(dir, `${stack}.txt`), "utf8");
1720
1989
  } catch {
1721
1990
  }
1722
1991
  }
@@ -1731,9 +2000,9 @@ ${readTemplate(s).trim()}`);
1731
2000
 
1732
2001
  // src/lib/write-or-merge-gitignore.ts
1733
2002
  import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
1734
- import { join as join13 } from "path";
2003
+ import { join as join14 } from "path";
1735
2004
  function writeOrMergeGitignore(folderPath, avatarBlock) {
1736
- const path = join13(folderPath, ".gitignore");
2005
+ const path = join14(folderPath, ".gitignore");
1737
2006
  if (!existsSync4(path)) {
1738
2007
  writeFileSync(path, avatarBlock, "utf8");
1739
2008
  return;
@@ -1775,7 +2044,7 @@ async function promptBootstrapStrategy(state, opts) {
1775
2044
  if (opts.presetStrategy) return opts.presetStrategy;
1776
2045
  if (opts.autoYes) return "stash";
1777
2046
  if (state === "empty" || state === "clean") return "commit-all";
1778
- return await select4({
2047
+ return await select6({
1779
2048
  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
2049
  choices: [
1781
2050
  {
@@ -1908,145 +2177,6 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
1908
2177
  await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
1909
2178
  }
1910
2179
 
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
2180
  // src/commands/init-conflict-detection-helpers.ts
2051
2181
  import { readdir } from "fs/promises";
2052
2182
  import { join as join15 } from "path";
@@ -2088,7 +2218,7 @@ function buildScaffoldVariables(args) {
2088
2218
  }
2089
2219
 
2090
2220
  // src/commands/login.ts
2091
- import boxen2 from "boxen";
2221
+ import boxen3 from "boxen";
2092
2222
  import open from "open";
2093
2223
 
2094
2224
  // src/lib/google-oauth-device-flow.ts
@@ -2235,7 +2365,7 @@ async function runLogin(opts) {
2235
2365
  "",
2236
2366
  `Ho\u1EB7c Avatar t\u1EF1 m\u1EDF browser, click ${chalk.green("Allow")}...`
2237
2367
  ].join("\n");
2238
- process.stdout.write(`${boxen2(instructions, { padding: 1, borderStyle: "round" })}
2368
+ process.stdout.write(`${boxen3(instructions, { padding: 1, borderStyle: "round" })}
2239
2369
  `);
2240
2370
  void open(verificationUrl).catch(() => {
2241
2371
  log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
@@ -2309,6 +2439,10 @@ function registerInitCommand(program2) {
2309
2439
  log.dim(err.message);
2310
2440
  process.exit(0);
2311
2441
  }
2442
+ if (err instanceof UserAbortedRecoveryError) {
2443
+ log.dim(err.message);
2444
+ process.exit(0);
2445
+ }
2312
2446
  log.error(err instanceof Error ? err.message : String(err));
2313
2447
  process.exit(1);
2314
2448
  }
@@ -2320,13 +2454,26 @@ async function runInit(opts) {
2320
2454
  log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
2321
2455
  }
2322
2456
  let userConfig = await readUserConfig();
2323
- if (!userConfig || isTokenExpired(userConfig)) {
2457
+ while (!userConfig || isTokenExpired(userConfig)) {
2324
2458
  log.info("Ch\u01B0a \u0111\u0103ng nh\u1EADp \u2014 ch\u1EA1y login flow tr\u01B0\u1EDBc khi init...");
2325
- await runLogin({});
2459
+ try {
2460
+ await runLogin({});
2461
+ } catch (err) {
2462
+ log.warn(`Login fail: ${err.message}`);
2463
+ }
2326
2464
  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);
2465
+ if (userConfig && !isTokenExpired(userConfig)) break;
2466
+ const action = await promptRetryOrSkip({
2467
+ taskName: "\u0110\u0103ng nh\u1EADp SSO Google",
2468
+ reason: "Token ch\u01B0a \u0111\u01B0\u1EE3c t\u1EA1o ho\u1EB7c \u0111\xE3 h\u1EBFt h\u1EA1n.",
2469
+ allowSkip: false,
2470
+ // Login bắt buộc, không skip được.
2471
+ hint: "\u0110\u1EA3m b\u1EA3o b\u1EA1n ch\u1ECDn 'Allow' tr\xEAn browser v\xE0 d\xF9ng email @nal.vn."
2472
+ });
2473
+ if (action === "abort") {
2474
+ throw new UserAbortedRecoveryError(
2475
+ "User abort t\u1EA1i b\u01B0\u1EDBc login. Ch\u1EA1y 'avatar login' tay r\u1ED3i 'avatar init' l\u1EA1i."
2476
+ );
2330
2477
  }
2331
2478
  }
2332
2479
  const status = opts.projectStatus ?? await promptProjectStatus();
@@ -2343,7 +2490,7 @@ async function runInit(opts) {
2343
2490
  }
2344
2491
  }
2345
2492
  async function promptProjectStatus() {
2346
- return await select6({
2493
+ return await select7({
2347
2494
  message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
2348
2495
  choices: [
2349
2496
  { name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
@@ -2353,14 +2500,14 @@ async function promptProjectStatus() {
2353
2500
  });
2354
2501
  }
2355
2502
  async function runInitFromExistingRemote(opts, ownerEmail) {
2356
- const remoteUrl = opts.clientRepo ?? await input2({
2503
+ const remoteUrl = opts.clientRepo ?? await input3({
2357
2504
  message: "URL git c\u1EE7a repo:",
2358
2505
  validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
2359
2506
  });
2360
2507
  await ensureGitHubReady(remoteUrl);
2361
2508
  const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
2362
2509
  const inferredName = inferWorkspaceName(remoteUrl);
2363
- const workspaceName = opts.workspaceName ?? await input2({ message: "T\xEAn workspace:", default: inferredName });
2510
+ const workspaceName = opts.workspaceName ?? await input3({ message: "T\xEAn workspace:", default: inferredName });
2364
2511
  const workspaceParent = resolve(opts.workspaceParent ?? ".");
2365
2512
  const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
2366
2513
  await scaffoldWorkspaceWithSrcSubmodule({
@@ -2377,12 +2524,13 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
2377
2524
  repoVisibility: opts.repoVisibility,
2378
2525
  repoOrg: opts.repoOrg,
2379
2526
  flow: "existing-remote",
2380
- aiSkip: opts.aiSkip
2527
+ aiSkip: opts.aiSkip,
2528
+ ssoEmail: ownerEmail
2381
2529
  });
2382
2530
  }
2383
2531
  async function runInitFromExistingFolder(opts, ownerEmail) {
2384
2532
  const folderPath = resolve(
2385
- opts.folderPath ?? await input2({
2533
+ opts.folderPath ?? await input3({
2386
2534
  message: "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3:",
2387
2535
  validate: (v) => v.length > 0 ? true : "Path b\u1EAFt bu\u1ED9c"
2388
2536
  })
@@ -2394,7 +2542,7 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
2394
2542
  const remoteUrl = await getOrCreateOriginRemote(folderPath, opts);
2395
2543
  const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
2396
2544
  const inferredName = opts.workspaceName ?? `${basename(folderPath)}-avatar-workspace`;
2397
- const workspaceName = opts.workspaceName ?? await input2({ message: "T\xEAn workspace:", default: inferredName });
2545
+ const workspaceName = opts.workspaceName ?? await input3({ message: "T\xEAn workspace:", default: inferredName });
2398
2546
  const workspaceParent = resolve(opts.workspaceParent ?? ".");
2399
2547
  const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
2400
2548
  await scaffoldWorkspaceWithSrcSubmodule({
@@ -2412,16 +2560,17 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
2412
2560
  repoVisibility: opts.repoVisibility,
2413
2561
  repoOrg: opts.repoOrg,
2414
2562
  flow: "existing-folder",
2415
- aiSkip: opts.aiSkip
2563
+ aiSkip: opts.aiSkip,
2564
+ ssoEmail: ownerEmail
2416
2565
  });
2417
2566
  }
2418
2567
  async function runInitFromScratch(opts, ownerEmail) {
2419
2568
  await ensureGitHubReady();
2420
- const projectName = opts.workspaceName ?? await input2({
2569
+ const projectName = opts.workspaceName ?? await input3({
2421
2570
  message: "T\xEAn d\u1EF1 \xE1n:",
2422
2571
  validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
2423
2572
  });
2424
- const visibility = opts.repoVisibility ?? await select6({
2573
+ const visibility = opts.repoVisibility ?? await select7({
2425
2574
  message: "Visibility?",
2426
2575
  choices: [
2427
2576
  { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
@@ -2449,7 +2598,12 @@ async function runInitFromScratch(opts, ownerEmail) {
2449
2598
  await git(workspacePath).subModule(["add", urls.sshUrl, "src"]);
2450
2599
  let pinnedTag = "HEAD";
2451
2600
  if (!opts.skipTeamPack) {
2452
- const result = await addTeamPackSubmodule(workspacePath, opts.packVersion);
2601
+ sp.stop();
2602
+ const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
2603
+ workspacePath,
2604
+ opts.packVersion,
2605
+ ownerEmail
2606
+ );
2453
2607
  pinnedTag = result.pinnedTag ?? "HEAD";
2454
2608
  sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
2455
2609
  } else {
@@ -2490,14 +2644,14 @@ async function getOrCreateOriginRemote(folderPath, opts) {
2490
2644
  return void 0;
2491
2645
  }
2492
2646
  await ensureGitHubReady();
2493
- const visibility = opts.repoVisibility ?? await select6({
2647
+ const visibility = opts.repoVisibility ?? await select7({
2494
2648
  message: "Visibility?",
2495
2649
  choices: [
2496
2650
  { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
2497
2651
  { name: "public", value: "public" }
2498
2652
  ]
2499
2653
  });
2500
- const repoName = await input2({
2654
+ const repoName = await input3({
2501
2655
  message: "T\xEAn repo:",
2502
2656
  default: basename(folderPath)
2503
2657
  });
@@ -2519,7 +2673,12 @@ async function scaffoldWorkspaceWithSrcSubmodule(args) {
2519
2673
  await git(args.workspacePath).subModule(["add", args.srcRemoteUrl, "src"]);
2520
2674
  let pinnedTag = "HEAD";
2521
2675
  if (!args.skipTeamPack) {
2522
- const result = await addTeamPackSubmodule(args.workspacePath, args.packVersion);
2676
+ sp.stop();
2677
+ const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
2678
+ args.workspacePath,
2679
+ args.packVersion,
2680
+ args.ssoEmail
2681
+ );
2523
2682
  pinnedTag = result.pinnedTag ?? "HEAD";
2524
2683
  sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
2525
2684
  } else {
@@ -2587,43 +2746,79 @@ async function maybeCreateWorkspaceRemote(args) {
2587
2746
  });
2588
2747
  }
2589
2748
  if (!shouldCreate) return;
2590
- const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select6({
2749
+ const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select7({
2591
2750
  message: "Workspace visibility?",
2592
2751
  choices: [
2593
2752
  { name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
2594
2753
  { name: "public", value: "public" }
2595
2754
  ]
2596
2755
  }));
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.");
2756
+ while (true) {
2757
+ try {
2758
+ await createWorkspaceRemoteViaGh({
2759
+ workspacePath: args.workspacePath,
2760
+ workspaceName: args.workspaceName,
2761
+ visibility,
2762
+ org: args.repoOrg
2763
+ });
2764
+ return;
2765
+ } catch (err) {
2766
+ const action = await promptRetryOrSkip({
2767
+ taskName: "T\u1EA1o workspace remote tr\xEAn GitHub",
2768
+ reason: err instanceof Error ? err.message : String(err),
2769
+ allowSkip: true,
2770
+ // Workspace remote OPTIONAL — skip OK, workspace local vẫn dùng được.
2771
+ hint: "Tip: sai org? Pass --repo-org=<your-gh-user>. Ho\u1EB7c switch gh: gh auth login."
2772
+ });
2773
+ if (action === "abort") {
2774
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc t\u1EA1o workspace remote.");
2775
+ }
2776
+ if (action === "skip") {
2777
+ log.warn("Workspace v\u1EABn s\u1EB5n s\xE0ng local-only. Setup remote sau khi c\u1EA7n.");
2778
+ return;
2779
+ }
2780
+ }
2607
2781
  }
2608
2782
  }
2609
2783
  async function resolveWorkspacePath(parent, desiredName, force) {
2610
2784
  const desired = join16(parent, desiredName);
2611
2785
  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
2786
  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;
2787
+ while (true) {
2788
+ const alternative = await findAlternativeWorkspaceName(parent, desiredName);
2789
+ if (force && alternative) {
2790
+ log.info(`--force: d\xF9ng ${alternative}`);
2791
+ return alternative;
2792
+ }
2793
+ const choices = [];
2794
+ if (alternative) {
2795
+ choices.push({ name: `D\xF9ng "${alternative}" (suggest)`, value: "use-alt" });
2796
+ }
2797
+ choices.push({ name: "Nh\u1EADp t\xEAn workspace kh\xE1c (manual)", value: "manual" });
2798
+ choices.push({ name: "T\u1EA1m ng\u01B0ng init", value: "abort" });
2799
+ const action = await select7({
2800
+ message: "C\xE1ch x\u1EED l\xFD workspace path conflict?",
2801
+ choices
2802
+ });
2803
+ if (action === "abort") {
2804
+ throw new UserAbortedRecoveryError(
2805
+ "User abort t\u1EA1i b\u01B0\u1EDBc resolve workspace path. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c."
2806
+ );
2807
+ }
2808
+ if (action === "use-alt" && alternative) {
2809
+ return alternative;
2810
+ }
2811
+ const newName = await input3({
2812
+ message: "T\xEAn workspace m\u1EDBi:",
2813
+ validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
2814
+ });
2815
+ const newPath = join16(parent, newName.trim());
2816
+ if (await isEmptyOrMissing(newPath)) return newPath;
2817
+ log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
2620
2818
  }
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
2819
  }
2625
2820
  async function promptTeamOwner(currentUserEmail) {
2626
- return await input2({ message: "Team owner email:", default: currentUserEmail });
2821
+ return await input3({ message: "Team owner email:", default: currentUserEmail });
2627
2822
  }
2628
2823
  async function maybeCommitWorkspace(workspacePath, skipCommit) {
2629
2824
  if (skipCommit) {
@@ -2659,7 +2854,7 @@ function printInitSuccessBox(rootPath, flow, aiResult = null) {
2659
2854
  ` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
2660
2855
  ` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
2661
2856
  ];
2662
- process.stdout.write(`${boxen3(lines.join("\n"), { padding: 1, borderStyle: "round" })}
2857
+ process.stdout.write(`${boxen4(lines.join("\n"), { padding: 1, borderStyle: "round" })}
2663
2858
  `);
2664
2859
  }
2665
2860
 
@@ -2696,7 +2891,7 @@ function registerSecretsCommand(program2) {
2696
2891
  // src/commands/status.ts
2697
2892
  import { promises as fs8 } from "fs";
2698
2893
  import { join as join18 } from "path";
2699
- import boxen4 from "boxen";
2894
+ import boxen5 from "boxen";
2700
2895
 
2701
2896
  // src/lib/pack-backup-manager.ts
2702
2897
  import { promises as fs7 } from "fs";
@@ -2774,7 +2969,7 @@ function renderStatusBox(s) {
2774
2969
  `${chalk.dim("Backups:")} ${s.backupCount}`,
2775
2970
  `${chalk.dim("Tech stack:")} ${s.techStackSummary}`
2776
2971
  ];
2777
- process.stdout.write(`${boxen4(lines.join("\n"), { padding: 1, borderStyle: "round" })}
2972
+ process.stdout.write(`${boxen5(lines.join("\n"), { padding: 1, borderStyle: "round" })}
2778
2973
  `);
2779
2974
  }
2780
2975
 
@@ -2794,7 +2989,7 @@ function registerToolsCommand(program2) {
2794
2989
  // src/commands/uninstall.ts
2795
2990
  import { relative as relative3 } from "path";
2796
2991
  import { confirm as confirm4 } from "@inquirer/prompts";
2797
- import boxen5 from "boxen";
2992
+ import boxen6 from "boxen";
2798
2993
 
2799
2994
  // src/lib/create-uninstall-backup-snapshot.ts
2800
2995
  import { cp, mkdir, writeFile } from "fs/promises";
@@ -2948,7 +3143,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
2948
3143
  }
2949
3144
 
2950
3145
  // src/commands/uninstall.ts
2951
- var CLI_VERSION = "1.2.4";
3146
+ var CLI_VERSION = "1.2.6";
2952
3147
  function registerUninstallCommand(program2) {
2953
3148
  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
3149
  try {
@@ -3025,12 +3220,12 @@ function printUninstallSuccessBox(backupPath) {
3025
3220
  lines.push(` ${chalk.dim("Backup:")} ${backupPath}`);
3026
3221
  lines.push(` ${chalk.dim("Restore:")} ${chalk.cyan(`cp -r "${backupPath}"/* .`)}`);
3027
3222
  }
3028
- process.stdout.write(`${boxen5(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3223
+ process.stdout.write(`${boxen6(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3029
3224
  `);
3030
3225
  }
3031
3226
 
3032
3227
  // src/index.ts
3033
- var CLI_VERSION2 = "1.2.4";
3228
+ var CLI_VERSION2 = "1.2.6";
3034
3229
  var program = new Command();
3035
3230
  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
3231
  "beforeAll",