@nalvietnam/avatar-cli 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,7 +4,6 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/ai.ts
7
- import { spawnSync as spawnSync4 } from "child_process";
8
7
  import { promises as fs4 } from "fs";
9
8
  import { join as join5 } from "path";
10
9
  import { confirm } from "@inquirer/prompts";
@@ -654,6 +653,102 @@ async function runAiSetupPhase(args) {
654
653
  }
655
654
  }
656
655
 
656
+ // src/lib/test-ai-provider-by-detected-mode.ts
657
+ import { spawnSync as spawnSync4 } from "child_process";
658
+ var FETCH_TIMEOUT_MS2 = 1e4;
659
+ var CLAUDE_PRINT_TIMEOUT_MS = 3e4;
660
+ var TEST_CHAT_MAX_TOKENS = 5;
661
+ var TEST_CHAT_PROMPT = "say ok";
662
+ async function testLLMLiteProvider(baseUrl, token, model) {
663
+ log.info(`Testing LLMLite provider: ${baseUrl} (key: ${maskApiKey(token)})`);
664
+ const controller = new AbortController();
665
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
666
+ try {
667
+ const modelsRes = await fetch(`${baseUrl}/v1/models`, {
668
+ headers: { Authorization: `Bearer ${token}` },
669
+ signal: controller.signal
670
+ });
671
+ if (modelsRes.status === 401 || modelsRes.status === 403) {
672
+ throw new Error(`API key invalid (HTTP ${modelsRes.status}). Re-run: avatar ai setup`);
673
+ }
674
+ if (!modelsRes.ok) {
675
+ throw new Error(`Endpoint /v1/models l\u1ED7i (HTTP ${modelsRes.status}).`);
676
+ }
677
+ const modelsJson = await modelsRes.json();
678
+ const models = (modelsJson.data || []).map((m) => typeof m.id === "string" ? m.id : null).filter((id) => id !== null);
679
+ log.success(`Connectivity OK \xB7 ${models.length} models available`);
680
+ if (models.length > 0) {
681
+ const preview = models.slice(0, 5).join(", ");
682
+ const more = models.length > 5 ? ` ...+${models.length - 5} more` : "";
683
+ log.dim(` Models: ${preview}${more}`);
684
+ }
685
+ log.info(`Testing chat completion v\u1EDBi model "${model}"...`);
686
+ const chatRes = await fetch(`${baseUrl}/v1/chat/completions`, {
687
+ method: "POST",
688
+ headers: {
689
+ Authorization: `Bearer ${token}`,
690
+ "Content-Type": "application/json"
691
+ },
692
+ body: JSON.stringify({
693
+ model,
694
+ messages: [{ role: "user", content: TEST_CHAT_PROMPT }],
695
+ max_tokens: TEST_CHAT_MAX_TOKENS
696
+ }),
697
+ signal: controller.signal
698
+ });
699
+ if (!chatRes.ok) {
700
+ const errBody = (await chatRes.text()).slice(0, 200);
701
+ throw new Error(`Chat completion fail (HTTP ${chatRes.status}). ${errBody}`);
702
+ }
703
+ const chatJson = await chatRes.json();
704
+ const reply = typeof chatJson.choices?.[0]?.message?.content === "string" ? chatJson.choices[0].message.content : "(empty response)";
705
+ const tokens = chatJson.usage?.total_tokens ?? "?";
706
+ log.success(`Response: "${String(reply).trim().slice(0, 100)}"`);
707
+ log.dim(` Tokens used: ${tokens}`);
708
+ } catch (err) {
709
+ if (err.name === "AbortError") {
710
+ throw new Error(`Timeout ${FETCH_TIMEOUT_MS2 / 1e3}s. Check m\u1EA1ng / endpoint ${baseUrl}.`);
711
+ }
712
+ throw err;
713
+ } finally {
714
+ clearTimeout(timer);
715
+ }
716
+ }
717
+ function testSubscriptionProvider() {
718
+ log.info("Testing Subscription provider qua `claude --print`...");
719
+ const result = spawnSync4("claude", ["--print", TEST_CHAT_PROMPT], {
720
+ encoding: "utf8",
721
+ timeout: CLAUDE_PRINT_TIMEOUT_MS
722
+ });
723
+ if (result.signal === "SIGTERM") {
724
+ throw new Error(`Timeout ${CLAUDE_PRINT_TIMEOUT_MS / 1e3}s. Check m\u1EA1ng / endpoint.`);
725
+ }
726
+ if (result.status !== 0) {
727
+ const stderr = (result.stderr || "").toLowerCase();
728
+ if (stderr.includes("401") || stderr.includes("invalid authentication") || stderr.includes("unauthorized")) {
729
+ throw new Error(
730
+ "Token Claude Code stale (401). Fix: `claude auth logout && claude auth login`."
731
+ );
732
+ }
733
+ throw new Error(
734
+ `Test fail (exit ${result.status}). Stderr: ${(result.stderr || "").slice(0, 200)}`
735
+ );
736
+ }
737
+ log.success(`Response: "${(result.stdout || "").trim().slice(0, 100)}"`);
738
+ }
739
+ async function testAiProviderByDetectedMode(settings) {
740
+ const env = settings.env || {};
741
+ const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
742
+ const token = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : void 0;
743
+ const model = typeof settings.model === "string" ? settings.model : "default";
744
+ if (baseUrl && token) {
745
+ await testLLMLiteProvider(baseUrl, token, model);
746
+ return { ok: true, provider: "llmlite", message: "LLMLite provider working" };
747
+ }
748
+ testSubscriptionProvider();
749
+ return { ok: true, provider: "subscription", message: "Subscription provider working" };
750
+ }
751
+
657
752
  // src/commands/ai.ts
