@nalvietnam/avatar-cli 1.2.1 → 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 +202 -197
- package/dist/index.js.map +1 -1
- package/dist/lib/print-welcome-screen.js +1 -1
- package/dist/lib/print-welcome-screen.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1109,7 +1109,7 @@ async function applyFixes(checks) {
|
|
|
1109
1109
|
// src/commands/init.ts
|
|
1110
1110
|
import { basename, join as join16, relative as relative2, resolve } from "path";
|
|
1111
1111
|
import { confirm as confirm2, input as input2, select as select4 } from "@inquirer/prompts";
|
|
1112
|
-
import
|
|
1112
|
+
import boxen3 from "boxen";
|
|
1113
1113
|
|
|
1114
1114
|
// src/lib/avatar-ascii-banner.ts
|
|
1115
1115
|
import chalk2 from "chalk";
|
|
@@ -1778,6 +1778,196 @@ function buildScaffoldVariables(args) {
|
|
|
1778
1778
|
};
|
|
1779
1779
|
}
|
|
1780
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
|
+
|
|
1781
1971
|
// src/commands/init.ts
|
|
1782
1972
|
function parseBootstrapStrategyOpts(opts) {
|
|
1783
1973
|
if (opts.preserveUncommitted) return "stash";
|
|
@@ -1812,10 +2002,15 @@ async function runInit(opts) {
|
|
|
1812
2002
|
if (opts.mode) {
|
|
1813
2003
|
log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
|
|
1814
2004
|
}
|
|
1815
|
-
|
|
2005
|
+
let userConfig = await readUserConfig();
|
|
1816
2006
|
if (!userConfig || isTokenExpired(userConfig)) {
|
|
1817
|
-
log.
|
|
1818
|
-
|
|
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
|
+
}
|
|
1819
2014
|
}
|
|
1820
2015
|
const status = opts.projectStatus ?? await promptProjectStatus();
|
|
1821
2016
|
switch (status) {
|
|
@@ -2147,200 +2342,10 @@ function printInitSuccessBox(rootPath, flow, aiResult = null) {
|
|
|
2147
2342
|
` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
|
|
2148
2343
|
` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
|
|
2149
2344
|
];
|
|
2150
|
-
process.stdout.write(`${
|
|
2345
|
+
process.stdout.write(`${boxen3(lines.join("\n"), { padding: 1, borderStyle: "round" })}
|
|
2151
2346
|
`);
|
|
2152
2347
|
}
|
|
2153
2348
|
|
|
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" })}
|
|
2303
|
-
`);
|
|
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
|
-
}
|
|
2343
|
-
|
|
2344
2349
|
// src/commands/mcp-run.ts
|
|
2345
2350
|
function registerMcpRunCommand(program2) {
|
|
2346
2351
|
program2.command("mcp-run <tool-id>", { hidden: true }).description("[internal] Spawn MCP v\u1EDBi secrets injected (M09)").action(notImplementedYet("mcp-run", "Milestone 09"));
|
|
@@ -2626,7 +2631,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
|
|
|
2626
2631
|
}
|
|
2627
2632
|
|
|
2628
2633
|
// src/commands/uninstall.ts
|
|
2629
|
-
var CLI_VERSION = "1.2.
|
|
2634
|
+
var CLI_VERSION = "1.2.2";
|
|
2630
2635
|
function registerUninstallCommand(program2) {
|
|
2631
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) => {
|
|
2632
2637
|
try {
|
|
@@ -2708,7 +2713,7 @@ function printUninstallSuccessBox(backupPath) {
|
|
|
2708
2713
|
}
|
|
2709
2714
|
|
|
2710
2715
|
// src/index.ts
|
|
2711
|
-
var CLI_VERSION2 = "1.2.
|
|
2716
|
+
var CLI_VERSION2 = "1.2.2";
|
|
2712
2717
|
var program = new Command();
|
|
2713
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(
|
|
2714
2719
|
"beforeAll",
|