@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 +447 -234
- 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
|
@@ -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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
})
|
|
700
|
-
|
|
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
|
|
1112
|
-
import
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2213
|
+
let userConfig = await readUserConfig();
|
|
1816
2214
|
if (!userConfig || isTokenExpired(userConfig)) {
|
|
1817
|
-
log.
|
|
1818
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(`${
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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",
|