658
753
  async function ensureWorkspaceCwd() {
659
754
  const cwd = process.cwd();
@@ -691,24 +786,15 @@ async function runAiStatus() {
691
786
  log.info(`Token: ${token ? maskApiKey(token) : "(kh\xF4ng set \u2014 d\xF9ng subscription auth)"}`);
692
787
  }
693
788
  async function runAiTest() {
694
- await ensureWorkspaceCwd();
695
- log.info("Calling provider v\u1EDBi prompt 'say ok'...");
696
- const result = spawnSync4("claude", ["--print", "say ok"], {
697
- encoding: "utf8",
698
- timeout: 3e4
699
- });
700
- if (result.signal === "SIGTERM") {
701
- log.error("Timeout sau 30s. Check m\u1EA1ng / endpoint.");
702
- process.exit(1);
703
- }
704
- if (result.status !== 0) {
705
- log.error(`Test th\u1EA5t b\u1EA1i (exit ${result.status}).`);
706
- if (result.stderr) process.stderr.write(`${result.stderr}
707
- `);
789
+ const workspacePath = await ensureWorkspaceCwd();
790
+ const settings = await readWorkspaceSettings(workspacePath);
791
+ try {
792
+ const result = await testAiProviderByDetectedMode(settings);
793
+ log.success(`\u2713 ${result.message}`);
794
+ } catch (err) {
795
+ log.error(`Test fail: ${err.message}`);
708
796
  process.exit(1);
709
797
  }
710
- log.success(`Response: ${(result.stdout || "").trim().slice(0, 200)}`);
711
- log.success("AI provider working");
712
798
  }
713
799
  async function runAiReset(opts) {
714
800
  const workspacePath = await ensureWorkspaceCwd();
@@ -1108,8 +1194,8 @@ async function applyFixes(checks) {
1108
1194
 
1109
1195
  // src/commands/init.ts
1110
1196
  import { basename, join as join16, relative as relative2, resolve } from "path";
1111
- import { confirm as confirm2, input as input2, select as select4 } from "@inquirer/prompts";
1112
- import boxen2 from "boxen";
1197
+ import { confirm as confirm3, input as input2, select as select5 } from "@inquirer/prompts";
1198
+ import boxen3 from "boxen";
1113
1199
 
1114
1200
  // src/lib/avatar-ascii-banner.ts
1115
1201
  import chalk2 from "chalk";
@@ -1385,11 +1471,41 @@ async function ensureGitHubReady(remoteUrl) {
1385
1471
  }
1386
1472
 
1387
1473
  // src/lib/create-workspace-remote-via-gh.ts
1474
+ function canCreateInNamespace(org, ghUser) {
1475
+ if (org.toLowerCase() === ghUser.toLowerCase()) return { ok: true };
1476
+ const r = spawnSync14("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
1477
+ stdio: "ignore"
1478
+ });
1479
+ if (r.status === 0) return { ok: true };
1480
+ const orgCheck = spawnSync14("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
1481
+ if (orgCheck.status !== 0) {
1482
+ const userCheck = spawnSync14("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
1483
+ if (userCheck.status === 0) {
1484
+ return {
1485
+ ok: false,
1486
+ reason: `'${org}' l\xE0 personal account kh\xE1c (kh\xF4ng ph\u1EA3i org). B\u1EA1n (${ghUser}) kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi account c\u1EE7a user kh\xE1c. Pass --repo-org=${ghUser} ho\u1EB7c switch gh: gh auth login`
1487
+ };
1488
+ }
1489
+ return {
1490
+ ok: false,
1491
+ reason: `'${org}' kh\xF4ng t\u1ED3n t\u1EA1i tr\xEAn GitHub. Check ch\xEDnh t\u1EA3 ho\u1EB7c t\u1EA1o org tr\u01B0\u1EDBc.`
1492
+ };
1493
+ }
1494
+ return {
1495
+ ok: false,
1496
+ reason: `'${ghUser}' kh\xF4ng ph\u1EA3i member c\u1EE7a org '${org}'. Li\xEAn h\u1EC7 admin org \u0111\u1EC3 \u0111\u01B0\u1EE3c invite, ho\u1EB7c pass --repo-org=${ghUser} t\u1EA1o d\u01B0\u1EDBi personal account.`
1497
+ };
1498
+ }
1388
1499
  async function createWorkspaceRemoteViaGh(input3) {
1389
1500
  validateRepoName(input3.workspaceName);
1390
1501
  validateRepoVisibility(input3.visibility);
1391
1502
  await ensureGitHubReady();
1392
- const org = input3.org ?? resolveGithubUsernameDefault();
1503
+ const ghUser = resolveGithubUsernameDefault();
1504
+ const org = input3.org ?? ghUser;
1505
+ const namespaceCheck = canCreateInNamespace(org, ghUser);
1506
+ if (!namespaceCheck.ok) {
1507
+ throw new Error(`Kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi '${org}/': ${namespaceCheck.reason}`);
1508
+ }
1393
1509
  const fullName = `${org}/${input3.workspaceName}`;
1394
1510
  log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input3.visibility})...`);
1395
1511
  const r = spawnSync14(
@@ -1405,9 +1521,12 @@ async function createWorkspaceRemoteViaGh(input3) {
1405
1521
  "origin",
1406
1522
  "--push"
1407
1523
  ],
1408
- { stdio: "inherit" }
1524
+ { stdio: ["inherit", "inherit", "pipe"], encoding: "utf8" }
1409
1525
  );
1410
1526
  if (r.status !== 0) {
1527
+ const stderr = (r.stderr || "").trim();
1528
+ if (stderr) process.stderr.write(`${stderr}
1529
+ `);
1411
1530
  throw new Error(
1412
1531
  `T\u1EA1o workspace remote th\u1EA5t b\u1EA1i (exit ${r.status}). Workspace v\u1EABn d\xF9ng \u0111\u01B0\u1EE3c local. Setup remote sau qua: gh repo create ${fullName} --${input3.visibility} --source=. --remote=origin --push`
1413
1532
  );
@@ -1687,25 +1806,110 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
1687
1806
  // src/lib/team-pack-submodule-manager.ts
1688
1807
  import { join as join14 } from "path";
1689
1808
 
1809
+ // src/lib/check-team-pack-access-with-retry-loop.ts
1810
+ import { spawnSync as spawnSync15 } from "child_process";
1811
+ import { confirm as confirm2, select as select4 } from "@inquirer/prompts";
1812
+ function parseRepoSlugFromGitUrl(url) {
1813
+ const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
1814
+ if (httpsMatch) return httpsMatch[1];
1815
+ return null;
1816
+ }
1817
+ function checkRepoAccess(repoSlug) {
1818
+ const r = spawnSync15("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
1819
+ return r.status === 0;
1820
+ }
1821
+ function getCurrentGhUser() {
1822
+ const r = spawnSync15("gh", ["api", "user", "--jq", ".login"], {
1823
+ encoding: "utf8",
1824
+ stdio: ["ignore", "pipe", "pipe"]
1825
+ });
1826
+ if (r.status !== 0) return null;
1827
+ return r.stdout.trim() || null;
1828
+ }
1829
+ async function copyInfoToClipboardWithConsent(info) {
1830
+ const ok = await confirm2({
1831
+ message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
1832
+ default: true
1833
+ });
1834
+ if (!ok) return;
1835
+ try {
1836
+ const { default: clipboardy } = await import("clipboardy");
1837
+ await clipboardy.write(info);
1838
+ log.success("\u0110\xE3 copy v\xE0o clipboard");
1839
+ } catch (err) {
1840
+ log.dim(`Copy clipboard fail: ${err.message}`);
1841
+ }
1842
+ }
1843
+ function buildAccessRequestInfo(ghUser, ssoEmail) {
1844
+ const lines = [
1845
+ "Request access team-ai-pack (NAL)",
1846
+ "",
1847
+ `GitHub username: ${ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)"}`,
1848
+ `NAL email: ${ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)"}`,
1849
+ `Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
1850
+ ];
1851
+ return lines.join("\n");
1852
+ }
1853
+ async function ensureTeamPackAccessWithRetry(args) {
1854
+ if (checkRepoAccess(args.repoSlug)) return true;
1855
+ const ghUser = getCurrentGhUser();
1856
+ const info = buildAccessRequestInfo(ghUser, args.ssoEmail ?? null);
1857
+ log.warn(`B\u1EA1n ch\u01B0a c\xF3 quy\u1EC1n access v\xE0o b\u1ED9 package ${args.repoSlug}.`);
1858
+ log.info("Li\xEAn h\u1EC7 admin (Luke @nal.vn) \u0111\u1EC3 \u0111\u01B0\u1EE3c add v\xE0o org nalvn.");
1859
+ log.plain("");
1860
+ log.plain(info);
1861
+ log.plain("");
1862
+ await copyInfoToClipboardWithConsent(info);
1863
+ while (true) {
1864
+ const action = await select4({
1865
+ message: "Ti\u1EBFp t\u1EE5c?",
1866
+ choices: [
1867
+ { name: "\u0110\xE3 \u0111\u01B0\u1EE3c add \u2014 ki\u1EC3m tra l\u1EA1i v\xE0 ti\u1EBFp t\u1EE5c", value: "retry" },
1868
+ { name: "T\u1EA1m ng\u01B0ng \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau", value: "abort" }
1869
+ ]
1870
+ });
1871
+ if (action === "abort") {
1872
+ log.dim("T\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\xE3 accept invite t\u1EEB GitHub.");
1873
+ return false;
1874
+ }
1875
+ log.info("Ki\u1EC3m tra access...");
1876
+ if (checkRepoAccess(args.repoSlug)) {
1877
+ log.success("\u0110\xE3 c\xF3 access \u2014 ti\u1EBFp t\u1EE5c.");
1878
+ return true;
1879
+ }
1880
+ log.warn("V\u1EABn ch\u01B0a c\xF3 access. \u0110\u1EA3m b\u1EA3o b\u1EA1n \u0111\xE3 accept email invite t\u1EEB GitHub (Inbox + Spam).");
1881
+ }
1882
+ }
1883
+
1690
1884
  // src/lib/resolve-team-pack-repo-url.ts
1691
- var LEGACY_FALLBACK = "https://github.com/LukeNALS/team-ai-pack.git";
1885
+ var ORG_DEFAULT = "https://github.com/nalvn/team-ai-pack.git";
1692
1886
  function resolveTeamPackRepoUrl() {
1693
1887
  if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
1694
1888
  return process.env.AVATAR_TEAM_PACK_REPO_URL;
1695
1889
  }
1696
- try {
1697
- const ghUser = resolveGithubUsernameDefault();
1698
- if (ghUser) return `https://github.com/${ghUser}/team-ai-pack.git`;
1699
- } catch {
1700
- }
1701
- return LEGACY_FALLBACK;
1890
+ return ORG_DEFAULT;
1702
1891
  }
1703
1892
 
1704
1893
  // src/lib/team-pack-submodule-manager.ts
1705
1894
  var TEAM_PACK_REPO_URL = resolveTeamPackRepoUrl();
1706
1895
  var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
1707
- async function addTeamPackSubmodule(projectRoot, tag) {
1896
+ var TeamPackAccessAbortedError = class extends Error {
1897
+ constructor(message) {
1898
+ super(message);
1899
+ this.name = "TeamPackAccessAbortedError";
1900
+ }
1901
+ };
1902
+ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
1708
1903
  const url = resolveTeamPackRepoUrl();
1904
+ const repoSlug = parseRepoSlugFromGitUrl(url);
1905
+ if (repoSlug) {
1906
+ const hasAccess = await ensureTeamPackAccessWithRetry({ repoSlug, ssoEmail });
1907
+ if (!hasAccess) {
1908
+ throw new TeamPackAccessAbortedError(
1909
+ "User ch\u1ECDn t\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\u01B0\u1EE3c add v\xE0o org."
1910
+ );
1911
+ }
1912
+ }
1709
1913
  try {
1710
1914
  await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
1711
1915
  } catch (err) {
@@ -1778,6 +1982,196 @@ function buildScaffoldVariables(args) {
1778
1982
  };
1779
1983
  }
1780
1984
 
1985
+ // src/commands/login.ts
1986
+ import boxen2 from "boxen";
1987
+ import open from "open";
1988
+
1989
+ // src/lib/google-oauth-device-flow.ts
1990
+ var GOOGLE_CLIENT_ID = "1014766441755-i4jimivh5rd7vt8phuhmepmt45sovtph.apps.googleusercontent.com";
1991
+ var GOOGLE_CLIENT_SECRET = "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";
1992
+ var HOSTED_DOMAIN = "nal.vn";
1993
+ var SCOPES = ["openid", "email", "profile"];
1994
+ var DEVICE_CODE_URL = "https://oauth2.googleapis.com/device/code";
1995
+ var TOKEN_URL = "https://oauth2.googleapis.com/token";
1996
+ var REVOKE_URL = "https://oauth2.googleapis.com/revoke";
1997
+ async function requestDeviceCode() {
1998
+ const body = new URLSearchParams({
1999
+ client_id: GOOGLE_CLIENT_ID,
2000
+ scope: SCOPES.join(" ")
2001
+ });
2002
+ const res = await fetch(DEVICE_CODE_URL, {
2003
+ method: "POST",
2004
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2005
+ body
2006
+ });
2007
+ if (!res.ok) {
2008
+ const text = await res.text();
2009
+ throw new Error(`Device code request failed (${res.status}): ${text}`);
2010
+ }
2011
+ return await res.json();
2012
+ }
2013
+ async function pollForToken(deviceCode) {
2014
+ const body = new URLSearchParams({
2015
+ client_id: GOOGLE_CLIENT_ID,
2016
+ client_secret: GOOGLE_CLIENT_SECRET,
2017
+ device_code: deviceCode,
2018
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
2019
+ });
2020
+ const res = await fetch(TOKEN_URL, {
2021
+ method: "POST",
2022
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2023
+ body
2024
+ });
2025
+ if (res.ok) {
2026
+ return await res.json();
2027
+ }
2028
+ let errorCode = "";
2029
+ try {
2030
+ const data = await res.json();
2031
+ errorCode = data.error ?? "";
2032
+ } catch {
2033
+ errorCode = "";
2034
+ }
2035
+ if (errorCode === "authorization_pending" || errorCode === "slow_down") {
2036
+ return null;
2037
+ }
2038
+ if (errorCode === "access_denied") {
2039
+ throw new Error("User t\u1EEB ch\u1ED1i quy\u1EC1n truy c\u1EADp");
2040
+ }
2041
+ if (errorCode === "expired_token") {
2042
+ throw new Error("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
2043
+ }
2044
+ throw new Error(`OAuth token endpoint tr\u1EA3 l\u1ED7i: ${errorCode || res.status}`);
2045
+ }
2046
+ function decodeIdToken(idToken) {
2047
+ const parts = idToken.split(".");
2048
+ if (parts.length !== 3) {
2049
+ throw new Error("id_token format kh\xF4ng h\u1EE3p l\u1EC7");
2050
+ }
2051
+ const payload = parts[1];
2052
+ if (!payload) throw new Error("id_token thi\u1EBFu payload");
2053
+ const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
2054
+ const json = Buffer.from(base64, "base64").toString("utf8");
2055
+ return JSON.parse(json);
2056
+ }
2057
+ function verifyHostedDomain(claims) {
2058
+ if (claims.hd !== HOSTED_DOMAIN) {
2059
+ throw new Error(
2060
+ `Email kh\xF4ng thu\u1ED9c workspace NAL (y\xEAu c\u1EA7u @${HOSTED_DOMAIN}). Nh\u1EADn: ${claims.email}`
2061
+ );
2062
+ }
2063
+ if (!claims.email_verified) {
2064
+ throw new Error("Email ch\u01B0a \u0111\u01B0\u1EE3c Google verify");
2065
+ }
2066
+ }
2067
+ function buildUserConfig(token, claims) {
2068
+ const expiresAt = new Date(Date.now() + token.expires_in * 1e3).toISOString();
2069
+ return {
2070
+ email: claims.email,
2071
+ name: claims.name ?? claims.email,
2072
+ access_token: token.access_token,
2073
+ refresh_token: token.refresh_token,
2074
+ expires_at: expiresAt,
2075
+ id_token: token.id_token
2076
+ };
2077
+ }
2078
+ async function revokeToken(token) {
2079
+ const body = new URLSearchParams({ token });
2080
+ await fetch(REVOKE_URL, {
2081
+ method: "POST",
2082
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2083
+ body
2084
+ }).catch(() => {
2085
+ });
2086
+ }
2087
+ function buildVerificationUrl(response) {
2088
+ const url = new URL(response.verification_url);
2089
+ url.searchParams.set("user_code", response.user_code);
2090
+ url.searchParams.set("hd", HOSTED_DOMAIN);
2091
+ return url.toString();
2092
+ }
2093
+
2094
+ // src/commands/login.ts
2095
+ function registerLoginCommand(program2) {
2096
+ program2.command("login").description("\u0110\u0103ng nh\u1EADp Google SSO (workspace @nal.vn)").option("--reset", "X\xF3a credential c\u0169 v\xE0 \u0111\u0103ng nh\u1EADp l\u1EA1i").action(async (opts) => {
2097
+ try {
2098
+ await runLogin(opts);
2099
+ } catch (err) {
2100
+ log.error(err instanceof Error ? err.message : String(err));
2101
+ process.exit(1);
2102
+ }
2103
+ });
2104
+ }
2105
+ async function runLogin(opts) {
2106
+ printAvatarBanner({ tagline: "\u0110\u0103ng nh\u1EADp Google SSO \xB7 workspace @nal.vn" });
2107
+ if (opts.reset) {
2108
+ await clearUserConfig();
2109
+ await appendAuditEntry("login_reset");
2110
+ } else {
2111
+ const existing = await readUserConfig();
2112
+ if (existing && !isTokenExpired(existing)) {
2113
+ log.success(`\u0110\xE3 \u0111\u0103ng nh\u1EADp: ${existing.email}`);
2114
+ return;
2115
+ }
2116
+ }
2117
+ const deviceSpinner = spinner("\u0110ang y\xEAu c\u1EA7u device code t\u1EEB Google...");
2118
+ let deviceCode;
2119
+ try {
2120
+ deviceCode = await requestDeviceCode();
2121
+ deviceSpinner.succeed("Nh\u1EADn device code");
2122
+ } catch (err) {
2123
+ deviceSpinner.fail("Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c Google");
2124
+ throw err;
2125
+ }
2126
+ const verificationUrl = buildVerificationUrl(deviceCode);
2127
+ const instructions = [
2128
+ `1. Truy c\u1EADp: ${chalk.cyan(deviceCode.verification_url)}`,
2129
+ `2. Nh\u1EADp code: ${chalk.bold.yellow(deviceCode.user_code)}`,
2130
+ "",
2131
+ `Ho\u1EB7c Avatar t\u1EF1 m\u1EDF browser, click ${chalk.green("Allow")}...`
2132
+ ].join("\n");
2133
+ process.stdout.write(`${boxen2(instructions, { padding: 1, borderStyle: "round" })}
2134
+ `);
2135
+ void open(verificationUrl).catch(() => {
2136
+ log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
2137
+ });
2138
+ const waitSpinner = spinner("\u0110ang ch\u1EDD x\xE1c nh\u1EADn trong browser...");
2139
+ const intervalMs = deviceCode.interval * 1e3;
2140
+ const deadline = Date.now() + deviceCode.expires_in * 1e3;
2141
+ let token = null;
2142
+ while (Date.now() < deadline) {
2143
+ await sleep(intervalMs);
2144
+ try {
2145
+ token = await pollForToken(deviceCode.device_code);
2146
+ if (token) break;
2147
+ } catch (err) {
2148
+ waitSpinner.fail("X\xE1c th\u1EF1c th\u1EA5t b\u1EA1i");
2149
+ throw err;
2150
+ }
2151
+ }
2152
+ if (!token) {
2153
+ waitSpinner.fail("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
2154
+ process.exit(1);
2155
+ }
2156
+ waitSpinner.succeed("\u0110\xE3 nh\u1EADn token t\u1EEB Google");
2157
+ const claims = decodeIdToken(token.id_token);
2158
+ try {
2159
+ verifyHostedDomain(claims);
2160
+ } catch (err) {
2161
+ await revokeToken(token.access_token);
2162
+ throw err;
2163
+ }
2164
+ const userConfig = buildUserConfig(token, claims);
2165
+ await writeUserConfig(userConfig);
2166
+ await appendAuditEntry("login", userConfig.email);
2167
+ log.success(`X\xE1c th\u1EF1c th\xE0nh c\xF4ng: ${userConfig.email}`);
2168
+ log.success(`Verify hosted domain: ${claims.hd} \u2713`);
2169
+ log.success(`L\u01B0u credential v\xE0o ${USER_CONFIG_PATH} (chmod 600)`);
2170
+ }
2171
+ function sleep(ms) {
2172
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2173
+ }
2174
+
1781
2175
  // src/commands/init.ts
1782
2176
  function parseBootstrapStrategyOpts(opts) {
1783
2177
  if (opts.preserveUncommitted) return "stash";
@@ -1802,6 +2196,10 @@ function registerInitCommand(program2) {
1802
2196
  log.dim(err.message);
1803
2197
  process.exit(0);
1804
2198
  }
2199
+ if (err instanceof TeamPackAccessAbortedError) {
2200
+ log.dim(err.message);
2201
+ process.exit(0);
2202
+ }
1805
2203
  log.error(err instanceof Error ? err.message : String(err));
1806
2204
  process.exit(1);
1807
2205
  }
@@ -1812,10 +2210,15 @@ async function runInit(opts) {
1812
2210
  if (opts.mode) {
1813
2211
  log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
1814
2212
  }
1815
- const userConfig = await readUserConfig();
2213
+ let userConfig = await readUserConfig();
1816
2214
  if (!userConfig || isTokenExpired(userConfig)) {
1817
- log.error("Ch\u01B0a \u0111\u0103ng nh\u1EADp ho\u1EB7c token h\u1EBFt h\u1EA1n. Ch\u1EA1y 'avatar login' tr\u01B0\u1EDBc.");
1818
- process.exit(1);
2215
+ log.info("Ch\u01B0a \u0111\u0103ng nh\u1EADp \u2014 ch\u1EA1y login flow tr\u01B0\u1EDBc khi init...");
2216
+ await runLogin({});
2217
+ userConfig = await readUserConfig();
2218
+ if (!userConfig || isTokenExpired(userConfig)) {
2219
+ log.error("Login kh\xF4ng ho\xE0n t\u1EA5t. Ch\u1EA1y 'avatar login' tay r\u1ED3i init l\u1EA1i.");
2220
+ process.exit(1);
2221
+ }
1819
2222
  }
1820
2223
  const status = opts.projectStatus ?? await promptProjectStatus();
1821
2224
  switch (status) {
@@ -1831,7 +2234,7 @@ async function runInit(opts) {
1831
2234
  }
1832
2235
  }
1833
2236
  async function promptProjectStatus() {
1834
- return await select4({
2237
+ return await select5({
1835
2238
  message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
1836
2239
  choices: [
1837
2240
  { name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
@@ -1909,7 +2312,7 @@ async function runInitFromScratch(opts, ownerEmail) {
1909
2312
  message: "T\xEAn d\u1EF1 \xE1n:",
1910
2313
  validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
1911
2314
  });
1912
- const visibility = opts.repoVisibility ?? await select4({
2315
+ const visibility = opts.repoVisibility ?? await select5({
1913
2316
  message: "Visibility?",
1914
2317
  choices: [
1915
2318
  { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
@@ -1969,7 +2372,7 @@ async function getOrCreateOriginRemote(folderPath, opts) {
1969
2372
  log.success(`Folder \u0111\xE3 c\xF3 remote origin: ${origin.refs.push}`);
1970
2373
  return origin.refs.push;
1971
2374
  }
1972
- const shouldCreate = opts.createRemote ?? await confirm2({
2375
+ const shouldCreate = opts.createRemote ?? await confirm3({
1973
2376
  message: "Folder ch\u01B0a c\xF3 remote. T\u1EA1o GitHub repo ngay \u0111\u1EC3 share team?",
1974
2377
  default: true
1975
2378
  });
@@ -1978,7 +2381,7 @@ async function getOrCreateOriginRemote(folderPath, opts) {
1978
2381
  return void 0;
1979
2382
  }
1980
2383
  await ensureGitHubReady();
1981
- const visibility = opts.repoVisibility ?? await select4({
2384
+ const visibility = opts.repoVisibility ?? await select5({
1982
2385
  message: "Visibility?",
1983
2386
  choices: [
1984
2387
  { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
@@ -2069,13 +2472,13 @@ async function maybeCreateWorkspaceRemote(args) {
2069
2472
  let shouldCreate = args.createWorkspaceRemote;
2070
2473
  if (shouldCreate === void 0) {
2071
2474
  if (args.autoYes) return;
2072
- shouldCreate = await confirm2({
2475
+ shouldCreate = await confirm3({
2073
2476
  message: "T\u1EA1o remote GitHub cho workspace \u0111\u1EC3 share team? (Avatar state)",
2074
2477
  default: false
2075
2478
  });
2076
2479
  }
2077
2480
  if (!shouldCreate) return;
2078
- const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select4({
2481
+ const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select5({
2079
2482
  message: "Workspace visibility?",
2080
2483
  choices: [
2081
2484
  { name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
@@ -2106,7 +2509,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
2106
2509
  log.info(`--force: d\xF9ng ${alternative}`);
2107
2510
  return alternative;
2108
2511
  }
2109
- const useAlt = await confirm2({ message: `D\xF9ng "${alternative}" thay th\u1EBF?`, default: true });
2512
+ const useAlt = await confirm3({ message: `D\xF9ng "${alternative}" thay th\u1EBF?`, default: true });
2110
2513
  if (!useAlt) throw new Error("H\u1EE7y init. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c.");
2111
2514
  return alternative;
2112
2515
  }
@@ -2147,198 +2550,8 @@ function printInitSuccessBox(rootPath, flow, aiResult = null) {
2147
2550
  ` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
2148
2551
  ` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
2149
2552
  ];
2150
- process.stdout.write(`${boxen2(lines.join("\n"), { padding: 1, borderStyle: "round" })}
2151
- `);
2152
- }
2153
-
2154
- // src/commands/login.ts
2155
- import boxen3 from "boxen";
2156
- import open from "open";
2157
-
2158
- // src/lib/google-oauth-device-flow.ts
2159
- var GOOGLE_CLIENT_ID = "1014766441755-i4jimivh5rd7vt8phuhmepmt45sovtph.apps.googleusercontent.com";
2160
- var GOOGLE_CLIENT_SECRET = "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";
2161
- var HOSTED_DOMAIN = "nal.vn";
2162
- var SCOPES = ["openid", "email", "profile"];
2163
- var DEVICE_CODE_URL = "https://oauth2.googleapis.com/device/code";
2164
- var TOKEN_URL = "https://oauth2.googleapis.com/token";
2165
- var REVOKE_URL = "https://oauth2.googleapis.com/revoke";
2166
- async function requestDeviceCode() {
2167
- const body = new URLSearchParams({
2168
- client_id: GOOGLE_CLIENT_ID,
2169
- scope: SCOPES.join(" ")
2170
- });
2171
- const res = await fetch(DEVICE_CODE_URL, {
2172
- method: "POST",
2173
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
2174
- body
2175
- });
2176
- if (!res.ok) {
2177
- const text = await res.text();
2178
- throw new Error(`Device code request failed (${res.status}): ${text}`);
2179
- }
2180
- return await res.json();
2181
- }
2182
- async function pollForToken(deviceCode) {
2183
- const body = new URLSearchParams({
2184
- client_id: GOOGLE_CLIENT_ID,
2185
- client_secret: GOOGLE_CLIENT_SECRET,
2186
- device_code: deviceCode,
2187
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
2188
- });
2189
- const res = await fetch(TOKEN_URL, {
2190
- method: "POST",
2191
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
2192
- body
2193
- });
2194
- if (res.ok) {
2195
- return await res.json();
2196
- }
2197
- let errorCode = "";
2198
- try {
2199
- const data = await res.json();
2200
- errorCode = data.error ?? "";
2201
- } catch {
2202
- errorCode = "";
2203
- }
2204
- if (errorCode === "authorization_pending" || errorCode === "slow_down") {
2205
- return null;
2206
- }
2207
- if (errorCode === "access_denied") {
2208
- throw new Error("User t\u1EEB ch\u1ED1i quy\u1EC1n truy c\u1EADp");
2209
- }
2210
- if (errorCode === "expired_token") {
2211
- throw new Error("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
2212
- }
2213
- throw new Error(`OAuth token endpoint tr\u1EA3 l\u1ED7i: ${errorCode || res.status}`);
2214
- }
2215
- function decodeIdToken(idToken) {
2216
- const parts = idToken.split(".");
2217
- if (parts.length !== 3) {
2218
- throw new Error("id_token format kh\xF4ng h\u1EE3p l\u1EC7");
2219
- }
2220
- const payload = parts[1];
2221
- if (!payload) throw new Error("id_token thi\u1EBFu payload");
2222
- const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
2223
- const json = Buffer.from(base64, "base64").toString("utf8");
2224
- return JSON.parse(json);
2225
- }
2226
- function verifyHostedDomain(claims) {
2227
- if (claims.hd !== HOSTED_DOMAIN) {
2228
- throw new Error(
2229
- `Email kh\xF4ng thu\u1ED9c workspace NAL (y\xEAu c\u1EA7u @${HOSTED_DOMAIN}). Nh\u1EADn: ${claims.email}`
2230
- );
2231
- }
2232
- if (!claims.email_verified) {
2233
- throw new Error("Email ch\u01B0a \u0111\u01B0\u1EE3c Google verify");
2234
- }
2235
- }
2236
- function buildUserConfig(token, claims) {
2237
- const expiresAt = new Date(Date.now() + token.expires_in * 1e3).toISOString();
2238
- return {
2239
- email: claims.email,
2240
- name: claims.name ?? claims.email,
2241
- access_token: token.access_token,
2242
- refresh_token: token.refresh_token,
2243
- expires_at: expiresAt,
2244
- id_token: token.id_token
2245
- };
2246
- }
2247
- async function revokeToken(token) {
2248
- const body = new URLSearchParams({ token });
2249
- await fetch(REVOKE_URL, {
2250
- method: "POST",
2251
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
2252
- body
2253
- }).catch(() => {
2254
- });
2255
- }
2256
- function buildVerificationUrl(response) {
2257
- const url = new URL(response.verification_url);
2258
- url.searchParams.set("user_code", response.user_code);
2259
- url.searchParams.set("hd", HOSTED_DOMAIN);
2260
- return url.toString();
2261
- }
2262
-
2263
- // src/commands/login.ts
2264
- function registerLoginCommand(program2) {
2265
- program2.command("login").description("\u0110\u0103ng nh\u1EADp Google SSO (workspace @nal.vn)").option("--reset", "X\xF3a credential c\u0169 v\xE0 \u0111\u0103ng nh\u1EADp l\u1EA1i").action(async (opts) => {
2266
- try {
2267
- await runLogin(opts);
2268
- } catch (err) {
2269
- log.error(err instanceof Error ? err.message : String(err));
2270
- process.exit(1);
2271
- }
2272
- });
2273
- }
2274
- async function runLogin(opts) {
2275
- printAvatarBanner({ tagline: "\u0110\u0103ng nh\u1EADp Google SSO \xB7 workspace @nal.vn" });
2276
- if (opts.reset) {
2277
- await clearUserConfig();
2278
- await appendAuditEntry("login_reset");
2279
- } else {
2280
- const existing = await readUserConfig();
2281
- if (existing && !isTokenExpired(existing)) {
2282
- log.success(`\u0110\xE3 \u0111\u0103ng nh\u1EADp: ${existing.email}`);
2283
- return;
2284
- }
2285
- }
2286
- const deviceSpinner = spinner("\u0110ang y\xEAu c\u1EA7u device code t\u1EEB Google...");
2287
- let deviceCode;
2288
- try {
2289
- deviceCode = await requestDeviceCode();
2290
- deviceSpinner.succeed("Nh\u1EADn device code");
2291
- } catch (err) {
2292
- deviceSpinner.fail("Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c Google");
2293
- throw err;
2294
- }
2295
- const verificationUrl = buildVerificationUrl(deviceCode);
2296
- const instructions = [
2297
- `1. Truy c\u1EADp: ${chalk.cyan(deviceCode.verification_url)}`,
2298
- `2. Nh\u1EADp code: ${chalk.bold.yellow(deviceCode.user_code)}`,
2299
- "",
2300
- `Ho\u1EB7c Avatar t\u1EF1 m\u1EDF browser, click ${chalk.green("Allow")}...`
2301
- ].join("\n");
2302
- process.stdout.write(`${boxen3(instructions, { padding: 1, borderStyle: "round" })}
2553
+ process.stdout.write(`${boxen3(lines.join("\n"), { padding: 1, borderStyle: "round" })}
2303
2554
  `);
2304
- void open(verificationUrl).catch(() => {
2305
- log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
2306
- });
2307
- const waitSpinner = spinner("\u0110ang ch\u1EDD x\xE1c nh\u1EADn trong browser...");
2308
- const intervalMs = deviceCode.interval * 1e3;
2309
- const deadline = Date.now() + deviceCode.expires_in * 1e3;
2310
- let token = null;
2311
- while (Date.now() < deadline) {
2312
- await sleep(intervalMs);
2313
- try {
2314
- token = await pollForToken(deviceCode.device_code);
2315
- if (token) break;
2316
- } catch (err) {
2317
- waitSpinner.fail("X\xE1c th\u1EF1c th\u1EA5t b\u1EA1i");
2318
- throw err;
2319
- }
2320
- }
2321
- if (!token) {
2322
- waitSpinner.fail("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
2323
- process.exit(1);
2324
- }
2325
- waitSpinner.succeed("\u0110\xE3 nh\u1EADn token t\u1EEB Google");
2326
- const claims = decodeIdToken(token.id_token);
2327
- try {
2328
- verifyHostedDomain(claims);
2329
- } catch (err) {
2330
- await revokeToken(token.access_token);
2331
- throw err;
2332
- }
2333
- const userConfig = buildUserConfig(token, claims);
2334
- await writeUserConfig(userConfig);
2335
- await appendAuditEntry("login", userConfig.email);
2336
- log.success(`X\xE1c th\u1EF1c th\xE0nh c\xF4ng: ${userConfig.email}`);
2337
- log.success(`Verify hosted domain: ${claims.hd} \u2713`);
2338
- log.success(`L\u01B0u credential v\xE0o ${USER_CONFIG_PATH} (chmod 600)`);
2339
- }
2340
- function sleep(ms) {
2341
- return new Promise((resolve2) => setTimeout(resolve2, ms));
2342
2555
  }
2343
2556
 
2344
2557
  // src/commands/mcp-run.ts
@@ -2471,7 +2684,7 @@ function registerToolsCommand(program2) {
2471
2684
 
2472
2685
  // src/commands/uninstall.ts
2473
2686
  import { relative as relative3 } from "path";
2474
- import { confirm as confirm3 } from "@inquirer/prompts";
2687
+ import { confirm as confirm4 } from "@inquirer/prompts";
2475
2688
  import boxen5 from "boxen";
2476
2689
 
2477
2690
  // src/lib/create-uninstall-backup-snapshot.ts
@@ -2626,7 +2839,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
2626
2839
  }
2627
2840
 
2628
2841
  // src/commands/uninstall.ts
2629
- var CLI_VERSION = "1.2.1";
2842
+ var CLI_VERSION = "1.2.3";
2630
2843
  function registerUninstallCommand(program2) {
2631
2844
  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) => {
2632
2845
  try {
@@ -2650,7 +2863,7 @@ async function runUninstall(opts) {
2650
2863
  return;
2651
2864
  }
2652
2865
  if (!opts.yes) {
2653
- const ok = await confirm3({
2866
+ const ok = await confirm4({
2654
2867
  message: "Ti\u1EBFp t\u1EE5c g\u1EE1 Avatar?",
2655
2868
  default: false
2656
2869
  });
@@ -2708,7 +2921,7 @@ function printUninstallSuccessBox(backupPath) {
2708
2921
  }
2709
2922
 
2710
2923
  // src/index.ts
2711
- var CLI_VERSION2 = "1.2.1";
2924
+ var CLI_VERSION2 = "1.2.3";
2712
2925
  var program = new Command();
2713
2926
  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(
2714
2927
  "beforeAll",