@nalvietnam/avatar-cli 1.2.0 → 1.2.2

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
@@ -157,8 +157,17 @@ function spinner(text) {
157
157
  var QUOTA_VERIFY_TIMEOUT_MS = 3e4;
158
158
  var QUOTA_VERIFY_PROMPT = "ok";
159
159
  function checkClaudeCodeSubscriptionAuth() {
160
- const result = spawnSync("claude", ["auth", "status"], { stdio: "ignore" });
160
+ const result = spawnSync("claude", ["auth", "status"], { encoding: "utf8" });
161
161
  if (result.error || result.status !== 0) return "not-authenticated";
162
+ const stdout = (result.stdout || "").trim();
163
+ if (stdout.startsWith("{")) {
164
+ try {
165
+ const parsed = JSON.parse(stdout);
166
+ return parsed.loggedIn === true ? "authenticated" : "not-authenticated";
167
+ } catch {
168
+ return "authenticated";
169
+ }
170
+ }
162
171
  return "authenticated";
163
172
  }
164
173
  function triggerClaudeCodeAuthLogin() {
@@ -179,14 +188,37 @@ function classifyQuotaError(combinedOutput) {
179
188
  if (text.includes("insufficient_quota") || text.includes("insufficient quota")) {
180
189
  return "insufficient_quota";
181
190
  }
191
+ if (text.includes("quota_exceeded") || text.includes("quota exceeded") || text.includes("usage limit") || text.includes("you've used all")) {
192
+ return "insufficient_quota";
193
+ }
194
+ if (text.includes("401") || text.includes("invalid authentication") || text.includes("authentication credentials") || text.includes("failed to authenticate") || text.includes("authentication failed") || text.includes("unauthorized")) {
195
+ return "auth-expired";
196
+ }
182
197
  if (text.includes("invalid_api_key") || text.includes("invalid api key")) {
183
198
  return "invalid_api_key";
184
199
  }
185
- if (text.includes("rate_limit") || text.includes("rate limit")) {
200
+ if (text.includes("rate_limit") || text.includes("rate limit") || text.includes("429")) {
186
201
  return "rate_limit";
187
202
  }
188
203
  return "unknown";
189
204
  }
205
+ function getQuotaErrorHint(reason) {
206
+ switch (reason) {
207
+ case "auth-expired":
208
+ return "Token Claude Code \u0111\xE3 h\u1EBFt h\u1EA1n/b\u1ECB revoke. Ch\u1EA1y: `claude auth logout && claude auth login`.";
209
+ case "credit_balance_too_low":
210
+ case "insufficient_quota":
211
+ return "H\u1EBFt quota subscription. Upgrade plan ho\u1EB7c d\xF9ng LLMLite (avatar ai setup \u2192 ch\u1ECDn LLMLite).";
212
+ case "invalid_api_key":
213
+ return "API key invalid. Re-login: `claude auth login`.";
214
+ case "rate_limit":
215
+ return "B\u1ECB rate limit t\u1EA1m th\u1EDDi. Ch\u1EDD v\xE0i ph\xFAt r\u1ED3i ch\u1EA1y `avatar ai setup`.";
216
+ case "timeout":
217
+ return "Network ch\u1EADm ho\u1EB7c Anthropic API down. Check m\u1EA1ng r\u1ED3i ch\u1EA1y l\u1EA1i.";
218
+ default:
219
+ return "L\u1ED7i ch\u01B0a bi\u1EBFt. Xem stderr \u1EDF tr\xEAn + ch\u1EA1y `claude --print ok` tay \u0111\u1EC3 debug.";
220
+ }
221
+ }
190
222
  function verifyClaudeCodeQuota() {
191
223
  const result = spawnSync("claude", ["--print", QUOTA_VERIFY_PROMPT], {
192
224
  encoding: "utf8",
@@ -202,6 +234,9 @@ function verifyClaudeCodeQuota() {
202
234
  }
203
235
  const reason = classifyQuotaError(`${stderr}
204
236
  ${stdout}`);
237
+ if (reason === "unknown" && stderr.trim()) {
238
+ log.dim(`[debug] claude --print stderr: ${stderr.slice(0, 500)}`);
239
+ }
205
240
  return { ok: false, reason, detail: stderr.slice(0, 500) };
206
241
  }
207
242
 
@@ -221,12 +256,8 @@ var VERSION_PROBE_TIMEOUT_MS = 5e3;
221
256
  var SEMVER_REGEX = /(\d+\.\d+\.\d+)/;
222
257
  function probeClaudeBinaryPath() {
223
258
  const isWindows = detectHostPlatform() === "win32";
224
- const probeCmd = isWindows ? "where" : "command";
225
- const probeArgs = isWindows ? ["claude"] : ["-v", "claude"];
226
- const result = spawnSync2(probeCmd, probeArgs, {
227
- encoding: "utf8",
228
- shell: !isWindows
229
- });
259
+ const probeCmd = isWindows ? "where" : "which";
260
+ const result = spawnSync2(probeCmd, ["claude"], { encoding: "utf8" });
230
261
  if (result.error || result.status !== 0) return null;
231
262
  const out = (result.stdout || "").trim();
232
263
  if (!out) return null;
@@ -562,16 +593,21 @@ async function runAiSetupPhase(args) {
562
593
  if (checkClaudeCodeSubscriptionAuth() !== "authenticated") {
563
594
  triggerClaudeCodeAuthLogin();
564
595
  }
565
- const quota = verifyClaudeCodeQuota();
596
+ let quota = verifyClaudeCodeQuota();
597
+ if (!quota.ok && quota.reason === "auth-expired") {
598
+ log.warn("Token Claude Code \u0111\xE3 h\u1EBFt h\u1EA1n. T\u1EF1 \u0111\u1ED9ng re-login...");
599
+ triggerClaudeCodeAuthLogin();
600
+ quota = verifyClaudeCodeQuota();
601
+ }
566
602
  if (!quota.ok) {
603
+ const reason = quota.reason ?? "unknown";
567
604
  await appendAuditEntry(
568
605
  "ai_setup",
569
- `provider=subscription,result=no-quota,reason=${quota.reason ?? "unknown"}`
570
- );
571
- log.warn(
572
- `Subscription verify th\u1EA5t b\u1EA1i (${quota.reason ?? "unknown"}). Suggest LLMLite ho\u1EB7c upgrade plan.`
606
+ `provider=subscription,result=no-quota,reason=${reason}`
573
607
  );
574
- return { ok: false, reason: `subscription-${quota.reason ?? "unknown"}`, phase: "quota" };
608
+ log.warn(`Subscription verify th\u1EA5t b\u1EA1i (${reason}).`);
609
+ log.dim(`\u2192 ${getQuotaErrorHint(reason)}`);
610
+ return { ok: false, reason: `subscription-${reason}`, phase: "quota" };
575
611
  }
576
612
  await writeClaudeSettings(args.workspacePath, {
577
613
  provider: "subscription",
@@ -1073,7 +1109,7 @@ async function applyFixes(checks) {
1073
1109
  // src/commands/init.ts
1074
1110
  import { basename, join as join16, relative as relative2, resolve } from "path";
1075
1111
  import { confirm as confirm2, input as input2, select as select4 } from "@inquirer/prompts";
1076
- import boxen2 from "boxen";
1112
+ import boxen3 from "boxen";
1077
1113
 
1078
1114
  // src/lib/avatar-ascii-banner.ts
1079
1115
  import chalk2 from "chalk";
@@ -1742,6 +1778,196 @@ function buildScaffoldVariables(args) {
1742
1778
  };
1743
1779
  }
1744
1780
 
1781
+ // src/commands/login.ts
1782
+ import boxen2 from "boxen";
1783
+ import open from "open";
1784
+
1785
+ // src/lib/google-oauth-device-flow.ts
1786
+ var GOOGLE_CLIENT_ID = "1014766441755-i4jimivh5rd7vt8phuhmepmt45sovtph.apps.googleusercontent.com";
1787
+ var GOOGLE_CLIENT_SECRET = "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";
1788
+ var HOSTED_DOMAIN = "nal.vn";
1789
+ var SCOPES = ["openid", "email", "profile"];
1790
+ var DEVICE_CODE_URL = "https://oauth2.googleapis.com/device/code";
1791
+ var TOKEN_URL = "https://oauth2.googleapis.com/token";
1792
+ var REVOKE_URL = "https://oauth2.googleapis.com/revoke";
1793
+ async function requestDeviceCode() {
1794
+ const body = new URLSearchParams({
1795
+ client_id: GOOGLE_CLIENT_ID,
1796
+ scope: SCOPES.join(" ")
1797
+ });
1798
+ const res = await fetch(DEVICE_CODE_URL, {
1799
+ method: "POST",
1800
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1801
+ body
1802
+ });
1803
+ if (!res.ok) {
1804
+ const text = await res.text();
1805
+ throw new Error(`Device code request failed (${res.status}): ${text}`);
1806
+ }
1807
+ return await res.json();
1808
+ }
1809
+ async function pollForToken(deviceCode) {
1810
+ const body = new URLSearchParams({
1811
+ client_id: GOOGLE_CLIENT_ID,
1812
+ client_secret: GOOGLE_CLIENT_SECRET,
1813
+ device_code: deviceCode,
1814
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1815
+ });
1816
+ const res = await fetch(TOKEN_URL, {
1817
+ method: "POST",
1818
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1819
+ body
1820
+ });
1821
+ if (res.ok) {
1822
+ return await res.json();
1823
+ }
1824
+ let errorCode = "";
1825
+ try {
1826
+ const data = await res.json();
1827
+ errorCode = data.error ?? "";
1828
+ } catch {
1829
+ errorCode = "";
1830
+ }
1831
+ if (errorCode === "authorization_pending" || errorCode === "slow_down") {
1832
+ return null;
1833
+ }
1834
+ if (errorCode === "access_denied") {
1835
+ throw new Error("User t\u1EEB ch\u1ED1i quy\u1EC1n truy c\u1EADp");
1836
+ }
1837
+ if (errorCode === "expired_token") {
1838
+ throw new Error("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
1839
+ }
1840
+ throw new Error(`OAuth token endpoint tr\u1EA3 l\u1ED7i: ${errorCode || res.status}`);
1841
+ }
1842
+ function decodeIdToken(idToken) {
1843
+ const parts = idToken.split(".");
1844
+ if (parts.length !== 3) {
1845
+ throw new Error("id_token format kh\xF4ng h\u1EE3p l\u1EC7");
1846
+ }
1847
+ const payload = parts[1];
1848
+ if (!payload) throw new Error("id_token thi\u1EBFu payload");
1849
+ const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
1850
+ const json = Buffer.from(base64, "base64").toString("utf8");
1851
+ return JSON.parse(json);
1852
+ }
1853
+ function verifyHostedDomain(claims) {
1854
+ if (claims.hd !== HOSTED_DOMAIN) {
1855
+ throw new Error(
1856
+ `Email kh\xF4ng thu\u1ED9c workspace NAL (y\xEAu c\u1EA7u @${HOSTED_DOMAIN}). Nh\u1EADn: ${claims.email}`
1857
+ );
1858
+ }
1859
+ if (!claims.email_verified) {
1860
+ throw new Error("Email ch\u01B0a \u0111\u01B0\u1EE3c Google verify");
1861
+ }
1862
+ }
1863
+ function buildUserConfig(token, claims) {
1864
+ const expiresAt = new Date(Date.now() + token.expires_in * 1e3).toISOString();
1865
+ return {
1866
+ email: claims.email,
1867
+ name: claims.name ?? claims.email,
1868
+ access_token: token.access_token,
1869
+ refresh_token: token.refresh_token,
1870
+ expires_at: expiresAt,
1871
+ id_token: token.id_token
1872
+ };
1873
+ }
1874
+ async function revokeToken(token) {
1875
+ const body = new URLSearchParams({ token });
1876
+ await fetch(REVOKE_URL, {
1877
+ method: "POST",
1878
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1879
+ body
1880
+ }).catch(() => {
1881
+ });
1882
+ }
1883
+ function buildVerificationUrl(response) {
1884
+ const url = new URL(response.verification_url);
1885
+ url.searchParams.set("user_code", response.user_code);
1886
+ url.searchParams.set("hd", HOSTED_DOMAIN);
1887
+ return url.toString();
1888
+ }
1889
+
1890
+ // src/commands/login.ts
1891
+ function registerLoginCommand(program2) {
1892
+ 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) => {
1893
+ try {
1894
+ await runLogin(opts);
1895
+ } catch (err) {
1896
+ log.error(err instanceof Error ? err.message : String(err));
1897
+ process.exit(1);
1898
+ }
1899
+ });
1900
+ }
1901
+ async function runLogin(opts) {
1902
+ printAvatarBanner({ tagline: "\u0110\u0103ng nh\u1EADp Google SSO \xB7 workspace @nal.vn" });
1903
+ if (opts.reset) {
1904
+ await clearUserConfig();
1905
+ await appendAuditEntry("login_reset");
1906
+ } else {
1907
+ const existing = await readUserConfig();
1908
+ if (existing && !isTokenExpired(existing)) {
1909
+ log.success(`\u0110\xE3 \u0111\u0103ng nh\u1EADp: ${existing.email}`);
1910
+ return;
1911
+ }
1912
+ }
1913
+ const deviceSpinner = spinner("\u0110ang y\xEAu c\u1EA7u device code t\u1EEB Google...");
1914
+ let deviceCode;
1915
+ try {
1916
+ deviceCode = await requestDeviceCode();
1917
+ deviceSpinner.succeed("Nh\u1EADn device code");
1918
+ } catch (err) {
1919
+ deviceSpinner.fail("Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c Google");
1920
+ throw err;
1921
+ }
1922
+ const verificationUrl = buildVerificationUrl(deviceCode);
1923
+ const instructions = [
1924
+ `1. Truy c\u1EADp: ${chalk.cyan(deviceCode.verification_url)}`,
1925
+ `2. Nh\u1EADp code: ${chalk.bold.yellow(deviceCode.user_code)}`,
1926
+ "",
1927
+ `Ho\u1EB7c Avatar t\u1EF1 m\u1EDF browser, click ${chalk.green("Allow")}...`
1928
+ ].join("\n");
1929
+ process.stdout.write(`${boxen2(instructions, { padding: 1, borderStyle: "round" })}
1930
+ `);
1931
+ void open(verificationUrl).catch(() => {
1932
+ log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
1933
+ });
1934
+ const waitSpinner = spinner("\u0110ang ch\u1EDD x\xE1c nh\u1EADn trong browser...");
1935
+ const intervalMs = deviceCode.interval * 1e3;
1936
+ const deadline = Date.now() + deviceCode.expires_in * 1e3;
1937
+ let token = null;
1938
+ while (Date.now() < deadline) {
1939
+ await sleep(intervalMs);
1940
+ try {
1941
+ token = await pollForToken(deviceCode.device_code);
1942
+ if (token) break;
1943
+ } catch (err) {
1944
+ waitSpinner.fail("X\xE1c th\u1EF1c th\u1EA5t b\u1EA1i");
1945
+ throw err;
1946
+ }
1947
+ }
1948
+ if (!token) {
1949
+ waitSpinner.fail("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
1950
+ process.exit(1);
1951
+ }
1952
+ waitSpinner.succeed("\u0110\xE3 nh\u1EADn token t\u1EEB Google");
1953
+ const claims = decodeIdToken(token.id_token);
1954
+ try {
1955
+ verifyHostedDomain(claims);
1956
+ } catch (err) {
1957
+ await revokeToken(token.access_token);
1958
+ throw err;
1959
+ }
1960
+ const userConfig = buildUserConfig(token, claims);
1961
+ await writeUserConfig(userConfig);
1962
+ await appendAuditEntry("login", userConfig.email);
1963
+ log.success(`X\xE1c th\u1EF1c th\xE0nh c\xF4ng: ${userConfig.email}`);
1964
+ log.success(`Verify hosted domain: ${claims.hd} \u2713`);
1965
+ log.success(`L\u01B0u credential v\xE0o ${USER_CONFIG_PATH} (chmod 600)`);
1966
+ }
1967
+ function sleep(ms) {
1968
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1969
+ }
1970
+
1745
1971
  // src/commands/init.ts
1746
1972
  function parseBootstrapStrategyOpts(opts) {
1747
1973
  if (opts.preserveUncommitted) return "stash";
@@ -1776,10 +2002,15 @@ async function runInit(opts) {
1776
2002
  if (opts.mode) {
1777
2003
  log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
1778
2004
  }
1779
- const userConfig = await readUserConfig();
2005
+ let userConfig = await readUserConfig();
1780
2006
  if (!userConfig || isTokenExpired(userConfig)) {
1781
- log.error("Ch\u01B0a \u0111\u0103ng nh\u1EADp ho\u1EB7c token h\u1EBFt h\u1EA1n. Ch\u1EA1y 'avatar login' tr\u01B0\u1EDBc.");
1782
- process.exit(1);
2007
+ log.info("Ch\u01B0a \u0111\u0103ng nh\u1EADp \u2014 ch\u1EA1y login flow tr\u01B0\u1EDBc khi init...");
2008
+ await runLogin({});
2009
+ userConfig = await readUserConfig();
2010
+ if (!userConfig || isTokenExpired(userConfig)) {
2011
+ log.error("Login kh\xF4ng ho\xE0n t\u1EA5t. Ch\u1EA1y 'avatar login' tay r\u1ED3i init l\u1EA1i.");
2012
+ process.exit(1);
2013
+ }
1783
2014
  }
1784
2015
  const status = opts.projectStatus ?? await promptProjectStatus();
1785
2016
  switch (status) {
@@ -2111,200 +2342,10 @@ function printInitSuccessBox(rootPath, flow, aiResult = null) {
2111
2342
  ` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
2112
2343
  ` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
2113
2344
  ];
2114
- process.stdout.write(`${boxen2(lines.join("\n"), { padding: 1, borderStyle: "round" })}
2345
+ process.stdout.write(`${boxen3(lines.join("\n"), { padding: 1, borderStyle: "round" })}
2115
2346
  `);
2116
2347
  }
2117
2348
 
2118
- // src/commands/login.ts
2119
- import boxen3 from "boxen";
2120
- import open from "open";
2121
-
2122
- // src/lib/google-oauth-device-flow.ts
2123
- var GOOGLE_CLIENT_ID = "1014766441755-i4jimivh5rd7vt8phuhmepmt45sovtph.apps.googleusercontent.com";
2124
- var GOOGLE_CLIENT_SECRET = "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";
2125
- var HOSTED_DOMAIN = "nal.vn";
2126
- var SCOPES = ["openid", "email", "profile"];
2127
- var DEVICE_CODE_URL = "https://oauth2.googleapis.com/device/code";
2128
- var TOKEN_URL = "https://oauth2.googleapis.com/token";
2129
- var REVOKE_URL = "https://oauth2.googleapis.com/revoke";
2130
- async function requestDeviceCode() {
2131
- const body = new URLSearchParams({
2132
- client_id: GOOGLE_CLIENT_ID,
2133
- scope: SCOPES.join(" ")
2134
- });
2135
- const res = await fetch(DEVICE_CODE_URL, {
2136
- method: "POST",
2137
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
2138
- body
2139
- });
2140
- if (!res.ok) {
2141
- const text = await res.text();
2142
- throw new Error(`Device code request failed (${res.status}): ${text}`);
2143
- }
2144
- return await res.json();
2145
- }
2146
- async function pollForToken(deviceCode) {
2147
- const body = new URLSearchParams({
2148
- client_id: GOOGLE_CLIENT_ID,
2149
- client_secret: GOOGLE_CLIENT_SECRET,
2150
- device_code: deviceCode,
2151
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
2152
- });
2153
- const res = await fetch(TOKEN_URL, {
2154
- method: "POST",
2155
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
2156
- body
2157
- });
2158
- if (res.ok) {
2159
- return await res.json();
2160
- }
2161
- let errorCode = "";
2162
- try {
2163
- const data = await res.json();
2164
- errorCode = data.error ?? "";
2165
- } catch {
2166
- errorCode = "";
2167
- }
2168
- if (errorCode === "authorization_pending" || errorCode === "slow_down") {
2169
- return null;
2170
- }
2171
- if (errorCode === "access_denied") {
2172
- throw new Error("User t\u1EEB ch\u1ED1i quy\u1EC1n truy c\u1EADp");
2173
- }
2174
- if (errorCode === "expired_token") {
2175
- throw new Error("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
2176
- }
2177
- throw new Error(`OAuth token endpoint tr\u1EA3 l\u1ED7i: ${errorCode || res.status}`);
2178
- }
2179
- function decodeIdToken(idToken) {
2180
- const parts = idToken.split(".");
2181
- if (parts.length !== 3) {
2182
- throw new Error("id_token format kh\xF4ng h\u1EE3p l\u1EC7");
2183
- }
2184
- const payload = parts[1];
2185
- if (!payload) throw new Error("id_token thi\u1EBFu payload");
2186
- const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
2187
- const json = Buffer.from(base64, "base64").toString("utf8");
2188
- return JSON.parse(json);
2189
- }
2190
- function verifyHostedDomain(claims) {
2191
- if (claims.hd !== HOSTED_DOMAIN) {
2192
- throw new Error(
2193
- `Email kh\xF4ng thu\u1ED9c workspace NAL (y\xEAu c\u1EA7u @${HOSTED_DOMAIN}). Nh\u1EADn: ${claims.email}`
2194
- );
2195
- }
2196
- if (!claims.email_verified) {
2197
- throw new Error("Email ch\u01B0a \u0111\u01B0\u1EE3c Google verify");
2198
- }
2199
- }
2200
- function buildUserConfig(token, claims) {
2201
- const expiresAt = new Date(Date.now() + token.expires_in * 1e3).toISOString();
2202
- return {
2203
- email: claims.email,
2204
- name: claims.name ?? claims.email,
2205
- access_token: token.access_token,
2206
- refresh_token: token.refresh_token,
2207
- expires_at: expiresAt,
2208
- id_token: token.id_token
2209
- };
2210
- }
2211
- async function revokeToken(token) {
2212
- const body = new URLSearchParams({ token });
2213
- await fetch(REVOKE_URL, {
2214
- method: "POST",
2215
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
2216
- body
2217
- }).catch(() => {
2218
- });
2219
- }
2220
- function buildVerificationUrl(response) {
2221
- const url = new URL(response.verification_url);
2222
- url.searchParams.set("user_code", response.user_code);
2223
- url.searchParams.set("hd", HOSTED_DOMAIN);
2224
- return url.toString();
2225
- }
2226
-
2227
- // src/commands/login.ts
2228
- function registerLoginCommand(program2) {
2229
- 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) => {
2230
- try {
2231
- await runLogin(opts);
2232
- } catch (err) {
2233
- log.error(err instanceof Error ? err.message : String(err));
2234
- process.exit(1);
2235
- }
2236
- });
2237
- }
2238
- async function runLogin(opts) {
2239
- printAvatarBanner({ tagline: "\u0110\u0103ng nh\u1EADp Google SSO \xB7 workspace @nal.vn" });
2240
- if (opts.reset) {
2241
- await clearUserConfig();
2242
- await appendAuditEntry("login_reset");
2243
- } else {
2244
- const existing = await readUserConfig();
2245
- if (existing && !isTokenExpired(existing)) {
2246
- log.success(`\u0110\xE3 \u0111\u0103ng nh\u1EADp: ${existing.email}`);
2247
- return;
2248
- }
2249
- }
2250
- const deviceSpinner = spinner("\u0110ang y\xEAu c\u1EA7u device code t\u1EEB Google...");
2251
- let deviceCode;
2252
- try {
2253
- deviceCode = await requestDeviceCode();
2254
- deviceSpinner.succeed("Nh\u1EADn device code");
2255
- } catch (err) {
2256
- deviceSpinner.fail("Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c Google");
2257
- throw err;
2258
- }
2259
- const verificationUrl = buildVerificationUrl(deviceCode);
2260
- const instructions = [
2261
- `1. Truy c\u1EADp: ${chalk.cyan(deviceCode.verification_url)}`,
2262
- `2. Nh\u1EADp code: ${chalk.bold.yellow(deviceCode.user_code)}`,
2263
- "",
2264
- `Ho\u1EB7c Avatar t\u1EF1 m\u1EDF browser, click ${chalk.green("Allow")}...`
2265
- ].join("\n");
2266
- process.stdout.write(`${boxen3(instructions, { padding: 1, borderStyle: "round" })}
2267
- `);
2268
- void open(verificationUrl).catch(() => {
2269
- log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
2270
- });
2271
- const waitSpinner = spinner("\u0110ang ch\u1EDD x\xE1c nh\u1EADn trong browser...");
2272
- const intervalMs = deviceCode.interval * 1e3;
2273
- const deadline = Date.now() + deviceCode.expires_in * 1e3;
2274
- let token = null;
2275
- while (Date.now() < deadline) {
2276
- await sleep(intervalMs);
2277
- try {
2278
- token = await pollForToken(deviceCode.device_code);
2279
- if (token) break;
2280
- } catch (err) {
2281
- waitSpinner.fail("X\xE1c th\u1EF1c th\u1EA5t b\u1EA1i");
2282
- throw err;
2283
- }
2284
- }
2285
- if (!token) {
2286
- waitSpinner.fail("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
2287
- process.exit(1);
2288
- }
2289
- waitSpinner.succeed("\u0110\xE3 nh\u1EADn token t\u1EEB Google");
2290
- const claims = decodeIdToken(token.id_token);
2291
- try {
2292
- verifyHostedDomain(claims);
2293
- } catch (err) {
2294
- await revokeToken(token.access_token);
2295
- throw err;
2296
- }
2297
- const userConfig = buildUserConfig(token, claims);
2298
- await writeUserConfig(userConfig);
2299
- await appendAuditEntry("login", userConfig.email);
2300
- log.success(`X\xE1c th\u1EF1c th\xE0nh c\xF4ng: ${userConfig.email}`);
2301
- log.success(`Verify hosted domain: ${claims.hd} \u2713`);
2302
- log.success(`L\u01B0u credential v\xE0o ${USER_CONFIG_PATH} (chmod 600)`);
2303
- }
2304
- function sleep(ms) {
2305
- return new Promise((resolve2) => setTimeout(resolve2, ms));
2306
- }
2307
-
2308
2349
  // src/commands/mcp-run.ts
2309
2350
  function registerMcpRunCommand(program2) {
2310
2351
  program2.command("mcp-run <tool-id>", { hidden: true }).description("[internal] Spawn MCP v\u1EDBi secrets injected (M09)").action(notImplementedYet("mcp-run", "Milestone 09"));
@@ -2590,7 +2631,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
2590
2631
  }
2591
2632
 
2592
2633
  // src/commands/uninstall.ts
2593
- var CLI_VERSION = "1.2.0";
2634
+ var CLI_VERSION = "1.2.2";
2594
2635
  function registerUninstallCommand(program2) {
2595
2636
  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) => {
2596
2637
  try {
@@ -2672,7 +2713,7 @@ function printUninstallSuccessBox(backupPath) {
2672
2713
  }
2673
2714
 
2674
2715
  // src/index.ts
2675
- var CLI_VERSION2 = "1.2.0";
2716
+ var CLI_VERSION2 = "1.2.2";
2676
2717
  var program = new Command();
2677
2718
  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(
2678
2719
  "beforeAll",