@nalvietnam/avatar-cli 1.11.1 → 1.11.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 +1681 -1662
- package/dist/index.js.map +1 -1
- package/dist/lib/print-welcome-screen.js +1 -13
- package/dist/lib/print-welcome-screen.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -291,7 +291,8 @@ function getQuotaErrorHint(reason) {
|
|
|
291
291
|
function verifyClaudeCodeQuota() {
|
|
292
292
|
const result = spawnSync("claude", ["--print", QUOTA_VERIFY_PROMPT], {
|
|
293
293
|
encoding: "utf8",
|
|
294
|
-
timeout: QUOTA_VERIFY_TIMEOUT_MS
|
|
294
|
+
timeout: QUOTA_VERIFY_TIMEOUT_MS,
|
|
295
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
295
296
|
});
|
|
296
297
|
if (result.signal === "SIGTERM") {
|
|
297
298
|
return { ok: false, reason: "timeout", detail: "claude --print > 30s" };
|
|
@@ -301,12 +302,24 @@ function verifyClaudeCodeQuota() {
|
|
|
301
302
|
if (result.status === 0) {
|
|
302
303
|
return { ok: true };
|
|
303
304
|
}
|
|
305
|
+
const stdoutTrimmed = stdout.trim();
|
|
306
|
+
const stderrLower = stderr.toLowerCase();
|
|
307
|
+
if (stdoutTrimmed.length > 20 && !stderrLower.includes("error") && !stderrLower.includes("limit") && !stderrLower.includes("quota") && !stderrLower.includes("401")) {
|
|
308
|
+
log.warn(
|
|
309
|
+
`claude --print exit=${result.status} nh\u01B0ng c\xF3 response (${stdoutTrimmed.length} chars). Accept v\u1EDBi caution.`
|
|
310
|
+
);
|
|
311
|
+
return { ok: true };
|
|
312
|
+
}
|
|
304
313
|
const reason = classifyQuotaError(`${stderr}
|
|
305
314
|
${stdout}`);
|
|
306
|
-
if (reason === "unknown"
|
|
307
|
-
log.
|
|
315
|
+
if (reason === "unknown") {
|
|
316
|
+
log.warn(
|
|
317
|
+
`[debug] claude --print exit=${result.status} signal=${result.signal ?? "none"}`
|
|
318
|
+
);
|
|
319
|
+
if (stderr.trim()) log.warn(`[debug] stderr: ${stderr.slice(0, 500)}`);
|
|
320
|
+
if (stdout.trim()) log.warn(`[debug] stdout: ${stdout.slice(0, 300)}`);
|
|
308
321
|
}
|
|
309
|
-
return { ok: false, reason, detail: stderr.slice(0, 500) };
|
|
322
|
+
return { ok: false, reason, detail: stderr.slice(0, 500) || stdout.slice(0, 500) };
|
|
310
323
|
}
|
|
311
324
|
|
|
312
325
|
// src/lib/detect-claude-code-installation.ts
|
|
@@ -340,7 +353,7 @@ function probeClaudeVersion() {
|
|
|
340
353
|
if (result.error || result.status !== 0) return null;
|
|
341
354
|
const out = (result.stdout || "").trim();
|
|
342
355
|
const match = SEMVER_REGEX.exec(out);
|
|
343
|
-
return match
|
|
356
|
+
return match?.[1] ?? null;
|
|
344
357
|
}
|
|
345
358
|
function detectClaudeCodeInstallation() {
|
|
346
359
|
const path = probeClaudeBinaryPath();
|
|
@@ -556,8 +569,9 @@ async function fetchAnthropicModels(apiKey) {
|
|
|
556
569
|
}
|
|
557
570
|
async function promptAnthropicModelChoice(models) {
|
|
558
571
|
if (models.length === 1) {
|
|
559
|
-
|
|
560
|
-
|
|
572
|
+
const only = models[0];
|
|
573
|
+
log.info(`Auto-pick model: ${only} (ch\u1EC9 1 model available)`);
|
|
574
|
+
return only;
|
|
561
575
|
}
|
|
562
576
|
const sorted = [...models].sort((a, b) => {
|
|
563
577
|
const score = (m) => {
|
|
@@ -645,8 +659,9 @@ async function fetchAvailableModels(baseUrl, apiKey) {
|
|
|
645
659
|
async function promptModelChoice(models) {
|
|
646
660
|
const claudeAliases = models.filter((m) => m.toLowerCase().includes("claude"));
|
|
647
661
|
if (claudeAliases.length === 1) {
|
|
648
|
-
|
|
649
|
-
|
|
662
|
+
const only = claudeAliases[0];
|
|
663
|
+
log.info(`Auto-pick model: ${only} (ch\u1EC9 1 claude alias tr\xEAn endpoint)`);
|
|
664
|
+
return only;
|
|
650
665
|
}
|
|
651
666
|
const choiceList = claudeAliases.length > 0 ? claudeAliases : models;
|
|
652
667
|
return await select3({
|
|
@@ -733,22 +748,22 @@ function applyUseGlobal(existing, source) {
|
|
|
733
748
|
...sourceModel ? { model: sourceModel } : {}
|
|
734
749
|
};
|
|
735
750
|
}
|
|
736
|
-
async function writeClaudeSettings(workspacePath,
|
|
751
|
+
async function writeClaudeSettings(workspacePath, input8) {
|
|
737
752
|
const path = getClaudeSettingsPath(workspacePath);
|
|
738
753
|
const existing = await readExistingSettings(path);
|
|
739
754
|
let merged;
|
|
740
|
-
switch (
|
|
755
|
+
switch (input8.provider) {
|
|
741
756
|
case "subscription":
|
|
742
|
-
merged = applySubscription(existing,
|
|
757
|
+
merged = applySubscription(existing, input8.model);
|
|
743
758
|
break;
|
|
744
759
|
case "llmlite":
|
|
745
|
-
merged = applyLLMLite(existing,
|
|
760
|
+
merged = applyLLMLite(existing, input8.apiKey, input8.baseUrl, input8.model);
|
|
746
761
|
break;
|
|
747
762
|
case "anthropic":
|
|
748
|
-
merged = applyAnthropic(existing,
|
|
763
|
+
merged = applyAnthropic(existing, input8.apiKey, input8.baseUrl, input8.model);
|
|
749
764
|
break;
|
|
750
765
|
case "use-global":
|
|
751
|
-
merged = applyUseGlobal(existing,
|
|
766
|
+
merged = applyUseGlobal(existing, input8.sourceSettings);
|
|
752
767
|
break;
|
|
753
768
|
}
|
|
754
769
|
await writeJsonAtomic(path, merged, SECRET_FILE_MODE2);
|
|
@@ -803,7 +818,10 @@ async function runAiSetupPhase(args) {
|
|
|
803
818
|
`provider=subscription,result=no-quota,reason=${reason}`
|
|
804
819
|
);
|
|
805
820
|
log.warn(`Subscription verify th\u1EA5t b\u1EA1i (${reason}).`);
|
|
806
|
-
|
|
821
|
+
if (quota.detail?.trim()) {
|
|
822
|
+
log.warn(` Chi ti\u1EBFt: ${quota.detail.slice(0, 200)}`);
|
|
823
|
+
}
|
|
824
|
+
log.warn(` \u2192 ${getQuotaErrorHint(reason)}`);
|
|
807
825
|
return { ok: false, reason: `subscription-${reason}`, phase: "quota" };
|
|
808
826
|
}
|
|
809
827
|
await writeClaudeSettings(args.workspacePath, {
|
|
@@ -1765,7 +1783,7 @@ function probeGitnexusVersion() {
|
|
|
1765
1783
|
if (result.error || result.status !== 0) return null;
|
|
1766
1784
|
const out = (result.stdout || "").trim();
|
|
1767
1785
|
const match = SEMVER_REGEX2.exec(out);
|
|
1768
|
-
return match
|
|
1786
|
+
return match?.[1] ?? null;
|
|
1769
1787
|
}
|
|
1770
1788
|
function detectGitnexusInstallation() {
|
|
1771
1789
|
const path = probeGitnexusBinaryPath();
|
|
@@ -2311,32 +2329,113 @@ function registerGitnexusCommand(program2) {
|
|
|
2311
2329
|
}
|
|
2312
2330
|
|
|
2313
2331
|
// src/commands/init.ts
|
|
2314
|
-
import {
|
|
2315
|
-
import { confirm as confirm5, input as input5, select as select9 } from "@inquirer/prompts";
|
|
2316
|
-
import boxen6 from "boxen";
|
|
2332
|
+
import { select as select12 } from "@inquirer/prompts";
|
|
2317
2333
|
|
|
2318
|
-
// src/lib/
|
|
2319
|
-
import
|
|
2320
|
-
|
|
2334
|
+
// src/lib/avatar-ascii-banner.ts
|
|
2335
|
+
import chalk2 from "chalk";
|
|
2336
|
+
var BANNER_LINES = [
|
|
2337
|
+
" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
2338
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
2339
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
|
|
2340
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
2341
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
2342
|
+
"\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
|
|
2343
|
+
];
|
|
2344
|
+
var GRADIENT_STOPS = [
|
|
2345
|
+
[217, 79, 30],
|
|
2346
|
+
// cam-cháy (#d94f1e)
|
|
2347
|
+
[200, 70, 80],
|
|
2348
|
+
// cam-hồng
|
|
2349
|
+
[170, 70, 140],
|
|
2350
|
+
// hồng-tím
|
|
2351
|
+
[125, 88, 217]
|
|
2352
|
+
// tím (#7d58d9)
|
|
2353
|
+
];
|
|
2354
|
+
function lerpChannel(a, b, t) {
|
|
2355
|
+
return Math.round(a + (b - a) * t);
|
|
2356
|
+
}
|
|
2357
|
+
function gradientAt(t) {
|
|
2358
|
+
const clamped = Math.max(0, Math.min(1, t));
|
|
2359
|
+
const scaled = clamped * (GRADIENT_STOPS.length - 1);
|
|
2360
|
+
const lo = Math.floor(scaled);
|
|
2361
|
+
const hi = Math.min(GRADIENT_STOPS.length - 1, lo + 1);
|
|
2362
|
+
const localT = scaled - lo;
|
|
2363
|
+
const a = GRADIENT_STOPS[lo];
|
|
2364
|
+
const b = GRADIENT_STOPS[hi];
|
|
2365
|
+
return [
|
|
2366
|
+
lerpChannel(a[0], b[0], localT),
|
|
2367
|
+
lerpChannel(a[1], b[1], localT),
|
|
2368
|
+
lerpChannel(a[2], b[2], localT)
|
|
2369
|
+
];
|
|
2370
|
+
}
|
|
2371
|
+
function renderAvatarBanner(opts) {
|
|
2372
|
+
const isTty = process.stdout.isTTY ?? false;
|
|
2373
|
+
const supportsColor = isTty && chalk2.level > 0;
|
|
2374
|
+
if (!supportsColor) {
|
|
2375
|
+
return [...BANNER_LINES, ...opts?.tagline ? ["", opts.tagline] : []].join("\n");
|
|
2376
|
+
}
|
|
2377
|
+
const colored = BANNER_LINES.map((line, idx) => {
|
|
2378
|
+
const t = BANNER_LINES.length === 1 ? 0 : idx / (BANNER_LINES.length - 1);
|
|
2379
|
+
const [r, g, b] = gradientAt(t);
|
|
2380
|
+
return chalk2.rgb(r, g, b).bold(line);
|
|
2381
|
+
});
|
|
2382
|
+
if (opts?.tagline) {
|
|
2383
|
+
colored.push("");
|
|
2384
|
+
colored.push(chalk2.dim(opts.tagline));
|
|
2385
|
+
}
|
|
2386
|
+
return colored.join("\n");
|
|
2387
|
+
}
|
|
2388
|
+
function printAvatarBanner(opts) {
|
|
2389
|
+
process.stdout.write(`
|
|
2390
|
+
${renderAvatarBanner(opts)}
|
|
2321
2391
|
|
|
2322
|
-
|
|
2323
|
-
|
|
2392
|
+
`);
|
|
2393
|
+
}
|
|
2324
2394
|
|
|
2325
|
-
// src/lib/
|
|
2395
|
+
// src/lib/handle-remote-access-failure-with-account-switch.ts
|
|
2396
|
+
import { spawnSync as spawnSync13 } from "child_process";
|
|
2397
|
+
import { input as input4, select as select5 } from "@inquirer/prompts";
|
|
2398
|
+
|
|
2399
|
+
// src/lib/verify-git-remote-accessible.ts
|
|
2326
2400
|
import { spawnSync as spawnSync12 } from "child_process";
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2401
|
+
var TIMEOUT_MS = 5e3;
|
|
2402
|
+
function classifyRemoteError(stderr) {
|
|
2403
|
+
const text = stderr.toLowerCase();
|
|
2404
|
+
if (text.includes("authentication") || text.includes("could not read username") || text.includes("permission denied") || text.includes("403") || text.includes("access denied") || text.includes("repository not found")) {
|
|
2405
|
+
return "no-access";
|
|
2406
|
+
}
|
|
2407
|
+
if (text.includes("404") || text.includes("does not exist")) {
|
|
2408
|
+
return "not-found";
|
|
2409
|
+
}
|
|
2410
|
+
if (text.includes("could not resolve host") || text.includes("network") || text.includes("connection refused") || text.includes("connection timed out")) {
|
|
2411
|
+
return "network";
|
|
2412
|
+
}
|
|
2413
|
+
return "unknown";
|
|
2333
2414
|
}
|
|
2334
|
-
function
|
|
2335
|
-
const r = spawnSync12("
|
|
2336
|
-
|
|
2415
|
+
function tryVerifyGitRemoteAccessible(url) {
|
|
2416
|
+
const r = spawnSync12("git", ["ls-remote", "--exit-code", url, "HEAD"], {
|
|
2417
|
+
encoding: "utf8",
|
|
2418
|
+
timeout: TIMEOUT_MS,
|
|
2419
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2420
|
+
});
|
|
2421
|
+
if (r.status === 0) return { ok: true };
|
|
2422
|
+
if (r.signal === "SIGTERM") {
|
|
2423
|
+
return { ok: false, reason: "timeout", detail: "git ls-remote > 5s" };
|
|
2424
|
+
}
|
|
2425
|
+
const stderr = (r.stderr || "").trim();
|
|
2426
|
+
const reason = classifyRemoteError(stderr);
|
|
2427
|
+
return { ok: false, reason, detail: stderr.slice(0, 300) };
|
|
2337
2428
|
}
|
|
2429
|
+
|
|
2430
|
+
// src/lib/handle-remote-access-failure-with-account-switch.ts
|
|
2431
|
+
var RemoteAccessAbortedError = class extends Error {
|
|
2432
|
+
constructor(message) {
|
|
2433
|
+
super(message);
|
|
2434
|
+
this.name = "RemoteAccessAbortedError";
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2338
2437
|
function getCurrentGhUser() {
|
|
2339
|
-
const r =
|
|
2438
|
+
const r = spawnSync13("gh", ["api", "user", "--jq", ".login"], {
|
|
2340
2439
|
encoding: "utf8",
|
|
2341
2440
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2342
2441
|
});
|
|
@@ -2345,666 +2444,749 @@ function getCurrentGhUser() {
|
|
|
2345
2444
|
}
|
|
2346
2445
|
function triggerGhAuthLoginInteractive() {
|
|
2347
2446
|
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
2348
|
-
const r =
|
|
2447
|
+
const r = spawnSync13("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
2349
2448
|
if (r.status !== 0) {
|
|
2350
|
-
log.warn(`gh auth login exit ${r.status}.
|
|
2449
|
+
log.warn(`gh auth login exit ${r.status}. B\u1EA1n c\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
2351
2450
|
}
|
|
2352
2451
|
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2452
|
+
function getReasonHint(reason, url, ghUser) {
|
|
2453
|
+
switch (reason) {
|
|
2454
|
+
case "no-access":
|
|
2455
|
+
return ghUser ? `Repo c\xF3 th\u1EC3 private v\xE0 account '${ghUser}' kh\xF4ng c\xF3 quy\u1EC1n access. Switch sang account c\xF3 quy\u1EC1n, ho\u1EB7c xin invite t\u1EEB owner repo.` : "Repo c\xF3 th\u1EC3 private v\xE0 gh CLI ch\u01B0a login. Login tr\u01B0\u1EDBc r\u1ED3i retry.";
|
|
2456
|
+
case "not-found":
|
|
2457
|
+
return `URL c\xF3 th\u1EC3 sai ch\xEDnh t\u1EA3, ho\u1EB7c repo \u0111\xE3 b\u1ECB x\xF3a/rename. Nh\u1EADp l\u1EA1i URL \u0111\xFAng. Check: ${url}`;
|
|
2458
|
+
case "network":
|
|
2459
|
+
return "Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c GitHub. Check m\u1EA1ng / VPN / firewall.";
|
|
2460
|
+
case "timeout":
|
|
2461
|
+
return "Network ch\u1EADm > 5s. Check m\u1EA1ng r\u1ED3i retry.";
|
|
2462
|
+
default:
|
|
2463
|
+
return "L\u1ED7i kh\xF4ng x\xE1c \u0111\u1ECBnh. URL c\xF3 th\u1EC3 sai \u2014 nh\u1EADp l\u1EA1i, ho\u1EB7c check gh auth status.";
|
|
2365
2464
|
}
|
|
2366
2465
|
}
|
|
2367
|
-
function
|
|
2368
|
-
const
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
`Repo: ${chalk.bold(repoSlug)}`,
|
|
2372
|
-
"",
|
|
2373
|
-
"B\u1EA1n c\u1EA7n \u0111\u01B0\u1EE3c admin add v\xE0o org \u0111\u1EC3 pull team-ai-pack.",
|
|
2374
|
-
"",
|
|
2375
|
-
`${chalk.dim("Th\xF4ng tin g\u1EEDi admin:")}`,
|
|
2376
|
-
` GitHub username: ${chalk.cyan(ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)")}`,
|
|
2377
|
-
` NAL email: ${chalk.cyan(ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)")}`,
|
|
2378
|
-
` Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
2379
|
-
"",
|
|
2380
|
-
`${chalk.dim("Li\xEAn h\u1EC7:")} luke@nal.vn (Slack #avatar-setup)`
|
|
2381
|
-
];
|
|
2382
|
-
process.stdout.write(
|
|
2383
|
-
`${boxen3(lines.join("\n"), { padding: 1, borderColor: "red", borderStyle: "round" })}
|
|
2384
|
-
`
|
|
2385
|
-
);
|
|
2386
|
-
}
|
|
2387
|
-
function buildAccessRequestInfo(repoSlug, ghUser, ssoEmail) {
|
|
2388
|
-
return [
|
|
2389
|
-
`Request access ${repoSlug} (NAL)`,
|
|
2390
|
-
"",
|
|
2391
|
-
`GitHub username: ${ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)"}`,
|
|
2392
|
-
`NAL email: ${ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)"}`,
|
|
2393
|
-
`Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
|
|
2394
|
-
].join("\n");
|
|
2466
|
+
function isValidGitUrl(url) {
|
|
2467
|
+
const trimmed = url.trim();
|
|
2468
|
+
if (!trimmed) return false;
|
|
2469
|
+
return /^https?:\/\/[\w.@/-]+$/.test(trimmed) || /^git@[\w.-]+:[\w./-]+\.git$/.test(trimmed) || /^[\w.-]+\/[\w.-]+$/.test(trimmed);
|
|
2395
2470
|
}
|
|
2396
|
-
async function
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
await copyInfoToClipboardWithConsent(
|
|
2401
|
-
buildAccessRequestInfo(args.repoSlug, initialGhUser, args.ssoEmail ?? null)
|
|
2402
|
-
);
|
|
2471
|
+
async function handleRemoteAccessFailureWithAccountSwitch(args) {
|
|
2472
|
+
let currentUrl = args.url;
|
|
2473
|
+
let reason = args.initialReason;
|
|
2474
|
+
let detail = args.initialDetail;
|
|
2403
2475
|
while (true) {
|
|
2404
2476
|
const ghUser = getCurrentGhUser();
|
|
2405
|
-
|
|
2477
|
+
log.warn(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c ${currentUrl}`);
|
|
2478
|
+
log.dim(` L\xFD do: ${reason}${detail ? ` \u2014 ${detail.slice(0, 150)}` : ""}`);
|
|
2479
|
+
log.info(getReasonHint(reason, currentUrl, ghUser));
|
|
2480
|
+
if (ghUser) log.dim(` gh CLI hi\u1EC7n \u0111ang login: ${ghUser}`);
|
|
2406
2481
|
const action = await select5({
|
|
2407
2482
|
message: "C\xE1ch x\u1EED l\xFD?",
|
|
2408
2483
|
choices: [
|
|
2409
2484
|
{
|
|
2410
|
-
name:
|
|
2411
|
-
value: "
|
|
2485
|
+
name: "Nh\u1EADp l\u1EA1i URL \u0111\xFAng (recommended khi URL sai ch\xEDnh t\u1EA3)",
|
|
2486
|
+
value: "re-input-url"
|
|
2412
2487
|
},
|
|
2413
2488
|
{
|
|
2414
|
-
name: "
|
|
2415
|
-
value: "switch
|
|
2489
|
+
name: "Switch GitHub account (gh auth login \u2014 m\u1EDF browser)",
|
|
2490
|
+
value: "switch"
|
|
2416
2491
|
},
|
|
2417
2492
|
{
|
|
2418
|
-
name: "T\
|
|
2493
|
+
name: "T\xF4i v\u1EEBa fix (accept invite / s\u1EEDa permission) \u2014 retry verify",
|
|
2494
|
+
value: "retry"
|
|
2495
|
+
},
|
|
2496
|
+
{
|
|
2497
|
+
name: "T\u1EA1m ng\u01B0ng init \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau",
|
|
2419
2498
|
value: "abort"
|
|
2420
2499
|
}
|
|
2421
2500
|
]
|
|
2422
2501
|
});
|
|
2423
2502
|
if (action === "abort") {
|
|
2424
|
-
|
|
2425
|
-
|
|
2503
|
+
throw new RemoteAccessAbortedError(
|
|
2504
|
+
`User ch\u1ECDn t\u1EA1m ng\u01B0ng. Fix access ${currentUrl} r\u1ED3i ch\u1EA1y l\u1EA1i 'avatar init'.`
|
|
2505
|
+
);
|
|
2426
2506
|
}
|
|
2427
|
-
if (action === "
|
|
2507
|
+
if (action === "re-input-url") {
|
|
2508
|
+
const newUrl = await input4({
|
|
2509
|
+
message: "URL git remote (https://github.com/owner/repo.git ho\u1EB7c git@github.com:owner/repo.git):",
|
|
2510
|
+
default: currentUrl,
|
|
2511
|
+
validate: (v) => isValidGitUrl(v) || "URL kh\xF4ng \u0111\xFAng format git remote"
|
|
2512
|
+
});
|
|
2513
|
+
currentUrl = newUrl.trim();
|
|
2514
|
+
}
|
|
2515
|
+
if (action === "switch") {
|
|
2428
2516
|
triggerGhAuthLoginInteractive();
|
|
2429
2517
|
}
|
|
2430
|
-
log.info(
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
log.success(
|
|
2434
|
-
return
|
|
2518
|
+
log.info(`Verify remote l\u1EA1i: ${currentUrl}...`);
|
|
2519
|
+
const result = tryVerifyGitRemoteAccessible(currentUrl);
|
|
2520
|
+
if (result.ok) {
|
|
2521
|
+
log.success(`Remote accessible: ${currentUrl}`);
|
|
2522
|
+
return { resolvedUrl: currentUrl };
|
|
2435
2523
|
}
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
);
|
|
2524
|
+
reason = result.reason ?? "unknown";
|
|
2525
|
+
detail = result.detail;
|
|
2439
2526
|
}
|
|
2440
2527
|
}
|
|
2441
2528
|
|
|
2442
|
-
// src/lib/
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
}
|
|
2456
|
-
function pickLatestStableSemVerTag(tags, includePrerelease = false) {
|
|
2457
|
-
const parsed = tags.map(parseSemVerTag).filter((t) => t !== null).filter((t) => includePrerelease || t.prerelease === null);
|
|
2458
|
-
if (parsed.length === 0) return null;
|
|
2459
|
-
parsed.sort((a, b) => {
|
|
2460
|
-
if (a.major !== b.major) return a.major - b.major;
|
|
2461
|
-
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
2462
|
-
return a.patch - b.patch;
|
|
2463
|
-
});
|
|
2464
|
-
return parsed[parsed.length - 1]?.raw ?? null;
|
|
2529
|
+
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
2530
|
+
import { readdirSync } from "fs";
|
|
2531
|
+
import { select as select6 } from "@inquirer/prompts";
|
|
2532
|
+
import { simpleGit as simpleGit3 } from "simple-git";
|
|
2533
|
+
|
|
2534
|
+
// src/lib/check-folder-has-git.ts
|
|
2535
|
+
import { existsSync as existsSync6, statSync } from "fs";
|
|
2536
|
+
import { join as join16 } from "path";
|
|
2537
|
+
function checkFolderHasGit(folderPath) {
|
|
2538
|
+
const gitPath = join16(folderPath, ".git");
|
|
2539
|
+
if (!existsSync6(gitPath)) return false;
|
|
2540
|
+
const stat = statSync(gitPath);
|
|
2541
|
+
return stat.isDirectory() || stat.isFile();
|
|
2465
2542
|
}
|
|
2466
2543
|
|
|
2467
|
-
// src/lib/
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2544
|
+
// src/lib/create-initial-git-commit.ts
|
|
2545
|
+
import { simpleGit as simpleGit2 } from "simple-git";
|
|
2546
|
+
var INITIAL_COMMIT_MESSAGE = "chore: initial commit";
|
|
2547
|
+
async function createInitialGitCommit(folderPath) {
|
|
2548
|
+
const g = simpleGit2({ baseDir: folderPath });
|
|
2549
|
+
const isRepo = await g.checkIsRepo().catch(() => false);
|
|
2550
|
+
if (!isRepo) {
|
|
2551
|
+
await g.init();
|
|
2552
|
+
}
|
|
2553
|
+
try {
|
|
2554
|
+
await g.branch(["-M", "main"]);
|
|
2555
|
+
} catch {
|
|
2556
|
+
}
|
|
2557
|
+
await g.add(".");
|
|
2558
|
+
const status = await g.status();
|
|
2559
|
+
const hasCommits = (await g.raw(["rev-list", "-n", "1", "--all"]).catch(() => "")).trim();
|
|
2560
|
+
if (hasCommits) return;
|
|
2561
|
+
if (status.files.length === 0) {
|
|
2562
|
+
await g.commit(INITIAL_COMMIT_MESSAGE, void 0, { "--allow-empty": null });
|
|
2563
|
+
} else {
|
|
2564
|
+
await g.commit(INITIAL_COMMIT_MESSAGE);
|
|
2472
2565
|
}
|
|
2473
|
-
return ORG_DEFAULT;
|
|
2474
2566
|
}
|
|
2475
2567
|
|
|
2476
|
-
// src/lib/
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
var
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2568
|
+
// src/lib/detect-folder-tech-stack.ts
|
|
2569
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2570
|
+
import { join as join17 } from "path";
|
|
2571
|
+
var SIGNATURES = {
|
|
2572
|
+
node: ["package.json"],
|
|
2573
|
+
python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
|
|
2574
|
+
go: ["go.mod"],
|
|
2575
|
+
rust: ["Cargo.toml"],
|
|
2576
|
+
java: ["pom.xml", "build.gradle", "build.gradle.kts"],
|
|
2577
|
+
ruby: ["Gemfile"]
|
|
2484
2578
|
};
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
const
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
const hasAccess = await ensureTeamPackAccessWithRetry({ repoSlug, ssoEmail });
|
|
2491
|
-
if (!hasAccess) {
|
|
2492
|
-
throw new TeamPackAccessAbortedError(
|
|
2493
|
-
"User ch\u1ECDn t\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\u01B0\u1EE3c add v\xE0o org."
|
|
2494
|
-
);
|
|
2579
|
+
function detectFolderTechStack(folderPath) {
|
|
2580
|
+
const matched = [];
|
|
2581
|
+
for (const [stack, files] of Object.entries(SIGNATURES)) {
|
|
2582
|
+
if (files.some((f) => existsSync7(join17(folderPath, f)))) {
|
|
2583
|
+
matched.push(stack);
|
|
2495
2584
|
}
|
|
2496
2585
|
}
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2586
|
+
return matched.length > 0 ? matched : ["generic"];
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// src/lib/gitignore-template-loader.ts
|
|
2590
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2591
|
+
import { dirname as dirname4, join as join18 } from "path";
|
|
2592
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2593
|
+
var __dirname = dirname4(fileURLToPath2(import.meta.url));
|
|
2594
|
+
var CANDIDATE_DIRS = [
|
|
2595
|
+
// Bundled production: dist/index.js → __dirname = .../dist/, sibling dist/templates
|
|
2596
|
+
join18(__dirname, "templates", "gitignore"),
|
|
2597
|
+
// Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
|
|
2598
|
+
join18(__dirname, "..", "templates", "gitignore"),
|
|
2599
|
+
// Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
|
|
2600
|
+
join18(__dirname, "..", "..", "src", "templates", "gitignore"),
|
|
2601
|
+
// npm-installed alt: __dirname = .../dist/ → package_root/src/templates
|
|
2602
|
+
join18(__dirname, "..", "src", "templates", "gitignore")
|
|
2603
|
+
];
|
|
2604
|
+
var AVATAR_MARKER_START = "# === avatar ===";
|
|
2605
|
+
var AVATAR_MARKER_END = "# === /avatar ===";
|
|
2606
|
+
function readTemplate(stack) {
|
|
2607
|
+
for (const dir of CANDIDATE_DIRS) {
|
|
2608
|
+
try {
|
|
2609
|
+
return readFileSync3(join18(dir, `${stack}.txt`), "utf8");
|
|
2610
|
+
} catch {
|
|
2509
2611
|
}
|
|
2510
|
-
throw err;
|
|
2511
|
-
}
|
|
2512
|
-
if (tag) {
|
|
2513
|
-
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, tag, projectRoot);
|
|
2514
|
-
return { pinnedTag: tag };
|
|
2515
|
-
}
|
|
2516
|
-
if (latest) {
|
|
2517
|
-
await checkoutBranchHeadInSubmodule(TEAM_PACK_RELATIVE_PATH, DEFAULT_PACK_BRANCH, projectRoot);
|
|
2518
|
-
return { pinnedTag: `${DEFAULT_PACK_BRANCH} (HEAD)` };
|
|
2519
|
-
}
|
|
2520
|
-
const submoduleDir = join16(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
2521
|
-
const allTags = await listTags(submoduleDir);
|
|
2522
|
-
const target = pickLatestStableSemVerTag(allTags);
|
|
2523
|
-
if (target) {
|
|
2524
|
-
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
|
|
2525
2612
|
}
|
|
2526
|
-
|
|
2613
|
+
throw new Error(`Kh\xF4ng t\xECm th\u1EA5y template gitignore cho stack "${stack}"`);
|
|
2527
2614
|
}
|
|
2528
|
-
|
|
2529
|
-
const
|
|
2530
|
-
const
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
return sha.slice(0, 7);
|
|
2615
|
+
function composeGitignoreContent(stacks) {
|
|
2616
|
+
const all = ["generic", ...stacks.filter((s) => s !== "generic")];
|
|
2617
|
+
const sections = all.map((s) => `# --- ${s} ---
|
|
2618
|
+
${readTemplate(s).trim()}`);
|
|
2619
|
+
return [AVATAR_MARKER_START, ...sections, AVATAR_MARKER_END, ""].join("\n");
|
|
2534
2620
|
}
|
|
2535
2621
|
|
|
2536
|
-
// src/lib/
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2622
|
+
// src/lib/write-or-merge-gitignore.ts
|
|
2623
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
2624
|
+
import { join as join19 } from "path";
|
|
2625
|
+
function writeOrMergeGitignore(folderPath, avatarBlock) {
|
|
2626
|
+
const path = join19(folderPath, ".gitignore");
|
|
2627
|
+
if (!existsSync8(path)) {
|
|
2628
|
+
writeFileSync(path, avatarBlock, "utf8");
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
const existing = readFileSync4(path, "utf8");
|
|
2632
|
+
const startIdx = existing.indexOf(AVATAR_MARKER_START);
|
|
2633
|
+
const endIdx = existing.indexOf(AVATAR_MARKER_END);
|
|
2634
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
2635
|
+
const before = existing.slice(0, startIdx);
|
|
2636
|
+
const after = existing.slice(endIdx + AVATAR_MARKER_END.length);
|
|
2637
|
+
writeFileSync(path, `${before.trimEnd()}
|
|
2638
|
+
|
|
2639
|
+
${avatarBlock}${after.trimStart()}`, "utf8");
|
|
2640
|
+
return;
|
|
2546
2641
|
}
|
|
2642
|
+
writeFileSync(path, `${existing.trimEnd()}
|
|
2643
|
+
|
|
2644
|
+
${avatarBlock}`, "utf8");
|
|
2547
2645
|
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2646
|
+
|
|
2647
|
+
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
2648
|
+
var InitAbortedByUserError = class extends Error {
|
|
2649
|
+
constructor(message) {
|
|
2650
|
+
super(message);
|
|
2651
|
+
this.name = "InitAbortedByUserError";
|
|
2652
|
+
}
|
|
2653
|
+
};
|
|
2654
|
+
async function detectFolderGitState(folderPath) {
|
|
2655
|
+
const hasGit = checkFolderHasGit(folderPath);
|
|
2656
|
+
if (!hasGit) {
|
|
2657
|
+
const entries = readdirSync(folderPath).filter((e) => e !== ".git");
|
|
2658
|
+
return entries.length === 0 ? "empty" : "untracked-only";
|
|
2553
2659
|
}
|
|
2660
|
+
const g = simpleGit3({ baseDir: folderPath });
|
|
2661
|
+
const status = await g.status();
|
|
2662
|
+
return status.isClean() ? "clean" : "dirty";
|
|
2554
2663
|
}
|
|
2555
|
-
async function
|
|
2664
|
+
async function promptBootstrapStrategy(state, opts) {
|
|
2665
|
+
if (opts.presetStrategy) return opts.presetStrategy;
|
|
2666
|
+
if (opts.autoYes) return "stash";
|
|
2667
|
+
if (state === "empty" || state === "clean") return "commit-all";
|
|
2556
2668
|
return await select6({
|
|
2557
|
-
message: "
|
|
2669
|
+
message: state === "dirty" ? "Folder c\xF3 changes ch\u01B0a commit. C\xE1ch x\u1EED l\xFD:" : "Folder c\xF3 file ch\u01B0a version. C\xE1ch x\u1EED l\xFD:",
|
|
2558
2670
|
choices: [
|
|
2559
2671
|
{
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
},
|
|
2563
|
-
{
|
|
2564
|
-
name: "D\xF9ng HTTPS thay SSH (override URL b\u1EB1ng env AVATAR_TEAM_PACK_REPO_URL)",
|
|
2565
|
-
value: "https"
|
|
2672
|
+
value: "stash",
|
|
2673
|
+
name: "1. Stash changes \u2192 bootstrap \u2192 restore (KHUY\u1EBEN NGH\u1ECA)"
|
|
2566
2674
|
},
|
|
2567
2675
|
{
|
|
2568
|
-
|
|
2569
|
-
|
|
2676
|
+
value: "commit-all",
|
|
2677
|
+
name: "2. Commit to\xE0n b\u1ED9 v\xE0o initial commit (legacy v1.1.6)"
|
|
2570
2678
|
},
|
|
2571
2679
|
{
|
|
2572
|
-
|
|
2573
|
-
|
|
2680
|
+
value: "skip",
|
|
2681
|
+
name: "3. Skip \u2014 t\xF4i commit th\u1EE7 c\xF4ng r\u1ED3i ch\u1EA1y l\u1EA1i"
|
|
2574
2682
|
},
|
|
2575
2683
|
{
|
|
2576
|
-
|
|
2577
|
-
|
|
2684
|
+
value: "branch",
|
|
2685
|
+
name: "4. Commit v\xE0o branch ri\xEAng `avatar/init` (main gi\u1EEF s\u1EA1ch)"
|
|
2578
2686
|
}
|
|
2579
|
-
]
|
|
2687
|
+
],
|
|
2688
|
+
default: "stash"
|
|
2580
2689
|
});
|
|
2581
2690
|
}
|
|
2582
|
-
async function
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
);
|
|
2600
|
-
}
|
|
2601
|
-
if (action2 === "skip") {
|
|
2602
|
-
log.warn(
|
|
2603
|
-
"Skip team-ai-pack. Workspace d\xF9ng \u0111\u01B0\u1EE3c nh\u01B0ng kh\xF4ng c\xF3 shared knowledge. Pull sau qua `avatar sync`."
|
|
2604
|
-
);
|
|
2605
|
-
return { pinnedTag: null, skipped: true };
|
|
2606
|
-
}
|
|
2607
|
-
if (action2 === "switch") {
|
|
2608
|
-
triggerGhAuthLoginInteractive2();
|
|
2609
|
-
continue;
|
|
2610
|
-
}
|
|
2611
|
-
if (action2 === "https") {
|
|
2612
|
-
process.env.AVATAR_TEAM_PACK_REPO_URL = "https://github.com/nalvn/team-ai-pack.git";
|
|
2613
|
-
log.info("Override URL sang HTTPS. L\u01B0u \xFD: HTTPS c\xF3 th\u1EC3 fail 403 n\u1EBFu read-only role.");
|
|
2614
|
-
openGithubSshKeysPage();
|
|
2615
|
-
continue;
|
|
2616
|
-
}
|
|
2617
|
-
continue;
|
|
2618
|
-
}
|
|
2619
|
-
const action = await promptRetryOrSkip({
|
|
2620
|
-
taskName: "Pull team-ai-pack submodule",
|
|
2621
|
-
reason: message,
|
|
2622
|
-
allowSkip: true,
|
|
2623
|
-
hint: "Network glitch? Retry th\u01B0\u1EDDng work. N\u1EBFu skip, d\xF9ng `avatar sync` sau \u0111\u1EC3 pull pack."
|
|
2624
|
-
});
|
|
2625
|
-
if (action === "abort") {
|
|
2626
|
-
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc pull team-ai-pack.");
|
|
2627
|
-
}
|
|
2628
|
-
if (action === "skip") {
|
|
2629
|
-
log.warn(
|
|
2630
|
-
"Skip team-ai-pack. Workspace d\xF9ng \u0111\u01B0\u1EE3c nh\u01B0ng kh\xF4ng c\xF3 shared knowledge. Pull sau qua `avatar sync`."
|
|
2631
|
-
);
|
|
2632
|
-
return { pinnedTag: null, skipped: true };
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2691
|
+
async function stashUserChanges(g, stashName) {
|
|
2692
|
+
const status = await g.status();
|
|
2693
|
+
if (status.isClean() && status.not_added.length === 0) return false;
|
|
2694
|
+
await g.stash(["push", "--include-untracked", "-m", stashName]);
|
|
2695
|
+
log.info(`Stashed changes: ${stashName}`);
|
|
2696
|
+
return true;
|
|
2697
|
+
}
|
|
2698
|
+
async function restoreStash(g, stashName) {
|
|
2699
|
+
try {
|
|
2700
|
+
await g.stash(["pop"]);
|
|
2701
|
+
log.success(`Restored stash: ${stashName}`);
|
|
2702
|
+
} catch (err) {
|
|
2703
|
+
log.warn(
|
|
2704
|
+
"Restore stash conflict \u2014 files c\xF3 Avatar t\u1EA1o v\xE0 stash c\u1EE7a user xung \u0111\u1ED9t. Stash gi\u1EEF t\u1EA1i ref stash@{0}."
|
|
2705
|
+
);
|
|
2706
|
+
log.warn("Resolve: git stash show -p stash@{0} \u2192 fix conflict \u2192 git stash drop");
|
|
2707
|
+
log.dim(`Detail: ${err.message}`);
|
|
2635
2708
|
}
|
|
2636
2709
|
}
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
"\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
2646
|
-
"\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
|
|
2647
|
-
];
|
|
2648
|
-
var GRADIENT_STOPS = [
|
|
2649
|
-
[217, 79, 30],
|
|
2650
|
-
// cam-cháy (#d94f1e)
|
|
2651
|
-
[200, 70, 80],
|
|
2652
|
-
// cam-hồng
|
|
2653
|
-
[170, 70, 140],
|
|
2654
|
-
// hồng-tím
|
|
2655
|
-
[125, 88, 217]
|
|
2656
|
-
// tím (#7d58d9)
|
|
2657
|
-
];
|
|
2658
|
-
function lerpChannel(a, b, t) {
|
|
2659
|
-
return Math.round(a + (b - a) * t);
|
|
2710
|
+
async function getCurrentBranch(g) {
|
|
2711
|
+
try {
|
|
2712
|
+
const result = await g.revparse(["--abbrev-ref", "HEAD"]);
|
|
2713
|
+
const branch = result.trim();
|
|
2714
|
+
return branch === "HEAD" ? "main" : branch;
|
|
2715
|
+
} catch {
|
|
2716
|
+
return "main";
|
|
2717
|
+
}
|
|
2660
2718
|
}
|
|
2661
|
-
function
|
|
2662
|
-
const
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
const localT = scaled - lo;
|
|
2667
|
-
const a = GRADIENT_STOPS[lo];
|
|
2668
|
-
const b = GRADIENT_STOPS[hi];
|
|
2669
|
-
return [
|
|
2670
|
-
lerpChannel(a[0], b[0], localT),
|
|
2671
|
-
lerpChannel(a[1], b[1], localT),
|
|
2672
|
-
lerpChannel(a[2], b[2], localT)
|
|
2673
|
-
];
|
|
2719
|
+
async function writeAvatarGitignore(folderPath) {
|
|
2720
|
+
const stacks = detectFolderTechStack(folderPath);
|
|
2721
|
+
log.info(`Tech stack: ${stacks.join(", ")}`);
|
|
2722
|
+
writeOrMergeGitignore(folderPath, composeGitignoreContent(stacks));
|
|
2723
|
+
log.success(".gitignore \u0111\xE3 ghi (Avatar block)");
|
|
2674
2724
|
}
|
|
2675
|
-
function
|
|
2676
|
-
const
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2725
|
+
async function executeBootstrapWithStrategy(folderPath, strategy) {
|
|
2726
|
+
const g = simpleGit3({ baseDir: folderPath });
|
|
2727
|
+
switch (strategy) {
|
|
2728
|
+
case "skip":
|
|
2729
|
+
throw new InitAbortedByUserError(
|
|
2730
|
+
"Init aborted. Commit th\u1EE7 c\xF4ng changes hi\u1EC7n t\u1EA1i r\u1ED3i ch\u1EA1y l\u1EA1i `avatar init`."
|
|
2731
|
+
);
|
|
2732
|
+
case "stash": {
|
|
2733
|
+
const stashName = `avatar-init-backup-${Date.now()}`;
|
|
2734
|
+
const hadGit = checkFolderHasGit(folderPath);
|
|
2735
|
+
if (!hadGit) {
|
|
2736
|
+
await g.init();
|
|
2737
|
+
await g.branch(["-M", "main"]).catch(() => void 0);
|
|
2738
|
+
}
|
|
2739
|
+
const hasCommit = (await g.raw(["rev-list", "-n", "1", "--all"]).catch(() => "")).trim();
|
|
2740
|
+
if (!hasCommit) {
|
|
2741
|
+
await g.commit("chore: avatar baseline (pre-stash)", void 0, { "--allow-empty": null });
|
|
2742
|
+
}
|
|
2743
|
+
const stashed = await stashUserChanges(g, stashName);
|
|
2744
|
+
try {
|
|
2745
|
+
await writeAvatarGitignore(folderPath);
|
|
2746
|
+
await createInitialGitCommit(folderPath);
|
|
2747
|
+
} finally {
|
|
2748
|
+
if (stashed) await restoreStash(g, stashName);
|
|
2749
|
+
}
|
|
2750
|
+
break;
|
|
2751
|
+
}
|
|
2752
|
+
case "commit-all": {
|
|
2753
|
+
await writeAvatarGitignore(folderPath);
|
|
2754
|
+
await createInitialGitCommit(folderPath);
|
|
2755
|
+
break;
|
|
2756
|
+
}
|
|
2757
|
+
case "branch": {
|
|
2758
|
+
const hadGit = checkFolderHasGit(folderPath);
|
|
2759
|
+
if (!hadGit) {
|
|
2760
|
+
await g.init();
|
|
2761
|
+
await g.branch(["-M", "main"]);
|
|
2762
|
+
}
|
|
2763
|
+
const originalBranch = await getCurrentBranch(g);
|
|
2764
|
+
try {
|
|
2765
|
+
await g.checkoutLocalBranch("avatar/init");
|
|
2766
|
+
} catch {
|
|
2767
|
+
await g.checkout("avatar/init");
|
|
2768
|
+
}
|
|
2769
|
+
await writeAvatarGitignore(folderPath);
|
|
2770
|
+
await createInitialGitCommit(folderPath);
|
|
2771
|
+
try {
|
|
2772
|
+
await g.checkout(originalBranch);
|
|
2773
|
+
log.info(
|
|
2774
|
+
`Avatar init committed \u1EDF branch 'avatar/init'. Switch back v\u1EC1 '${originalBranch}'. Merge khi s\u1EB5n s\xE0ng: git merge avatar/init`
|
|
2775
|
+
);
|
|
2776
|
+
} catch {
|
|
2777
|
+
log.warn(
|
|
2778
|
+
`Kh\xF4ng switch v\u1EC1 '${originalBranch}' \u0111\u01B0\u1EE3c \u2014 \u1EDF l\u1EA1i branch 'avatar/init'. Switch tay sau.`
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
break;
|
|
2782
|
+
}
|
|
2680
2783
|
}
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2784
|
+
}
|
|
2785
|
+
async function safeBootstrapGitInFolder(folderPath, opts = {}) {
|
|
2786
|
+
const state = await detectFolderGitState(folderPath);
|
|
2787
|
+
log.info(`Folder state: ${state}`);
|
|
2788
|
+
if (state === "empty" || state === "clean") {
|
|
2789
|
+
await writeAvatarGitignore(folderPath);
|
|
2790
|
+
if (state === "empty") {
|
|
2791
|
+
await createInitialGitCommit(folderPath);
|
|
2792
|
+
}
|
|
2793
|
+
await appendAuditEntry("bootstrap", `state=${state},strategy=auto`);
|
|
2794
|
+
return;
|
|
2689
2795
|
}
|
|
2690
|
-
|
|
2796
|
+
const strategy = await promptBootstrapStrategy(state, opts);
|
|
2797
|
+
await executeBootstrapWithStrategy(folderPath, strategy);
|
|
2798
|
+
await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
|
|
2691
2799
|
}
|
|
2692
|
-
function printAvatarBanner(opts) {
|
|
2693
|
-
process.stdout.write(`
|
|
2694
|
-
${renderAvatarBanner(opts)}
|
|
2695
2800
|
|
|
2696
|
-
|
|
2697
|
-
}
|
|
2801
|
+
// src/lib/team-pack-submodule-manager.ts
|
|
2802
|
+
import { join as join20 } from "path";
|
|
2698
2803
|
|
|
2699
|
-
// src/lib/
|
|
2804
|
+
// src/lib/check-team-pack-access-with-retry-loop.ts
|
|
2700
2805
|
import { spawnSync as spawnSync14 } from "child_process";
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
};
|
|
2707
|
-
function executeGhRepoCreate(input6) {
|
|
2708
|
-
const fullName = `${input6.org}/${input6.name}`;
|
|
2709
|
-
const args = [
|
|
2710
|
-
"repo",
|
|
2711
|
-
"create",
|
|
2712
|
-
fullName,
|
|
2713
|
-
`--${input6.visibility}`,
|
|
2714
|
-
"--source",
|
|
2715
|
-
input6.folder,
|
|
2716
|
-
"--remote",
|
|
2717
|
-
"origin",
|
|
2718
|
-
"--push"
|
|
2719
|
-
];
|
|
2720
|
-
const r = spawnSync14("gh", args, { stdio: "inherit" });
|
|
2721
|
-
if (r.status !== 0) {
|
|
2722
|
-
if (r.status === 1) {
|
|
2723
|
-
throw new RepoAlreadyExistsError(fullName);
|
|
2724
|
-
}
|
|
2725
|
-
throw new Error(`gh repo create th\u1EA5t b\u1EA1i (exit ${r.status})`);
|
|
2726
|
-
}
|
|
2727
|
-
return {
|
|
2728
|
-
sshUrl: `git@github.com:${fullName}.git`,
|
|
2729
|
-
httpsUrl: `https://github.com/${fullName}.git`
|
|
2730
|
-
};
|
|
2806
|
+
import { confirm as confirm4, select as select7 } from "@inquirer/prompts";
|
|
2807
|
+
import boxen3 from "boxen";
|
|
2808
|
+
function parseRepoSlugFromGitUrl(url) {
|
|
2809
|
+
const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
2810
|
+
return httpsMatch?.[1] ?? null;
|
|
2731
2811
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2812
|
+
function checkRepoAccess(repoSlug) {
|
|
2813
|
+
const r = spawnSync14("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
|
|
2814
|
+
return r.status === 0;
|
|
2815
|
+
}
|
|
2816
|
+
function getCurrentGhUser2() {
|
|
2817
|
+
const r = spawnSync14("gh", ["api", "user", "--jq", ".login"], {
|
|
2737
2818
|
encoding: "utf8",
|
|
2738
2819
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2739
2820
|
});
|
|
2740
|
-
if (r.status !== 0)
|
|
2741
|
-
|
|
2742
|
-
}
|
|
2743
|
-
return r.stdout.trim();
|
|
2744
|
-
}
|
|
2745
|
-
|
|
2746
|
-
// src/lib/validate-repo-name-and-visibility.ts
|
|
2747
|
-
var REPO_NAME_REGEX = /^[a-zA-Z0-9._-]{1,100}$/;
|
|
2748
|
-
var InvalidRepoNameError = class extends Error {
|
|
2749
|
-
constructor(name) {
|
|
2750
|
-
super(
|
|
2751
|
-
`T\xEAn repo "${name}" kh\xF4ng h\u1EE3p l\u1EC7. Ch\u1EC9 d\xF9ng ch\u1EEF/s\u1ED1/d\u1EA5u ch\u1EA5m/g\u1EA1ch/underscore, d\xE0i 1-100 k\xFD t\u1EF1.`
|
|
2752
|
-
);
|
|
2753
|
-
this.name = "InvalidRepoNameError";
|
|
2754
|
-
}
|
|
2755
|
-
};
|
|
2756
|
-
function validateRepoName(name) {
|
|
2757
|
-
if (!REPO_NAME_REGEX.test(name)) {
|
|
2758
|
-
throw new InvalidRepoNameError(name);
|
|
2759
|
-
}
|
|
2821
|
+
if (r.status !== 0) return null;
|
|
2822
|
+
return r.stdout.trim() || null;
|
|
2760
2823
|
}
|
|
2761
|
-
function
|
|
2762
|
-
|
|
2763
|
-
|
|
2824
|
+
function triggerGhAuthLoginInteractive2() {
|
|
2825
|
+
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
2826
|
+
const r = spawnSync14("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
2827
|
+
if (r.status !== 0) {
|
|
2828
|
+
log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
2764
2829
|
}
|
|
2765
2830
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
validateRepoVisibility(input6.visibility);
|
|
2771
|
-
const org = input6.org ?? resolveGithubUsernameDefault();
|
|
2772
|
-
log.info(`T\u1EA1o GitHub repo ${org}/${input6.name} (${input6.visibility})...`);
|
|
2773
|
-
const urls = executeGhRepoCreate({
|
|
2774
|
-
folder: input6.folder,
|
|
2775
|
-
org,
|
|
2776
|
-
name: input6.name,
|
|
2777
|
-
visibility: input6.visibility
|
|
2831
|
+
async function copyInfoToClipboardWithConsent(info) {
|
|
2832
|
+
const ok = await confirm4({
|
|
2833
|
+
message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
|
|
2834
|
+
default: true
|
|
2778
2835
|
});
|
|
2779
|
-
|
|
2780
|
-
|
|
2836
|
+
if (!ok) return;
|
|
2837
|
+
try {
|
|
2838
|
+
const { default: clipboardy } = await import("clipboardy");
|
|
2839
|
+
await clipboardy.write(info);
|
|
2840
|
+
log.success("\u0110\xE3 copy v\xE0o clipboard");
|
|
2841
|
+
} catch (err) {
|
|
2842
|
+
log.dim(`Copy clipboard fail: ${err.message}`);
|
|
2843
|
+
}
|
|
2781
2844
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
}
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
const args = platform2 === "win32" ? [name] : ["-v", name];
|
|
2802
|
-
const r = spawnSync17(probe, args, {
|
|
2803
|
-
shell: platform2 !== "win32",
|
|
2804
|
-
stdio: "ignore"
|
|
2805
|
-
});
|
|
2806
|
-
return r.status === 0;
|
|
2807
|
-
}
|
|
2808
|
-
function detectPackageManager() {
|
|
2809
|
-
const platform2 = detectHostPlatform();
|
|
2810
|
-
const candidates = platform2 === "darwin" ? ["brew"] : platform2 === "win32" ? ["winget"] : platform2 === "linux" ? ["apt", "dnf", "pacman"] : [];
|
|
2811
|
-
for (const pm of candidates) {
|
|
2812
|
-
if (hasBinary(pm)) return pm;
|
|
2813
|
-
}
|
|
2814
|
-
return null;
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
// src/lib/handle-remote-access-failure-with-account-switch.ts
|
|
2818
|
-
import { spawnSync as spawnSync19 } from "child_process";
|
|
2819
|
-
import { input as input4, select as select7 } from "@inquirer/prompts";
|
|
2820
|
-
|
|
2821
|
-
// src/lib/verify-git-remote-accessible.ts
|
|
2822
|
-
import { spawnSync as spawnSync18 } from "child_process";
|
|
2823
|
-
var TIMEOUT_MS = 5e3;
|
|
2824
|
-
function classifyRemoteError(stderr) {
|
|
2825
|
-
const text = stderr.toLowerCase();
|
|
2826
|
-
if (text.includes("authentication") || text.includes("could not read username") || text.includes("permission denied") || text.includes("403") || text.includes("access denied") || text.includes("repository not found")) {
|
|
2827
|
-
return "no-access";
|
|
2828
|
-
}
|
|
2829
|
-
if (text.includes("404") || text.includes("does not exist")) {
|
|
2830
|
-
return "not-found";
|
|
2831
|
-
}
|
|
2832
|
-
if (text.includes("could not resolve host") || text.includes("network") || text.includes("connection refused") || text.includes("connection timed out")) {
|
|
2833
|
-
return "network";
|
|
2834
|
-
}
|
|
2835
|
-
return "unknown";
|
|
2836
|
-
}
|
|
2837
|
-
function tryVerifyGitRemoteAccessible(url) {
|
|
2838
|
-
const r = spawnSync18("git", ["ls-remote", "--exit-code", url, "HEAD"], {
|
|
2839
|
-
encoding: "utf8",
|
|
2840
|
-
timeout: TIMEOUT_MS,
|
|
2841
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2842
|
-
});
|
|
2843
|
-
if (r.status === 0) return { ok: true };
|
|
2844
|
-
if (r.signal === "SIGTERM") {
|
|
2845
|
-
return { ok: false, reason: "timeout", detail: "git ls-remote > 5s" };
|
|
2846
|
-
}
|
|
2847
|
-
const stderr = (r.stderr || "").trim();
|
|
2848
|
-
const reason = classifyRemoteError(stderr);
|
|
2849
|
-
return { ok: false, reason, detail: stderr.slice(0, 300) };
|
|
2850
|
-
}
|
|
2851
|
-
|
|
2852
|
-
// src/lib/handle-remote-access-failure-with-account-switch.ts
|
|
2853
|
-
var RemoteAccessAbortedError = class extends Error {
|
|
2854
|
-
constructor(message) {
|
|
2855
|
-
super(message);
|
|
2856
|
-
this.name = "RemoteAccessAbortedError";
|
|
2857
|
-
}
|
|
2858
|
-
};
|
|
2859
|
-
function getCurrentGhUser2() {
|
|
2860
|
-
const r = spawnSync19("gh", ["api", "user", "--jq", ".login"], {
|
|
2861
|
-
encoding: "utf8",
|
|
2862
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2863
|
-
});
|
|
2864
|
-
if (r.status !== 0) return null;
|
|
2865
|
-
return r.stdout.trim() || null;
|
|
2866
|
-
}
|
|
2867
|
-
function triggerGhAuthLoginInteractive3() {
|
|
2868
|
-
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
2869
|
-
const r = spawnSync19("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
2870
|
-
if (r.status !== 0) {
|
|
2871
|
-
log.warn(`gh auth login exit ${r.status}. B\u1EA1n c\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
2872
|
-
}
|
|
2873
|
-
}
|
|
2874
|
-
function getReasonHint(reason, url, ghUser) {
|
|
2875
|
-
switch (reason) {
|
|
2876
|
-
case "no-access":
|
|
2877
|
-
return ghUser ? `Repo c\xF3 th\u1EC3 private v\xE0 account '${ghUser}' kh\xF4ng c\xF3 quy\u1EC1n access. Switch sang account c\xF3 quy\u1EC1n, ho\u1EB7c xin invite t\u1EEB owner repo.` : "Repo c\xF3 th\u1EC3 private v\xE0 gh CLI ch\u01B0a login. Login tr\u01B0\u1EDBc r\u1ED3i retry.";
|
|
2878
|
-
case "not-found":
|
|
2879
|
-
return `URL c\xF3 th\u1EC3 sai ch\xEDnh t\u1EA3, ho\u1EB7c repo \u0111\xE3 b\u1ECB x\xF3a/rename. Nh\u1EADp l\u1EA1i URL \u0111\xFAng. Check: ${url}`;
|
|
2880
|
-
case "network":
|
|
2881
|
-
return "Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c GitHub. Check m\u1EA1ng / VPN / firewall.";
|
|
2882
|
-
case "timeout":
|
|
2883
|
-
return "Network ch\u1EADm > 5s. Check m\u1EA1ng r\u1ED3i retry.";
|
|
2884
|
-
default:
|
|
2885
|
-
return "L\u1ED7i kh\xF4ng x\xE1c \u0111\u1ECBnh. URL c\xF3 th\u1EC3 sai \u2014 nh\u1EADp l\u1EA1i, ho\u1EB7c check gh auth status.";
|
|
2886
|
-
}
|
|
2845
|
+
function printAccessWarningBox(repoSlug, ghUser, ssoEmail) {
|
|
2846
|
+
const lines = [
|
|
2847
|
+
`${chalk.red("\u26D4 KH\xD4NG C\xD3 QUY\u1EC0N ACCESS")}`,
|
|
2848
|
+
"",
|
|
2849
|
+
`Repo: ${chalk.bold(repoSlug)}`,
|
|
2850
|
+
"",
|
|
2851
|
+
"B\u1EA1n c\u1EA7n \u0111\u01B0\u1EE3c admin add v\xE0o org \u0111\u1EC3 pull team-ai-pack.",
|
|
2852
|
+
"",
|
|
2853
|
+
`${chalk.dim("Th\xF4ng tin g\u1EEDi admin:")}`,
|
|
2854
|
+
` GitHub username: ${chalk.cyan(ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)")}`,
|
|
2855
|
+
` NAL email: ${chalk.cyan(ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)")}`,
|
|
2856
|
+
` Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
2857
|
+
"",
|
|
2858
|
+
`${chalk.dim("Li\xEAn h\u1EC7:")} luke@nal.vn (Slack #avatar-setup)`
|
|
2859
|
+
];
|
|
2860
|
+
process.stdout.write(
|
|
2861
|
+
`${boxen3(lines.join("\n"), { padding: 1, borderColor: "red", borderStyle: "round" })}
|
|
2862
|
+
`
|
|
2863
|
+
);
|
|
2887
2864
|
}
|
|
2888
|
-
function
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2865
|
+
function buildAccessRequestInfo(repoSlug, ghUser, ssoEmail) {
|
|
2866
|
+
return [
|
|
2867
|
+
`Request access ${repoSlug} (NAL)`,
|
|
2868
|
+
"",
|
|
2869
|
+
`GitHub username: ${ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)"}`,
|
|
2870
|
+
`NAL email: ${ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)"}`,
|
|
2871
|
+
`Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
|
|
2872
|
+
].join("\n");
|
|
2892
2873
|
}
|
|
2893
|
-
async function
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2874
|
+
async function ensureTeamPackAccessWithRetry(args) {
|
|
2875
|
+
if (checkRepoAccess(args.repoSlug)) return true;
|
|
2876
|
+
const initialGhUser = getCurrentGhUser2();
|
|
2877
|
+
printAccessWarningBox(args.repoSlug, initialGhUser, args.ssoEmail ?? null);
|
|
2878
|
+
await copyInfoToClipboardWithConsent(
|
|
2879
|
+
buildAccessRequestInfo(args.repoSlug, initialGhUser, args.ssoEmail ?? null)
|
|
2880
|
+
);
|
|
2897
2881
|
while (true) {
|
|
2898
2882
|
const ghUser = getCurrentGhUser2();
|
|
2899
|
-
|
|
2900
|
-
log.dim(` L\xFD do: ${reason}${detail ? ` \u2014 ${detail.slice(0, 150)}` : ""}`);
|
|
2901
|
-
log.info(getReasonHint(reason, currentUrl, ghUser));
|
|
2902
|
-
if (ghUser) log.dim(` gh CLI hi\u1EC7n \u0111ang login: ${ghUser}`);
|
|
2883
|
+
const ghUserDisplay = ghUser ?? "(ch\u01B0a gh auth)";
|
|
2903
2884
|
const action = await select7({
|
|
2904
2885
|
message: "C\xE1ch x\u1EED l\xFD?",
|
|
2905
2886
|
choices: [
|
|
2906
2887
|
{
|
|
2907
|
-
name:
|
|
2908
|
-
value: "
|
|
2909
|
-
},
|
|
2910
|
-
{
|
|
2911
|
-
name: "Switch GitHub account (gh auth login \u2014 m\u1EDF browser)",
|
|
2912
|
-
value: "switch"
|
|
2888
|
+
name: `\u0110\xE3 \u0111\u01B0\u1EE3c grant access v\u1EDBi GitHub username '${ghUserDisplay}' \u2014 ki\u1EC3m tra l\u1EA1i`,
|
|
2889
|
+
value: "retry-same"
|
|
2913
2890
|
},
|
|
2914
2891
|
{
|
|
2915
|
-
name: "
|
|
2916
|
-
value: "
|
|
2892
|
+
name: "\u0110\xE3 grant v\u1EDBi GitHub account kh\xE1c \u2014 switch gh (m\u1EDF browser)",
|
|
2893
|
+
value: "switch-account"
|
|
2917
2894
|
},
|
|
2918
2895
|
{
|
|
2919
|
-
name: "T\u1EA1m ng\u01B0ng
|
|
2896
|
+
name: "T\u1EA1m ng\u01B0ng \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau",
|
|
2920
2897
|
value: "abort"
|
|
2921
2898
|
}
|
|
2922
2899
|
]
|
|
2923
2900
|
});
|
|
2924
2901
|
if (action === "abort") {
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
);
|
|
2928
|
-
}
|
|
2929
|
-
if (action === "re-input-url") {
|
|
2930
|
-
const newUrl = await input4({
|
|
2931
|
-
message: "URL git remote (https://github.com/owner/repo.git ho\u1EB7c git@github.com:owner/repo.git):",
|
|
2932
|
-
default: currentUrl,
|
|
2933
|
-
validate: (v) => isValidGitUrl(v) || "URL kh\xF4ng \u0111\xFAng format git remote"
|
|
2934
|
-
});
|
|
2935
|
-
currentUrl = newUrl.trim();
|
|
2902
|
+
log.dim("T\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\xE3 accept invite t\u1EEB GitHub.");
|
|
2903
|
+
return false;
|
|
2936
2904
|
}
|
|
2937
|
-
if (action === "switch") {
|
|
2938
|
-
|
|
2905
|
+
if (action === "switch-account") {
|
|
2906
|
+
triggerGhAuthLoginInteractive2();
|
|
2939
2907
|
}
|
|
2940
|
-
log.info(
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
log.success(
|
|
2944
|
-
return
|
|
2908
|
+
log.info("Ki\u1EC3m tra access...");
|
|
2909
|
+
if (checkRepoAccess(args.repoSlug)) {
|
|
2910
|
+
const finalUser = getCurrentGhUser2();
|
|
2911
|
+
log.success(`\u0110\xE3 c\xF3 access v\u1EDBi '${finalUser ?? "(unknown)"}' \u2014 ti\u1EBFp t\u1EE5c.`);
|
|
2912
|
+
return true;
|
|
2945
2913
|
}
|
|
2946
|
-
|
|
2947
|
-
|
|
2914
|
+
log.warn(
|
|
2915
|
+
`V\u1EABn ch\u01B0a c\xF3 access v\u1EDBi account '${ghUser ?? "(unknown)"}'. \u0110\u1EA3m b\u1EA3o \u0111\xE3 accept email invite ho\u1EB7c switch \u0111\xFAng account.`
|
|
2916
|
+
);
|
|
2948
2917
|
}
|
|
2949
2918
|
}
|
|
2950
2919
|
|
|
2951
|
-
// src/lib/
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
if (r.status !== 0) {
|
|
2965
|
-
throw new Error(`C\xE0i gh CLI th\u1EA5t b\u1EA1i qua ${pm} (exit ${r.status}). C\xE0i tay r\u1ED3i ch\u1EA1y l\u1EA1i.`);
|
|
2966
|
-
}
|
|
2967
|
-
log.success("\u0110\xE3 c\xE0i gh CLI");
|
|
2920
|
+
// src/lib/pick-latest-stable-semver-tag.ts
|
|
2921
|
+
var SEMVER_REGEX3 = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
2922
|
+
function parseSemVerTag(tag) {
|
|
2923
|
+
const match = tag.match(SEMVER_REGEX3);
|
|
2924
|
+
if (!match) return null;
|
|
2925
|
+
const [, major, minor, patch, prerelease] = match;
|
|
2926
|
+
return {
|
|
2927
|
+
raw: tag,
|
|
2928
|
+
major: Number.parseInt(major ?? "0", 10),
|
|
2929
|
+
minor: Number.parseInt(minor ?? "0", 10),
|
|
2930
|
+
patch: Number.parseInt(patch ?? "0", 10),
|
|
2931
|
+
prerelease: prerelease ?? null
|
|
2932
|
+
};
|
|
2968
2933
|
}
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
log.dim("Git credential helper \u0111\xE3 link v\u1EDBi gh token.");
|
|
2934
|
+
function pickLatestStableSemVerTag(tags, includePrerelease = false) {
|
|
2935
|
+
const parsed = tags.map(parseSemVerTag).filter((t) => t !== null).filter((t) => includePrerelease || t.prerelease === null);
|
|
2936
|
+
if (parsed.length === 0) return null;
|
|
2937
|
+
parsed.sort((a, b) => {
|
|
2938
|
+
if (a.major !== b.major) return a.major - b.major;
|
|
2939
|
+
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
2940
|
+
return a.patch - b.patch;
|
|
2941
|
+
});
|
|
2942
|
+
return parsed[parsed.length - 1]?.raw ?? null;
|
|
2979
2943
|
}
|
|
2980
2944
|
|
|
2981
|
-
// src/lib/
|
|
2982
|
-
|
|
2983
|
-
function
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
"gh",
|
|
2987
|
-
["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
|
|
2988
|
-
{ stdio: "inherit" }
|
|
2989
|
-
);
|
|
2990
|
-
if (r.status !== 0) {
|
|
2991
|
-
throw new Error(`gh auth login th\u1EA5t b\u1EA1i (exit ${r.status}). Th\u1EED 'gh auth login' tay.`);
|
|
2945
|
+
// src/lib/resolve-team-pack-repo-url.ts
|
|
2946
|
+
var ORG_DEFAULT = "git@github.com:nalvn/team-ai-pack.git";
|
|
2947
|
+
function resolveTeamPackRepoUrl() {
|
|
2948
|
+
if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
|
|
2949
|
+
return process.env.AVATAR_TEAM_PACK_REPO_URL;
|
|
2992
2950
|
}
|
|
2993
|
-
|
|
2951
|
+
return ORG_DEFAULT;
|
|
2994
2952
|
}
|
|
2995
2953
|
|
|
2996
|
-
// src/lib/
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
2954
|
+
// src/lib/team-pack-submodule-manager.ts
|
|
2955
|
+
var TEAM_PACK_REPO_URL = resolveTeamPackRepoUrl();
|
|
2956
|
+
var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
|
|
2957
|
+
var TeamPackAccessAbortedError = class extends Error {
|
|
2958
|
+
constructor(message) {
|
|
2959
|
+
super(message);
|
|
2960
|
+
this.name = "TeamPackAccessAbortedError";
|
|
2961
|
+
}
|
|
2962
|
+
};
|
|
2963
|
+
var DEFAULT_PACK_BRANCH = "main";
|
|
2964
|
+
async function addTeamPackSubmodule(projectRoot, tag, ssoEmail, latest = false) {
|
|
2965
|
+
const url = resolveTeamPackRepoUrl();
|
|
2966
|
+
const repoSlug = parseRepoSlugFromGitUrl(url);
|
|
2967
|
+
if (repoSlug) {
|
|
2968
|
+
const hasAccess = await ensureTeamPackAccessWithRetry({ repoSlug, ssoEmail });
|
|
2969
|
+
if (!hasAccess) {
|
|
2970
|
+
throw new TeamPackAccessAbortedError(
|
|
2971
|
+
"User ch\u1ECDn t\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\u01B0\u1EE3c add v\xE0o org."
|
|
2972
|
+
);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
try {
|
|
2976
|
+
await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
|
|
2977
|
+
} catch (err) {
|
|
2978
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2979
|
+
if (msg.includes("Repository not found") || msg.includes("not found")) {
|
|
2980
|
+
log.error(
|
|
2981
|
+
`Repo team-ai-pack kh\xF4ng t\u1ED3n t\u1EA1i: ${url}
|
|
2982
|
+
C\xE1ch fix:
|
|
2983
|
+
1. T\u1EA1o repo: gh repo create <owner>/team-ai-pack --private --add-readme
|
|
2984
|
+
2. Ho\u1EB7c override URL: export AVATAR_TEAM_PACK_REPO_URL=<url-repo-c\u1EE7a-b\u1EA1n>
|
|
2985
|
+
3. Ho\u1EB7c d\xF9ng flag --skip-team-pack`
|
|
2986
|
+
);
|
|
2987
|
+
}
|
|
2988
|
+
throw err;
|
|
2989
|
+
}
|
|
2990
|
+
if (tag) {
|
|
2991
|
+
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, tag, projectRoot);
|
|
2992
|
+
return { pinnedTag: tag };
|
|
2993
|
+
}
|
|
2994
|
+
if (latest) {
|
|
2995
|
+
await checkoutBranchHeadInSubmodule(TEAM_PACK_RELATIVE_PATH, DEFAULT_PACK_BRANCH, projectRoot);
|
|
2996
|
+
return { pinnedTag: `${DEFAULT_PACK_BRANCH} (HEAD)` };
|
|
2997
|
+
}
|
|
2998
|
+
const submoduleDir = join20(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
2999
|
+
const allTags = await listTags(submoduleDir);
|
|
3000
|
+
const target = pickLatestStableSemVerTag(allTags);
|
|
3001
|
+
if (target) {
|
|
3002
|
+
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
|
|
3003
|
+
}
|
|
3004
|
+
return { pinnedTag: target };
|
|
3005
|
+
}
|
|
3006
|
+
async function readPinnedPackVersion(projectRoot) {
|
|
3007
|
+
const submoduleRoot = join20(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
3008
|
+
const tag = await tagAtHead(submoduleRoot);
|
|
3009
|
+
if (tag) return tag;
|
|
3010
|
+
const sha = await currentCommitSha(submoduleRoot);
|
|
3011
|
+
return sha.slice(0, 7);
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// src/commands/init-flow-handlers-for-each-project-status.ts
|
|
3015
|
+
import { basename as basename2, join as join26, resolve as resolve2 } from "path";
|
|
3016
|
+
import { input as input7, select as select11 } from "@inquirer/prompts";
|
|
3017
|
+
|
|
3018
|
+
// src/lib/execute-gh-repo-create.ts
|
|
3019
|
+
import { spawnSync as spawnSync15 } from "child_process";
|
|
3020
|
+
var RepoAlreadyExistsError = class extends Error {
|
|
3021
|
+
constructor(fullName) {
|
|
3022
|
+
super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
|
|
3023
|
+
this.name = "RepoAlreadyExistsError";
|
|
3024
|
+
}
|
|
3025
|
+
};
|
|
3026
|
+
function executeGhRepoCreate(input8) {
|
|
3027
|
+
const fullName = `${input8.org}/${input8.name}`;
|
|
3028
|
+
const args = [
|
|
3029
|
+
"repo",
|
|
3030
|
+
"create",
|
|
3031
|
+
fullName,
|
|
3032
|
+
`--${input8.visibility}`,
|
|
3033
|
+
"--source",
|
|
3034
|
+
input8.folder,
|
|
3035
|
+
"--remote",
|
|
3036
|
+
"origin",
|
|
3037
|
+
"--push"
|
|
3038
|
+
];
|
|
3039
|
+
const r = spawnSync15("gh", args, { stdio: "inherit" });
|
|
3040
|
+
if (r.status !== 0) {
|
|
3041
|
+
if (r.status === 1) {
|
|
3042
|
+
throw new RepoAlreadyExistsError(fullName);
|
|
3043
|
+
}
|
|
3044
|
+
throw new Error(`gh repo create th\u1EA5t b\u1EA1i (exit ${r.status})`);
|
|
3045
|
+
}
|
|
3046
|
+
return {
|
|
3047
|
+
sshUrl: `git@github.com:${fullName}.git`,
|
|
3048
|
+
httpsUrl: `https://github.com/${fullName}.git`
|
|
3049
|
+
};
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
// src/lib/resolve-github-username-default.ts
|
|
3053
|
+
import { spawnSync as spawnSync16 } from "child_process";
|
|
3054
|
+
function resolveGithubUsernameDefault() {
|
|
3055
|
+
const r = spawnSync16("gh", ["api", "user", "--jq", ".login"], {
|
|
3056
|
+
encoding: "utf8",
|
|
3057
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3058
|
+
});
|
|
3059
|
+
if (r.status !== 0) {
|
|
3060
|
+
throw new Error(`Kh\xF4ng l\u1EA5y \u0111\u01B0\u1EE3c GitHub username: ${r.stderr?.trim()}`);
|
|
3061
|
+
}
|
|
3062
|
+
return r.stdout.trim();
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
// src/lib/validate-repo-name-and-visibility.ts
|
|
3066
|
+
var REPO_NAME_REGEX = /^[a-zA-Z0-9._-]{1,100}$/;
|
|
3067
|
+
var InvalidRepoNameError = class extends Error {
|
|
3068
|
+
constructor(name) {
|
|
3069
|
+
super(
|
|
3070
|
+
`T\xEAn repo "${name}" kh\xF4ng h\u1EE3p l\u1EC7. Ch\u1EC9 d\xF9ng ch\u1EEF/s\u1ED1/d\u1EA5u ch\u1EA5m/g\u1EA1ch/underscore, d\xE0i 1-100 k\xFD t\u1EF1.`
|
|
3071
|
+
);
|
|
3072
|
+
this.name = "InvalidRepoNameError";
|
|
3073
|
+
}
|
|
3074
|
+
};
|
|
3075
|
+
function validateRepoName(name) {
|
|
3076
|
+
if (!REPO_NAME_REGEX.test(name)) {
|
|
3077
|
+
throw new InvalidRepoNameError(name);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
function validateRepoVisibility(v) {
|
|
3081
|
+
if (v !== "private" && v !== "public") {
|
|
3082
|
+
throw new Error(`Visibility ph\u1EA3i l\xE0 "private" ho\u1EB7c "public", nh\u1EADn: "${v}"`);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
// src/lib/create-github-remote-from-folder.ts
|
|
3087
|
+
function createGithubRemoteFromFolder(input8) {
|
|
3088
|
+
validateRepoName(input8.name);
|
|
3089
|
+
validateRepoVisibility(input8.visibility);
|
|
3090
|
+
const org = input8.org ?? resolveGithubUsernameDefault();
|
|
3091
|
+
log.info(`T\u1EA1o GitHub repo ${org}/${input8.name} (${input8.visibility})...`);
|
|
3092
|
+
const urls = executeGhRepoCreate({
|
|
3093
|
+
folder: input8.folder,
|
|
3094
|
+
org,
|
|
3095
|
+
name: input8.name,
|
|
3096
|
+
visibility: input8.visibility
|
|
3097
|
+
});
|
|
3098
|
+
log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
|
|
3099
|
+
return urls;
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
// src/lib/check-gh-cli-auth-status.ts
|
|
3103
|
+
import { spawnSync as spawnSync17 } from "child_process";
|
|
3104
|
+
function checkGhCliAuthStatus() {
|
|
3105
|
+
const r = spawnSync17("gh", ["auth", "status"], { stdio: "ignore" });
|
|
3106
|
+
if (r.error && r.error.code === "ENOENT") {
|
|
3107
|
+
return "not-installed";
|
|
3108
|
+
}
|
|
3109
|
+
return r.status === 0 ? "authenticated" : "not-authenticated";
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
// src/lib/detect-package-manager.ts
|
|
3113
|
+
import { spawnSync as spawnSync18 } from "child_process";
|
|
3114
|
+
function hasBinary(name) {
|
|
3115
|
+
const platform2 = detectHostPlatform();
|
|
3116
|
+
const probe = platform2 === "win32" ? "where" : "command";
|
|
3117
|
+
const args = platform2 === "win32" ? [name] : ["-v", name];
|
|
3118
|
+
const r = spawnSync18(probe, args, {
|
|
3119
|
+
shell: platform2 !== "win32",
|
|
3120
|
+
stdio: "ignore"
|
|
3121
|
+
});
|
|
3122
|
+
return r.status === 0;
|
|
3123
|
+
}
|
|
3124
|
+
function detectPackageManager() {
|
|
3125
|
+
const platform2 = detectHostPlatform();
|
|
3126
|
+
const candidates = platform2 === "darwin" ? ["brew"] : platform2 === "win32" ? ["winget"] : platform2 === "linux" ? ["apt", "dnf", "pacman"] : [];
|
|
3127
|
+
for (const pm of candidates) {
|
|
3128
|
+
if (hasBinary(pm)) return pm;
|
|
3129
|
+
}
|
|
3130
|
+
return null;
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
// src/lib/install-gh-cli-via-package-manager.ts
|
|
3134
|
+
import { spawnSync as spawnSync19 } from "child_process";
|
|
3135
|
+
var INSTALL_COMMANDS = {
|
|
3136
|
+
brew: { cmd: "brew", args: ["install", "gh"] },
|
|
3137
|
+
apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
|
|
3138
|
+
dnf: { cmd: "sudo", args: ["dnf", "install", "-y", "gh"] },
|
|
3139
|
+
pacman: { cmd: "sudo", args: ["pacman", "-S", "--noconfirm", "github-cli"] },
|
|
3140
|
+
winget: { cmd: "winget", args: ["install", "--id", "GitHub.cli", "-e", "--silent"] }
|
|
3141
|
+
};
|
|
3142
|
+
function installGhCliViaPackageManager(pm) {
|
|
3143
|
+
const spec = INSTALL_COMMANDS[pm];
|
|
3144
|
+
log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
|
|
3145
|
+
const r = spawnSync19(spec.cmd, spec.args, { stdio: "inherit" });
|
|
3146
|
+
if (r.status !== 0) {
|
|
3147
|
+
throw new Error(`C\xE0i gh CLI th\u1EA5t b\u1EA1i qua ${pm} (exit ${r.status}). C\xE0i tay r\u1ED3i ch\u1EA1y l\u1EA1i.`);
|
|
3148
|
+
}
|
|
3149
|
+
log.success("\u0110\xE3 c\xE0i gh CLI");
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// src/lib/setup-git-credential-via-gh.ts
|
|
3153
|
+
import { spawnSync as spawnSync20 } from "child_process";
|
|
3154
|
+
function setupGitCredentialViaGh() {
|
|
3155
|
+
const r = spawnSync20("gh", ["auth", "setup-git"], { stdio: "ignore" });
|
|
3156
|
+
if (r.status !== 0) {
|
|
3157
|
+
log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
log.dim("Git credential helper \u0111\xE3 link v\u1EDBi gh token.");
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
// src/lib/trigger-gh-cli-auth-login.ts
|
|
3164
|
+
import { spawnSync as spawnSync21 } from "child_process";
|
|
3165
|
+
function triggerGhCliAuthLogin() {
|
|
3166
|
+
log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
|
|
3167
|
+
const r = spawnSync21(
|
|
3168
|
+
"gh",
|
|
3169
|
+
["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
|
|
3170
|
+
{ stdio: "inherit" }
|
|
3171
|
+
);
|
|
3172
|
+
if (r.status !== 0) {
|
|
3173
|
+
throw new Error(`gh auth login th\u1EA5t b\u1EA1i (exit ${r.status}). Th\u1EED 'gh auth login' tay.`);
|
|
3174
|
+
}
|
|
3175
|
+
log.success("\u0110\xE3 \u0111\u0103ng nh\u1EADp GitHub");
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
// src/lib/git-auth-and-install-orchestrator.ts
|
|
3179
|
+
async function ensureGitHubReady(remoteUrl) {
|
|
3180
|
+
while (checkGhCliAuthStatus() === "not-installed") {
|
|
3181
|
+
log.warn("gh CLI ch\u01B0a c\xE0i. Avatar s\u1EBD t\u1EF1 c\xE0i.");
|
|
3182
|
+
const pm = detectPackageManager();
|
|
3183
|
+
if (!pm) {
|
|
3184
|
+
const action = await promptRetryOrSkip({
|
|
3185
|
+
taskName: "Ph\xE1t hi\u1EC7n package manager",
|
|
3186
|
+
reason: "Kh\xF4ng t\xECm th\u1EA5y brew/apt/dnf/pacman/winget tr\xEAn m\xE1y.",
|
|
3187
|
+
allowSkip: false,
|
|
3188
|
+
hint: "C\xE0i gh CLI tay (https://cli.github.com) r\u1ED3i ch\u1ECDn Retry."
|
|
3189
|
+
});
|
|
3008
3190
|
if (action === "abort") {
|
|
3009
3191
|
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc c\xE0i gh CLI.");
|
|
3010
3192
|
}
|
|
@@ -3070,37 +3252,238 @@ async function ensureGitHubReady(remoteUrl) {
|
|
|
3070
3252
|
return {};
|
|
3071
3253
|
}
|
|
3072
3254
|
|
|
3073
|
-
// src/lib/
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
};
|
|
3086
|
-
function classifyGhCreateError(stderr) {
|
|
3087
|
-
const text = stderr.toLowerCase();
|
|
3088
|
-
if (text.includes("name already exists") || text.includes("already exists on this account") || text.includes("repository already exists")) {
|
|
3089
|
-
return "repo-exists";
|
|
3090
|
-
}
|
|
3091
|
-
if (text.includes("403") || text.includes("permission") || text.includes("not authorized") || text.includes("forbidden")) {
|
|
3092
|
-
return "no-permission";
|
|
3093
|
-
}
|
|
3094
|
-
if (text.includes("invalid") && text.includes("name")) {
|
|
3095
|
-
return "name-invalid";
|
|
3255
|
+
// src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
|
|
3256
|
+
import { spawnSync as spawnSync22 } from "child_process";
|
|
3257
|
+
import { select as select8 } from "@inquirer/prompts";
|
|
3258
|
+
function isSshPermissionError(message) {
|
|
3259
|
+
const text = message.toLowerCase();
|
|
3260
|
+
return text.includes("permission denied (publickey)") || text.includes("publickey)") || text.includes("ssh: could not resolve") || text.includes("host key verification failed");
|
|
3261
|
+
}
|
|
3262
|
+
function triggerGhAuthLoginInteractive3() {
|
|
3263
|
+
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
3264
|
+
const r = spawnSync22("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
3265
|
+
if (r.status !== 0) {
|
|
3266
|
+
log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
3096
3267
|
}
|
|
3097
|
-
|
|
3098
|
-
|
|
3268
|
+
}
|
|
3269
|
+
function openGithubSshKeysPage() {
|
|
3270
|
+
log.info("M\u1EDF trang GitHub Settings \u2192 SSH Keys...");
|
|
3271
|
+
const r = spawnSync22("open", ["https://github.com/settings/keys"], { stdio: "ignore" });
|
|
3272
|
+
if (r.status !== 0) {
|
|
3273
|
+
log.info("URL: https://github.com/settings/keys");
|
|
3099
3274
|
}
|
|
3100
|
-
return "unknown";
|
|
3101
3275
|
}
|
|
3102
|
-
function
|
|
3103
|
-
|
|
3276
|
+
async function handleSshPermissionError() {
|
|
3277
|
+
return await select8({
|
|
3278
|
+
message: "SSH permission denied. C\xE1ch x\u1EED l\xFD?",
|
|
3279
|
+
choices: [
|
|
3280
|
+
{
|
|
3281
|
+
name: "Switch GitHub account (gh auth login \u2014 m\u1EDF browser)",
|
|
3282
|
+
value: "switch"
|
|
3283
|
+
},
|
|
3284
|
+
{
|
|
3285
|
+
name: "D\xF9ng HTTPS thay SSH (override URL b\u1EB1ng env AVATAR_TEAM_PACK_REPO_URL)",
|
|
3286
|
+
value: "https"
|
|
3287
|
+
},
|
|
3288
|
+
{
|
|
3289
|
+
name: "T\xF4i v\u1EEBa add SSH key l\xEAn GitHub \u2014 retry",
|
|
3290
|
+
value: "retry"
|
|
3291
|
+
},
|
|
3292
|
+
{
|
|
3293
|
+
name: "B\u1ECF qua team-ai-pack (d\xF9ng avatar sync sau)",
|
|
3294
|
+
value: "skip"
|
|
3295
|
+
},
|
|
3296
|
+
{
|
|
3297
|
+
name: "T\u1EA1m ng\u01B0ng init \u2014 fix SSH key tay r\u1ED3i ch\u1EA1y l\u1EA1i",
|
|
3298
|
+
value: "abort"
|
|
3299
|
+
}
|
|
3300
|
+
]
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
async function addTeamPackSubmoduleWithRetryOnNetworkFail(projectRoot, tag, ssoEmail, latest = false) {
|
|
3304
|
+
while (true) {
|
|
3305
|
+
try {
|
|
3306
|
+
const result = await addTeamPackSubmodule(projectRoot, tag, ssoEmail, latest);
|
|
3307
|
+
return { pinnedTag: result.pinnedTag, skipped: false };
|
|
3308
|
+
} catch (err) {
|
|
3309
|
+
if (err instanceof TeamPackAccessAbortedError) throw err;
|
|
3310
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3311
|
+
if (isSshPermissionError(message)) {
|
|
3312
|
+
log.warn("Pull team-ai-pack th\u1EA5t b\u1EA1i: SSH permission denied (publickey).");
|
|
3313
|
+
log.dim(
|
|
3314
|
+
" \u2192 M\xE1y n\xE0y ch\u01B0a c\xF3 SSH key \u0111\u01B0\u1EE3c register tr\xEAn GitHub, ho\u1EB7c key thu\u1ED9c account kh\xE1c."
|
|
3315
|
+
);
|
|
3316
|
+
const action2 = await handleSshPermissionError();
|
|
3317
|
+
if (action2 === "abort") {
|
|
3318
|
+
throw new UserAbortedRecoveryError(
|
|
3319
|
+
"User abort t\u1EA1i b\u01B0\u1EDBc pull team-ai-pack. Fix SSH key (https://github.com/settings/keys) r\u1ED3i ch\u1EA1y l\u1EA1i 'avatar init'."
|
|
3320
|
+
);
|
|
3321
|
+
}
|
|
3322
|
+
if (action2 === "skip") {
|
|
3323
|
+
log.warn(
|
|
3324
|
+
"Skip team-ai-pack. Workspace d\xF9ng \u0111\u01B0\u1EE3c nh\u01B0ng kh\xF4ng c\xF3 shared knowledge. Pull sau qua `avatar sync`."
|
|
3325
|
+
);
|
|
3326
|
+
return { pinnedTag: null, skipped: true };
|
|
3327
|
+
}
|
|
3328
|
+
if (action2 === "switch") {
|
|
3329
|
+
triggerGhAuthLoginInteractive3();
|
|
3330
|
+
continue;
|
|
3331
|
+
}
|
|
3332
|
+
if (action2 === "https") {
|
|
3333
|
+
process.env.AVATAR_TEAM_PACK_REPO_URL = "https://github.com/nalvn/team-ai-pack.git";
|
|
3334
|
+
log.info("Override URL sang HTTPS. L\u01B0u \xFD: HTTPS c\xF3 th\u1EC3 fail 403 n\u1EBFu read-only role.");
|
|
3335
|
+
openGithubSshKeysPage();
|
|
3336
|
+
continue;
|
|
3337
|
+
}
|
|
3338
|
+
continue;
|
|
3339
|
+
}
|
|
3340
|
+
const action = await promptRetryOrSkip({
|
|
3341
|
+
taskName: "Pull team-ai-pack submodule",
|
|
3342
|
+
reason: message,
|
|
3343
|
+
allowSkip: true,
|
|
3344
|
+
hint: "Network glitch? Retry th\u01B0\u1EDDng work. N\u1EBFu skip, d\xF9ng `avatar sync` sau \u0111\u1EC3 pull pack."
|
|
3345
|
+
});
|
|
3346
|
+
if (action === "abort") {
|
|
3347
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc pull team-ai-pack.");
|
|
3348
|
+
}
|
|
3349
|
+
if (action === "skip") {
|
|
3350
|
+
log.warn(
|
|
3351
|
+
"Skip team-ai-pack. Workspace d\xF9ng \u0111\u01B0\u1EE3c nh\u01B0ng kh\xF4ng c\xF3 shared knowledge. Pull sau qua `avatar sync`."
|
|
3352
|
+
);
|
|
3353
|
+
return { pinnedTag: null, skipped: true };
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
// src/lib/read-cli-version-from-package-json.ts
|
|
3360
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3361
|
+
import { dirname as dirname5, resolve } from "path";
|
|
3362
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3363
|
+
var cachedVersion = null;
|
|
3364
|
+
function readCliVersion() {
|
|
3365
|
+
if (cachedVersion !== null) return cachedVersion;
|
|
3366
|
+
const here = dirname5(fileURLToPath3(import.meta.url));
|
|
3367
|
+
for (let i = 0; i < 5; i++) {
|
|
3368
|
+
const candidate = resolve(here, ...Array(i).fill(".."), "package.json");
|
|
3369
|
+
try {
|
|
3370
|
+
const raw = readFileSync5(candidate, "utf8");
|
|
3371
|
+
const pkg = JSON.parse(raw);
|
|
3372
|
+
if (pkg.name === "@nalvietnam/avatar-cli" && typeof pkg.version === "string") {
|
|
3373
|
+
cachedVersion = pkg.version;
|
|
3374
|
+
return cachedVersion;
|
|
3375
|
+
}
|
|
3376
|
+
} catch {
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
cachedVersion = "unknown";
|
|
3380
|
+
return cachedVersion;
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
// src/commands/init-scaffold-variable-builders.ts
|
|
3384
|
+
function inferWorkspaceName(repoUrl) {
|
|
3385
|
+
const trimmed = repoUrl.trim().replace(/\/+$/, "");
|
|
3386
|
+
const lastSegment = trimmed.split(/[/:]/).pop() ?? "";
|
|
3387
|
+
const baseName = lastSegment.replace(/\.git$/, "");
|
|
3388
|
+
if (!baseName) return "avatar-client-workspace";
|
|
3389
|
+
const withoutPrefix = baseName.replace(/^avatar-/, "");
|
|
3390
|
+
return `avatar-${withoutPrefix}-workspace`;
|
|
3391
|
+
}
|
|
3392
|
+
function buildGitnexusSection(gitnexusReady) {
|
|
3393
|
+
if (!gitnexusReady) return "";
|
|
3394
|
+
return `
|
|
3395
|
+
### \u{1F9E0} CODEBASE INTELLIGENCE \u2014 GitNexus
|
|
3396
|
+
|
|
3397
|
+
Workspace c\xF3 GitNexus index t\u1EA1i \`.gitnexus/\` cung c\u1EA5p architectural awareness
|
|
3398
|
+
cho Claude Code (impact analysis, call chains, blast radius).
|
|
3399
|
+
|
|
3400
|
+
**H\u01B0\u1EDBng d\u1EABn cho Claude:**
|
|
3401
|
+
|
|
3402
|
+
- Khi c\u1EA7n hi\u1EC3u codebase architecture (vd: tr\u01B0\u1EDBc refactor, debug call chain) \u2192
|
|
3403
|
+
query GitNexus MCP resource \`gitnexus://repo/<name>/context\`
|
|
3404
|
+
- Khi resource tr\u1EA3 staleness hint "\u26A0\uFE0F Index is N commit(s) behind HEAD" \u2192
|
|
3405
|
+
T\u1EF0 ch\u1EA1y \`gitnexus analyze .\` (trong workspace root) tr\u01B0\u1EDBc khi ti\u1EBFp t\u1EE5c task
|
|
3406
|
+
- Sau khi \`gitnexus analyze\` xong: **KH\xD4NG t\u1EF1 ch\u1EA1y \`gitnexus wiki\`**
|
|
3407
|
+
(cost cao, c\xF3 th\u1EC3 block session 2-5 ph\xFAt) \u2014 user s\u1EBD ch\u1EA1y manual khi c\u1EA7n
|
|
3408
|
+
|
|
3409
|
+
**Folders Claude scan auto cho skills:**
|
|
3410
|
+
|
|
3411
|
+
- \`~/.claude/skills/gitnexus-*/\` \u2014 GitNexus global skills (exploring, debugging, ...)
|
|
3412
|
+
- \`.claude/pack/skills/\` \u2014 NAL team-shared skills (qua team-ai-pack submodule)
|
|
3413
|
+
- C\u1EA3 2 \u0111\u1EC1u \u0111\u01B0\u1EE3c scan, kh\xF4ng xung \u0111\u1ED9t (different naming prefix)
|
|
3414
|
+
|
|
3415
|
+
**Manual wiki update:**
|
|
3416
|
+
|
|
3417
|
+
Khi user c\u1EA7n regenerate wiki sau refactor l\u1EDBn \u2014 ch\u1EA1y:
|
|
3418
|
+
|
|
3419
|
+
\`\`\`bash
|
|
3420
|
+
gitnexus wiki . --api-key <key> --base-url <url>
|
|
3421
|
+
\`\`\`
|
|
3422
|
+
`;
|
|
3423
|
+
}
|
|
3424
|
+
function buildScaffoldVariables(args) {
|
|
3425
|
+
return {
|
|
3426
|
+
projectName: args.projectName,
|
|
3427
|
+
projectDescription: args.projectDescription,
|
|
3428
|
+
teamOwner: args.teamOwner,
|
|
3429
|
+
avatarVersion: readCliVersion(),
|
|
3430
|
+
packVersion: args.packVersion,
|
|
3431
|
+
lastScan: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3432
|
+
mode: args.mode,
|
|
3433
|
+
gitnexusSection: buildGitnexusSection(args.gitnexusReady ?? false)
|
|
3434
|
+
};
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
// src/commands/init-options-and-bootstrap-strategy-parser.ts
|
|
3438
|
+
function parseBootstrapStrategyOpts(opts) {
|
|
3439
|
+
if (opts.preserveUncommitted) return "stash";
|
|
3440
|
+
if (!opts.bootstrapStrategy) return void 0;
|
|
3441
|
+
const valid = ["stash", "commit-all", "skip", "branch"];
|
|
3442
|
+
if (valid.includes(opts.bootstrapStrategy)) {
|
|
3443
|
+
return opts.bootstrapStrategy;
|
|
3444
|
+
}
|
|
3445
|
+
throw new Error(
|
|
3446
|
+
`--bootstrap-strategy kh\xF4ng h\u1EE3p l\u1EC7: ${opts.bootstrapStrategy}. Ch\u1ECDn: ${valid.join(" | ")}`
|
|
3447
|
+
);
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
// src/commands/workspace-scaffold-and-finalize-orchestrator.ts
|
|
3451
|
+
import { join as join25 } from "path";
|
|
3452
|
+
import { basename } from "path";
|
|
3453
|
+
import { confirm as confirm5, input as input6, select as select10 } from "@inquirer/prompts";
|
|
3454
|
+
|
|
3455
|
+
// src/lib/create-workspace-remote-via-gh.ts
|
|
3456
|
+
import { spawnSync as spawnSync23 } from "child_process";
|
|
3457
|
+
var CreateWorkspaceRemoteError = class extends Error {
|
|
3458
|
+
reason;
|
|
3459
|
+
fullName;
|
|
3460
|
+
stderr;
|
|
3461
|
+
constructor(reason, fullName, message, stderr) {
|
|
3462
|
+
super(message);
|
|
3463
|
+
this.name = "CreateWorkspaceRemoteError";
|
|
3464
|
+
this.reason = reason;
|
|
3465
|
+
this.fullName = fullName;
|
|
3466
|
+
this.stderr = stderr;
|
|
3467
|
+
}
|
|
3468
|
+
};
|
|
3469
|
+
function classifyGhCreateError(stderr) {
|
|
3470
|
+
const text = stderr.toLowerCase();
|
|
3471
|
+
if (text.includes("name already exists") || text.includes("already exists on this account") || text.includes("repository already exists")) {
|
|
3472
|
+
return "repo-exists";
|
|
3473
|
+
}
|
|
3474
|
+
if (text.includes("403") || text.includes("permission") || text.includes("not authorized") || text.includes("forbidden")) {
|
|
3475
|
+
return "no-permission";
|
|
3476
|
+
}
|
|
3477
|
+
if (text.includes("invalid") && text.includes("name")) {
|
|
3478
|
+
return "name-invalid";
|
|
3479
|
+
}
|
|
3480
|
+
if (text.includes("could not resolve") || text.includes("network") || text.includes("connection refused")) {
|
|
3481
|
+
return "network";
|
|
3482
|
+
}
|
|
3483
|
+
return "unknown";
|
|
3484
|
+
}
|
|
3485
|
+
function repoExistsOnGitHub(fullName) {
|
|
3486
|
+
const r = spawnSync23("gh", ["repo", "view", fullName, "--json", "name"], {
|
|
3104
3487
|
stdio: "ignore"
|
|
3105
3488
|
});
|
|
3106
3489
|
return r.status === 0;
|
|
@@ -3130,17 +3513,17 @@ function canCreateInNamespace(org, ghUser) {
|
|
|
3130
3513
|
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.`
|
|
3131
3514
|
};
|
|
3132
3515
|
}
|
|
3133
|
-
async function createWorkspaceRemoteViaGh(
|
|
3134
|
-
validateRepoName(
|
|
3135
|
-
validateRepoVisibility(
|
|
3516
|
+
async function createWorkspaceRemoteViaGh(input8) {
|
|
3517
|
+
validateRepoName(input8.workspaceName);
|
|
3518
|
+
validateRepoVisibility(input8.visibility);
|
|
3136
3519
|
await ensureGitHubReady();
|
|
3137
3520
|
const ghUser = resolveGithubUsernameDefault();
|
|
3138
|
-
const org =
|
|
3521
|
+
const org = input8.org ?? ghUser;
|
|
3139
3522
|
const namespaceCheck = canCreateInNamespace(org, ghUser);
|
|
3140
3523
|
if (!namespaceCheck.ok) {
|
|
3141
3524
|
throw new Error(`Kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi '${org}/': ${namespaceCheck.reason}`);
|
|
3142
3525
|
}
|
|
3143
|
-
const fullName = `${org}/${
|
|
3526
|
+
const fullName = `${org}/${input8.workspaceName}`;
|
|
3144
3527
|
if (repoExistsOnGitHub(fullName)) {
|
|
3145
3528
|
throw new CreateWorkspaceRemoteError(
|
|
3146
3529
|
"repo-exists",
|
|
@@ -3148,16 +3531,16 @@ async function createWorkspaceRemoteViaGh(input6) {
|
|
|
3148
3531
|
`Repo '${fullName}' \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. C\xF3 th\u1EC3 b\u1EA1n \u0111\xE3 init workspace n\xE0y tr\u01B0\u1EDBc \u0111\xF3.`
|
|
3149
3532
|
);
|
|
3150
3533
|
}
|
|
3151
|
-
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${
|
|
3534
|
+
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input8.visibility})...`);
|
|
3152
3535
|
const r = spawnSync23(
|
|
3153
3536
|
"gh",
|
|
3154
3537
|
[
|
|
3155
3538
|
"repo",
|
|
3156
3539
|
"create",
|
|
3157
3540
|
fullName,
|
|
3158
|
-
`--${
|
|
3541
|
+
`--${input8.visibility}`,
|
|
3159
3542
|
"--source",
|
|
3160
|
-
|
|
3543
|
+
input8.workspacePath,
|
|
3161
3544
|
"--remote",
|
|
3162
3545
|
"origin",
|
|
3163
3546
|
"--push"
|
|
@@ -3207,45 +3590,9 @@ function linkExistingRemoteToWorkspace(args) {
|
|
|
3207
3590
|
return { sshUrl, httpsUrl };
|
|
3208
3591
|
}
|
|
3209
3592
|
|
|
3210
|
-
// src/lib/format-pack-commands-cheatsheet-box.ts
|
|
3211
|
-
import boxen4 from "boxen";
|
|
3212
|
-
var PACK_COMMAND_CHEATSHEET = [
|
|
3213
|
-
{ cmd: "/avatar:help", desc: "H\u01B0\u1EDBng d\u1EABn s\u1EED d\u1EE5ng Avatar \u2014 ch\u1EC9 c\u1EA7n g\xF5 t\u1EF1 nhi\xEAn" },
|
|
3214
|
-
{ cmd: "/avatar:brainstorm", desc: "Brainstorm \xFD t\u01B0\u1EDFng cho m\u1ED9t feature" },
|
|
3215
|
-
{ cmd: "/avatar:plan", desc: "T\u1EA1o plan th\xF4ng minh v\u1EDBi prompt enhancement" },
|
|
3216
|
-
{ cmd: "/avatar:scout", desc: "T\xECm file li\xEAn quan trong codebase, ti\u1EBFt ki\u1EC7m token" },
|
|
3217
|
-
{ cmd: "/avatar:implement", desc: "B\u1EAFt \u0111\u1EA7u code & test theo plan c\xF3 s\u1EB5n" },
|
|
3218
|
-
{ cmd: "/avatar:build-full-flow", desc: "Implement m\u1ED9t feature t\u1EEBng b\u01B0\u1EDBc (end-to-end)" },
|
|
3219
|
-
{ cmd: "/avatar:fix", desc: "Ph\xE2n t\xEDch v\xE0 fix v\u1EA5n \u0111\u1EC1 t\u1EF1 \u0111i\u1EC1u h\u01B0\u1EDBng" },
|
|
3220
|
-
{ cmd: "/avatar:debug", desc: "Debug v\u1EA5n \u0111\u1EC1 k\u1EF9 thu\u1EADt + \u0111\u01B0a ra gi\u1EA3i ph\xE1p" },
|
|
3221
|
-
{ cmd: "/avatar:test", desc: "Ch\u1EA1y test tr\xEAn m\xE1y + ph\xE2n t\xEDch b\xE1o c\xE1o t\u1ED5ng h\u1EE3p" },
|
|
3222
|
-
{ cmd: "/avatar:design:good", desc: "T\u1EA1o thi\u1EBFt k\u1EBF ch\u1EC9n chu, s\u1ED1ng \u0111\u1ED9ng" },
|
|
3223
|
-
{ cmd: "/avatar:docs:init", desc: "Ph\xE2n t\xEDch codebase + t\u1EA1o t\xE0i li\u1EC7u kh\u1EDFi \u0111\u1EA7u" },
|
|
3224
|
-
{ cmd: "/avatar:status", desc: "Xem l\u1EA1i thay \u0111\u1ED5i g\u1EA7n \u0111\xE2y + t\u1ED5ng k\u1EBFt c\xF4ng vi\u1EC7c" },
|
|
3225
|
-
{ cmd: "/avatar:journal", desc: "Ghi nh\u1EADt k\xFD session" }
|
|
3226
|
-
];
|
|
3227
|
-
function formatPackCommandsCheatsheetBox() {
|
|
3228
|
-
const maxCmdWidth = Math.max(...PACK_COMMAND_CHEATSHEET.map((e) => e.cmd.length));
|
|
3229
|
-
const header = chalk.bold("\u{1F3AF} Slash commands t\u1EEB team-ai-pack");
|
|
3230
|
-
const subheader = chalk.dim("G\xF5 trong Claude Code session \u0111\u1EC3 g\u1ECDi capability c\u1EE7a pack:");
|
|
3231
|
-
const lines = PACK_COMMAND_CHEATSHEET.map((e) => {
|
|
3232
|
-
const cmdPadded = chalk.cyan(e.cmd.padEnd(maxCmdWidth));
|
|
3233
|
-
return ` ${cmdPadded} ${chalk.dim(e.desc)}`;
|
|
3234
|
-
});
|
|
3235
|
-
const footer = chalk.dim(
|
|
3236
|
-
"Catalog \u0111\u1EA7y \u0111\u1EE7 46 commands: cat .claude/pack/scripts/commands_data.yaml"
|
|
3237
|
-
);
|
|
3238
|
-
const content = [header, subheader, "", ...lines, "", footer].join("\n");
|
|
3239
|
-
return boxen4(content, {
|
|
3240
|
-
padding: 1,
|
|
3241
|
-
borderStyle: "round",
|
|
3242
|
-
borderColor: "cyan"
|
|
3243
|
-
});
|
|
3244
|
-
}
|
|
3245
|
-
|
|
3246
3593
|
// src/lib/merge-pack-settings-into-project-settings.ts
|
|
3247
3594
|
import { promises as fs9 } from "fs";
|
|
3248
|
-
import { join as
|
|
3595
|
+
import { join as join21 } from "path";
|
|
3249
3596
|
async function isStatusLineCommandResolvable(workspacePath, command) {
|
|
3250
3597
|
const trimmed = command.trim();
|
|
3251
3598
|
const match = trimmed.match(/^(node|python|python3|bash|sh)\s+([^\s]+)/);
|
|
@@ -3253,7 +3600,7 @@ async function isStatusLineCommandResolvable(workspacePath, command) {
|
|
|
3253
3600
|
return true;
|
|
3254
3601
|
}
|
|
3255
3602
|
const filePath = match[2];
|
|
3256
|
-
const fullPath = filePath.startsWith("/") ? filePath :
|
|
3603
|
+
const fullPath = filePath.startsWith("/") ? filePath : join21(workspacePath, filePath);
|
|
3257
3604
|
return await pathExists(fullPath);
|
|
3258
3605
|
}
|
|
3259
3606
|
function backupFilename(originalPath) {
|
|
@@ -3287,8 +3634,8 @@ function mergeHooksPerEvent(packHooks, userHooks) {
|
|
|
3287
3634
|
return { merged, touchedEvents: touched };
|
|
3288
3635
|
}
|
|
3289
3636
|
async function mergePackSettingsIntoProjectSettings(workspacePath) {
|
|
3290
|
-
const packTemplatePath =
|
|
3291
|
-
const projectSettingsPath =
|
|
3637
|
+
const packTemplatePath = join21(workspacePath, ".claude", "pack", "templates", "settings.json.tpl");
|
|
3638
|
+
const projectSettingsPath = join21(workspacePath, ".claude", "settings.json");
|
|
3292
3639
|
if (!await pathExists(packTemplatePath)) {
|
|
3293
3640
|
return { action: "no-pack-template", changes: [] };
|
|
3294
3641
|
}
|
|
@@ -3388,281 +3735,9 @@ async function mergePackSettingsIntoProjectSettings(workspacePath) {
|
|
|
3388
3735
|
return { action: "merged", backupPath, changes };
|
|
3389
3736
|
}
|
|
3390
3737
|
|
|
3391
|
-
// src/lib/
|
|
3392
|
-
import {
|
|
3393
|
-
import {
|
|
3394
|
-
import { simpleGit as simpleGit3 } from "simple-git";
|
|
3395
|
-
|
|
3396
|
-
// src/lib/check-folder-has-git.ts
|
|
3397
|
-
import { existsSync as existsSync6, statSync } from "fs";
|
|
3398
|
-
import { join as join18 } from "path";
|
|
3399
|
-
function checkFolderHasGit(folderPath) {
|
|
3400
|
-
const gitPath = join18(folderPath, ".git");
|
|
3401
|
-
if (!existsSync6(gitPath)) return false;
|
|
3402
|
-
const stat = statSync(gitPath);
|
|
3403
|
-
return stat.isDirectory() || stat.isFile();
|
|
3404
|
-
}
|
|
3405
|
-
|
|
3406
|
-
// src/lib/create-initial-git-commit.ts
|
|
3407
|
-
import { simpleGit as simpleGit2 } from "simple-git";
|
|
3408
|
-
var INITIAL_COMMIT_MESSAGE = "chore: initial commit";
|
|
3409
|
-
async function createInitialGitCommit(folderPath) {
|
|
3410
|
-
const g = simpleGit2({ baseDir: folderPath });
|
|
3411
|
-
const isRepo = await g.checkIsRepo().catch(() => false);
|
|
3412
|
-
if (!isRepo) {
|
|
3413
|
-
await g.init();
|
|
3414
|
-
}
|
|
3415
|
-
try {
|
|
3416
|
-
await g.branch(["-M", "main"]);
|
|
3417
|
-
} catch {
|
|
3418
|
-
}
|
|
3419
|
-
await g.add(".");
|
|
3420
|
-
const status = await g.status();
|
|
3421
|
-
const hasCommits = (await g.raw(["rev-list", "-n", "1", "--all"]).catch(() => "")).trim();
|
|
3422
|
-
if (hasCommits) return;
|
|
3423
|
-
if (status.files.length === 0) {
|
|
3424
|
-
await g.commit(INITIAL_COMMIT_MESSAGE, void 0, { "--allow-empty": null });
|
|
3425
|
-
} else {
|
|
3426
|
-
await g.commit(INITIAL_COMMIT_MESSAGE);
|
|
3427
|
-
}
|
|
3428
|
-
}
|
|
3429
|
-
|
|
3430
|
-
// src/lib/detect-folder-tech-stack.ts
|
|
3431
|
-
import { existsSync as existsSync7 } from "fs";
|
|
3432
|
-
import { join as join19 } from "path";
|
|
3433
|
-
var SIGNATURES = {
|
|
3434
|
-
node: ["package.json"],
|
|
3435
|
-
python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
|
|
3436
|
-
go: ["go.mod"],
|
|
3437
|
-
rust: ["Cargo.toml"],
|
|
3438
|
-
java: ["pom.xml", "build.gradle", "build.gradle.kts"],
|
|
3439
|
-
ruby: ["Gemfile"]
|
|
3440
|
-
};
|
|
3441
|
-
function detectFolderTechStack(folderPath) {
|
|
3442
|
-
const matched = [];
|
|
3443
|
-
for (const [stack, files] of Object.entries(SIGNATURES)) {
|
|
3444
|
-
if (files.some((f) => existsSync7(join19(folderPath, f)))) {
|
|
3445
|
-
matched.push(stack);
|
|
3446
|
-
}
|
|
3447
|
-
}
|
|
3448
|
-
return matched.length > 0 ? matched : ["generic"];
|
|
3449
|
-
}
|
|
3450
|
-
|
|
3451
|
-
// src/lib/gitignore-template-loader.ts
|
|
3452
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
3453
|
-
import { dirname as dirname4, join as join20 } from "path";
|
|
3454
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3455
|
-
var __dirname = dirname4(fileURLToPath2(import.meta.url));
|
|
3456
|
-
var CANDIDATE_DIRS = [
|
|
3457
|
-
// Bundled production: dist/index.js → __dirname = .../dist/, sibling dist/templates
|
|
3458
|
-
join20(__dirname, "templates", "gitignore"),
|
|
3459
|
-
// Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
|
|
3460
|
-
join20(__dirname, "..", "templates", "gitignore"),
|
|
3461
|
-
// Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
|
|
3462
|
-
join20(__dirname, "..", "..", "src", "templates", "gitignore"),
|
|
3463
|
-
// npm-installed alt: __dirname = .../dist/ → package_root/src/templates
|
|
3464
|
-
join20(__dirname, "..", "src", "templates", "gitignore")
|
|
3465
|
-
];
|
|
3466
|
-
var AVATAR_MARKER_START = "# === avatar ===";
|
|
3467
|
-
var AVATAR_MARKER_END = "# === /avatar ===";
|
|
3468
|
-
function readTemplate(stack) {
|
|
3469
|
-
for (const dir of CANDIDATE_DIRS) {
|
|
3470
|
-
try {
|
|
3471
|
-
return readFileSync3(join20(dir, `${stack}.txt`), "utf8");
|
|
3472
|
-
} catch {
|
|
3473
|
-
}
|
|
3474
|
-
}
|
|
3475
|
-
throw new Error(`Kh\xF4ng t\xECm th\u1EA5y template gitignore cho stack "${stack}"`);
|
|
3476
|
-
}
|
|
3477
|
-
function composeGitignoreContent(stacks) {
|
|
3478
|
-
const all = ["generic", ...stacks.filter((s) => s !== "generic")];
|
|
3479
|
-
const sections = all.map((s) => `# --- ${s} ---
|
|
3480
|
-
${readTemplate(s).trim()}`);
|
|
3481
|
-
return [AVATAR_MARKER_START, ...sections, AVATAR_MARKER_END, ""].join("\n");
|
|
3482
|
-
}
|
|
3483
|
-
|
|
3484
|
-
// src/lib/write-or-merge-gitignore.ts
|
|
3485
|
-
import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
3486
|
-
import { join as join21 } from "path";
|
|
3487
|
-
function writeOrMergeGitignore(folderPath, avatarBlock) {
|
|
3488
|
-
const path = join21(folderPath, ".gitignore");
|
|
3489
|
-
if (!existsSync8(path)) {
|
|
3490
|
-
writeFileSync(path, avatarBlock, "utf8");
|
|
3491
|
-
return;
|
|
3492
|
-
}
|
|
3493
|
-
const existing = readFileSync4(path, "utf8");
|
|
3494
|
-
const startIdx = existing.indexOf(AVATAR_MARKER_START);
|
|
3495
|
-
const endIdx = existing.indexOf(AVATAR_MARKER_END);
|
|
3496
|
-
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
3497
|
-
const before = existing.slice(0, startIdx);
|
|
3498
|
-
const after = existing.slice(endIdx + AVATAR_MARKER_END.length);
|
|
3499
|
-
writeFileSync(path, `${before.trimEnd()}
|
|
3500
|
-
|
|
3501
|
-
${avatarBlock}${after.trimStart()}`, "utf8");
|
|
3502
|
-
return;
|
|
3503
|
-
}
|
|
3504
|
-
writeFileSync(path, `${existing.trimEnd()}
|
|
3505
|
-
|
|
3506
|
-
${avatarBlock}`, "utf8");
|
|
3507
|
-
}
|
|
3508
|
-
|
|
3509
|
-
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
3510
|
-
var InitAbortedByUserError = class extends Error {
|
|
3511
|
-
constructor(message) {
|
|
3512
|
-
super(message);
|
|
3513
|
-
this.name = "InitAbortedByUserError";
|
|
3514
|
-
}
|
|
3515
|
-
};
|
|
3516
|
-
async function detectFolderGitState(folderPath) {
|
|
3517
|
-
const hasGit = checkFolderHasGit(folderPath);
|
|
3518
|
-
if (!hasGit) {
|
|
3519
|
-
const entries = readdirSync(folderPath).filter((e) => e !== ".git");
|
|
3520
|
-
return entries.length === 0 ? "empty" : "untracked-only";
|
|
3521
|
-
}
|
|
3522
|
-
const g = simpleGit3({ baseDir: folderPath });
|
|
3523
|
-
const status = await g.status();
|
|
3524
|
-
return status.isClean() ? "clean" : "dirty";
|
|
3525
|
-
}
|
|
3526
|
-
async function promptBootstrapStrategy(state, opts) {
|
|
3527
|
-
if (opts.presetStrategy) return opts.presetStrategy;
|
|
3528
|
-
if (opts.autoYes) return "stash";
|
|
3529
|
-
if (state === "empty" || state === "clean") return "commit-all";
|
|
3530
|
-
return await select8({
|
|
3531
|
-
message: state === "dirty" ? "Folder c\xF3 changes ch\u01B0a commit. C\xE1ch x\u1EED l\xFD:" : "Folder c\xF3 file ch\u01B0a version. C\xE1ch x\u1EED l\xFD:",
|
|
3532
|
-
choices: [
|
|
3533
|
-
{
|
|
3534
|
-
value: "stash",
|
|
3535
|
-
name: "1. Stash changes \u2192 bootstrap \u2192 restore (KHUY\u1EBEN NGH\u1ECA)"
|
|
3536
|
-
},
|
|
3537
|
-
{
|
|
3538
|
-
value: "commit-all",
|
|
3539
|
-
name: "2. Commit to\xE0n b\u1ED9 v\xE0o initial commit (legacy v1.1.6)"
|
|
3540
|
-
},
|
|
3541
|
-
{
|
|
3542
|
-
value: "skip",
|
|
3543
|
-
name: "3. Skip \u2014 t\xF4i commit th\u1EE7 c\xF4ng r\u1ED3i ch\u1EA1y l\u1EA1i"
|
|
3544
|
-
},
|
|
3545
|
-
{
|
|
3546
|
-
value: "branch",
|
|
3547
|
-
name: "4. Commit v\xE0o branch ri\xEAng `avatar/init` (main gi\u1EEF s\u1EA1ch)"
|
|
3548
|
-
}
|
|
3549
|
-
],
|
|
3550
|
-
default: "stash"
|
|
3551
|
-
});
|
|
3552
|
-
}
|
|
3553
|
-
async function stashUserChanges(g, stashName) {
|
|
3554
|
-
const status = await g.status();
|
|
3555
|
-
if (status.isClean() && status.not_added.length === 0) return false;
|
|
3556
|
-
await g.stash(["push", "--include-untracked", "-m", stashName]);
|
|
3557
|
-
log.info(`Stashed changes: ${stashName}`);
|
|
3558
|
-
return true;
|
|
3559
|
-
}
|
|
3560
|
-
async function restoreStash(g, stashName) {
|
|
3561
|
-
try {
|
|
3562
|
-
await g.stash(["pop"]);
|
|
3563
|
-
log.success(`Restored stash: ${stashName}`);
|
|
3564
|
-
} catch (err) {
|
|
3565
|
-
log.warn(
|
|
3566
|
-
"Restore stash conflict \u2014 files c\xF3 Avatar t\u1EA1o v\xE0 stash c\u1EE7a user xung \u0111\u1ED9t. Stash gi\u1EEF t\u1EA1i ref stash@{0}."
|
|
3567
|
-
);
|
|
3568
|
-
log.warn("Resolve: git stash show -p stash@{0} \u2192 fix conflict \u2192 git stash drop");
|
|
3569
|
-
log.dim(`Detail: ${err.message}`);
|
|
3570
|
-
}
|
|
3571
|
-
}
|
|
3572
|
-
async function getCurrentBranch(g) {
|
|
3573
|
-
try {
|
|
3574
|
-
const result = await g.revparse(["--abbrev-ref", "HEAD"]);
|
|
3575
|
-
const branch = result.trim();
|
|
3576
|
-
return branch === "HEAD" ? "main" : branch;
|
|
3577
|
-
} catch {
|
|
3578
|
-
return "main";
|
|
3579
|
-
}
|
|
3580
|
-
}
|
|
3581
|
-
async function writeAvatarGitignore(folderPath) {
|
|
3582
|
-
const stacks = detectFolderTechStack(folderPath);
|
|
3583
|
-
log.info(`Tech stack: ${stacks.join(", ")}`);
|
|
3584
|
-
writeOrMergeGitignore(folderPath, composeGitignoreContent(stacks));
|
|
3585
|
-
log.success(".gitignore \u0111\xE3 ghi (Avatar block)");
|
|
3586
|
-
}
|
|
3587
|
-
async function executeBootstrapWithStrategy(folderPath, strategy) {
|
|
3588
|
-
const g = simpleGit3({ baseDir: folderPath });
|
|
3589
|
-
switch (strategy) {
|
|
3590
|
-
case "skip":
|
|
3591
|
-
throw new InitAbortedByUserError(
|
|
3592
|
-
"Init aborted. Commit th\u1EE7 c\xF4ng changes hi\u1EC7n t\u1EA1i r\u1ED3i ch\u1EA1y l\u1EA1i `avatar init`."
|
|
3593
|
-
);
|
|
3594
|
-
case "stash": {
|
|
3595
|
-
const stashName = `avatar-init-backup-${Date.now()}`;
|
|
3596
|
-
const hadGit = checkFolderHasGit(folderPath);
|
|
3597
|
-
if (!hadGit) {
|
|
3598
|
-
await g.init();
|
|
3599
|
-
await g.branch(["-M", "main"]).catch(() => void 0);
|
|
3600
|
-
}
|
|
3601
|
-
const hasCommit = (await g.raw(["rev-list", "-n", "1", "--all"]).catch(() => "")).trim();
|
|
3602
|
-
if (!hasCommit) {
|
|
3603
|
-
await g.commit("chore: avatar baseline (pre-stash)", void 0, { "--allow-empty": null });
|
|
3604
|
-
}
|
|
3605
|
-
const stashed = await stashUserChanges(g, stashName);
|
|
3606
|
-
try {
|
|
3607
|
-
await writeAvatarGitignore(folderPath);
|
|
3608
|
-
await createInitialGitCommit(folderPath);
|
|
3609
|
-
} finally {
|
|
3610
|
-
if (stashed) await restoreStash(g, stashName);
|
|
3611
|
-
}
|
|
3612
|
-
break;
|
|
3613
|
-
}
|
|
3614
|
-
case "commit-all": {
|
|
3615
|
-
await writeAvatarGitignore(folderPath);
|
|
3616
|
-
await createInitialGitCommit(folderPath);
|
|
3617
|
-
break;
|
|
3618
|
-
}
|
|
3619
|
-
case "branch": {
|
|
3620
|
-
const hadGit = checkFolderHasGit(folderPath);
|
|
3621
|
-
if (!hadGit) {
|
|
3622
|
-
await g.init();
|
|
3623
|
-
await g.branch(["-M", "main"]);
|
|
3624
|
-
}
|
|
3625
|
-
const originalBranch = await getCurrentBranch(g);
|
|
3626
|
-
try {
|
|
3627
|
-
await g.checkoutLocalBranch("avatar/init");
|
|
3628
|
-
} catch {
|
|
3629
|
-
await g.checkout("avatar/init");
|
|
3630
|
-
}
|
|
3631
|
-
await writeAvatarGitignore(folderPath);
|
|
3632
|
-
await createInitialGitCommit(folderPath);
|
|
3633
|
-
try {
|
|
3634
|
-
await g.checkout(originalBranch);
|
|
3635
|
-
log.info(
|
|
3636
|
-
`Avatar init committed \u1EDF branch 'avatar/init'. Switch back v\u1EC1 '${originalBranch}'. Merge khi s\u1EB5n s\xE0ng: git merge avatar/init`
|
|
3637
|
-
);
|
|
3638
|
-
} catch {
|
|
3639
|
-
log.warn(
|
|
3640
|
-
`Kh\xF4ng switch v\u1EC1 '${originalBranch}' \u0111\u01B0\u1EE3c \u2014 \u1EDF l\u1EA1i branch 'avatar/init'. Switch tay sau.`
|
|
3641
|
-
);
|
|
3642
|
-
}
|
|
3643
|
-
break;
|
|
3644
|
-
}
|
|
3645
|
-
}
|
|
3646
|
-
}
|
|
3647
|
-
async function safeBootstrapGitInFolder(folderPath, opts = {}) {
|
|
3648
|
-
const state = await detectFolderGitState(folderPath);
|
|
3649
|
-
log.info(`Folder state: ${state}`);
|
|
3650
|
-
if (state === "empty" || state === "clean") {
|
|
3651
|
-
await writeAvatarGitignore(folderPath);
|
|
3652
|
-
if (state === "empty") {
|
|
3653
|
-
await createInitialGitCommit(folderPath);
|
|
3654
|
-
}
|
|
3655
|
-
await appendAuditEntry("bootstrap", `state=${state},strategy=auto`);
|
|
3656
|
-
return;
|
|
3657
|
-
}
|
|
3658
|
-
const strategy = await promptBootstrapStrategy(state, opts);
|
|
3659
|
-
await executeBootstrapWithStrategy(folderPath, strategy);
|
|
3660
|
-
await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
|
|
3661
|
-
}
|
|
3662
|
-
|
|
3663
|
-
// src/lib/symlink-farm-for-team-pack-mount-dirs.ts
|
|
3664
|
-
import { promises as fs11 } from "fs";
|
|
3665
|
-
import { dirname as dirname5, join as join22, relative as relative2 } from "path";
|
|
3738
|
+
// src/lib/symlink-farm-for-team-pack-mount-dirs.ts
|
|
3739
|
+
import { promises as fs11 } from "fs";
|
|
3740
|
+
import { dirname as dirname6, join as join22, relative as relative2 } from "path";
|
|
3666
3741
|
|
|
3667
3742
|
// src/lib/backup-existing-dir-before-symlink-override.ts
|
|
3668
3743
|
import { promises as fs10 } from "fs";
|
|
@@ -3696,7 +3771,7 @@ async function isSymbolicLink(path) {
|
|
|
3696
3771
|
}
|
|
3697
3772
|
}
|
|
3698
3773
|
async function syncMountedDir(source, dest, force) {
|
|
3699
|
-
const dir = relative2(
|
|
3774
|
+
const dir = relative2(dirname6(dest), dest) || dest;
|
|
3700
3775
|
if (!await pathExists(source)) {
|
|
3701
3776
|
return { dir, action: "source-missing" };
|
|
3702
3777
|
}
|
|
@@ -3705,14 +3780,14 @@ async function syncMountedDir(source, dest, force) {
|
|
|
3705
3780
|
await fs11.unlink(dest);
|
|
3706
3781
|
} else if (force) {
|
|
3707
3782
|
const backupPath = await backupDirBeforeReplace(dest);
|
|
3708
|
-
const relativeSource2 = relative2(
|
|
3783
|
+
const relativeSource2 = relative2(dirname6(dest), source);
|
|
3709
3784
|
await fs11.symlink(relativeSource2, dest);
|
|
3710
3785
|
return { dir, action: "backed-up-and-linked", backupPath };
|
|
3711
3786
|
} else {
|
|
3712
3787
|
return { dir, action: "skipped-conflict" };
|
|
3713
3788
|
}
|
|
3714
3789
|
}
|
|
3715
|
-
const relativeSource = relative2(
|
|
3790
|
+
const relativeSource = relative2(dirname6(dest), source);
|
|
3716
3791
|
await fs11.symlink(relativeSource, dest);
|
|
3717
3792
|
return { dir, action: "created" };
|
|
3718
3793
|
}
|
|
@@ -3726,11 +3801,52 @@ async function syncAllMountDirs(packDir, claudeDir, force) {
|
|
|
3726
3801
|
return results;
|
|
3727
3802
|
}
|
|
3728
3803
|
|
|
3729
|
-
// src/commands/init-
|
|
3730
|
-
import {
|
|
3731
|
-
import {
|
|
3732
|
-
|
|
3733
|
-
|
|
3804
|
+
// src/commands/init-success-rendering-and-helpers.ts
|
|
3805
|
+
import { join as join24, relative as relative3 } from "path";
|
|
3806
|
+
import { input as input5, select as select9 } from "@inquirer/prompts";
|
|
3807
|
+
import boxen5 from "boxen";
|
|
3808
|
+
|
|
3809
|
+
// src/lib/format-pack-commands-cheatsheet-box.ts
|
|
3810
|
+
import boxen4 from "boxen";
|
|
3811
|
+
var PACK_COMMAND_CHEATSHEET = [
|
|
3812
|
+
{ cmd: "/avatar:help", desc: "H\u01B0\u1EDBng d\u1EABn s\u1EED d\u1EE5ng Avatar \u2014 ch\u1EC9 c\u1EA7n g\xF5 t\u1EF1 nhi\xEAn" },
|
|
3813
|
+
{ cmd: "/avatar:brainstorm", desc: "Brainstorm \xFD t\u01B0\u1EDFng cho m\u1ED9t feature" },
|
|
3814
|
+
{ cmd: "/avatar:plan", desc: "T\u1EA1o plan th\xF4ng minh v\u1EDBi prompt enhancement" },
|
|
3815
|
+
{ cmd: "/avatar:scout", desc: "T\xECm file li\xEAn quan trong codebase, ti\u1EBFt ki\u1EC7m token" },
|
|
3816
|
+
{ cmd: "/avatar:implement", desc: "B\u1EAFt \u0111\u1EA7u code & test theo plan c\xF3 s\u1EB5n" },
|
|
3817
|
+
{ cmd: "/avatar:build-full-flow", desc: "Implement m\u1ED9t feature t\u1EEBng b\u01B0\u1EDBc (end-to-end)" },
|
|
3818
|
+
{ cmd: "/avatar:fix", desc: "Ph\xE2n t\xEDch v\xE0 fix v\u1EA5n \u0111\u1EC1 t\u1EF1 \u0111i\u1EC1u h\u01B0\u1EDBng" },
|
|
3819
|
+
{ cmd: "/avatar:debug", desc: "Debug v\u1EA5n \u0111\u1EC1 k\u1EF9 thu\u1EADt + \u0111\u01B0a ra gi\u1EA3i ph\xE1p" },
|
|
3820
|
+
{ cmd: "/avatar:test", desc: "Ch\u1EA1y test tr\xEAn m\xE1y + ph\xE2n t\xEDch b\xE1o c\xE1o t\u1ED5ng h\u1EE3p" },
|
|
3821
|
+
{ cmd: "/avatar:design:good", desc: "T\u1EA1o thi\u1EBFt k\u1EBF ch\u1EC9n chu, s\u1ED1ng \u0111\u1ED9ng" },
|
|
3822
|
+
{ cmd: "/avatar:docs:init", desc: "Ph\xE2n t\xEDch codebase + t\u1EA1o t\xE0i li\u1EC7u kh\u1EDFi \u0111\u1EA7u" },
|
|
3823
|
+
{ cmd: "/avatar:status", desc: "Xem l\u1EA1i thay \u0111\u1ED5i g\u1EA7n \u0111\xE2y + t\u1ED5ng k\u1EBFt c\xF4ng vi\u1EC7c" },
|
|
3824
|
+
{ cmd: "/avatar:journal", desc: "Ghi nh\u1EADt k\xFD session" }
|
|
3825
|
+
];
|
|
3826
|
+
function formatPackCommandsCheatsheetBox() {
|
|
3827
|
+
const maxCmdWidth = Math.max(...PACK_COMMAND_CHEATSHEET.map((e) => e.cmd.length));
|
|
3828
|
+
const header = chalk.bold("\u{1F3AF} Slash commands t\u1EEB team-ai-pack");
|
|
3829
|
+
const subheader = chalk.dim("G\xF5 trong Claude Code session \u0111\u1EC3 g\u1ECDi capability c\u1EE7a pack:");
|
|
3830
|
+
const lines = PACK_COMMAND_CHEATSHEET.map((e) => {
|
|
3831
|
+
const cmdPadded = chalk.cyan(e.cmd.padEnd(maxCmdWidth));
|
|
3832
|
+
return ` ${cmdPadded} ${chalk.dim(e.desc)}`;
|
|
3833
|
+
});
|
|
3834
|
+
const footer = chalk.dim(
|
|
3835
|
+
"Catalog \u0111\u1EA7y \u0111\u1EE7 46 commands: cat .claude/pack/scripts/commands_data.yaml"
|
|
3836
|
+
);
|
|
3837
|
+
const content = [header, subheader, "", ...lines, "", footer].join("\n");
|
|
3838
|
+
return boxen4(content, {
|
|
3839
|
+
padding: 1,
|
|
3840
|
+
borderStyle: "round",
|
|
3841
|
+
borderColor: "cyan"
|
|
3842
|
+
});
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
// src/commands/init-conflict-detection-helpers.ts
|
|
3846
|
+
import { readdir } from "fs/promises";
|
|
3847
|
+
import { join as join23 } from "path";
|
|
3848
|
+
async function isEmptyOrMissing(path) {
|
|
3849
|
+
if (!await pathExists(path)) return true;
|
|
3734
3850
|
try {
|
|
3735
3851
|
const entries = await readdir(path);
|
|
3736
3852
|
const meaningful = entries.filter((e) => !e.startsWith(".") && e !== "Thumbs.db");
|
|
@@ -3747,383 +3863,359 @@ async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 1
|
|
|
3747
3863
|
return null;
|
|
3748
3864
|
}
|
|
3749
3865
|
|
|
3750
|
-
// src/
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
try {
|
|
3761
|
-
const raw = readFileSync5(candidate, "utf8");
|
|
3762
|
-
const pkg = JSON.parse(raw);
|
|
3763
|
-
if (pkg.name === "@nalvietnam/avatar-cli" && typeof pkg.version === "string") {
|
|
3764
|
-
cachedVersion = pkg.version;
|
|
3765
|
-
return cachedVersion;
|
|
3766
|
-
}
|
|
3767
|
-
} catch {
|
|
3866
|
+
// src/commands/init-success-rendering-and-helpers.ts
|
|
3867
|
+
async function resolveWorkspacePath(parent, desiredName, force) {
|
|
3868
|
+
const desired = join24(parent, desiredName);
|
|
3869
|
+
if (await isEmptyOrMissing(desired)) return desired;
|
|
3870
|
+
log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
|
|
3871
|
+
while (true) {
|
|
3872
|
+
const alternative = await findAlternativeWorkspaceName(parent, desiredName);
|
|
3873
|
+
if (force && alternative) {
|
|
3874
|
+
log.info(`--force: d\xF9ng ${alternative}`);
|
|
3875
|
+
return alternative;
|
|
3768
3876
|
}
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
const raw = readFileSync5(sibling, "utf8");
|
|
3773
|
-
const pkg = JSON.parse(raw);
|
|
3774
|
-
if (pkg.name === "@nalvietnam/avatar-cli" && typeof pkg.version === "string") {
|
|
3775
|
-
cachedVersion = pkg.version;
|
|
3776
|
-
return cachedVersion;
|
|
3777
|
-
}
|
|
3778
|
-
} catch {
|
|
3779
|
-
}
|
|
3877
|
+
const choices = [];
|
|
3878
|
+
if (alternative) {
|
|
3879
|
+
choices.push({ name: `D\xF9ng "${alternative}" (suggest)`, value: "use-alt" });
|
|
3780
3880
|
}
|
|
3881
|
+
choices.push({ name: "Nh\u1EADp t\xEAn workspace kh\xE1c (manual)", value: "manual" });
|
|
3882
|
+
choices.push({ name: "T\u1EA1m ng\u01B0ng init", value: "abort" });
|
|
3883
|
+
const action = await select9({
|
|
3884
|
+
message: "C\xE1ch x\u1EED l\xFD workspace path conflict?",
|
|
3885
|
+
choices
|
|
3886
|
+
});
|
|
3887
|
+
if (action === "abort") {
|
|
3888
|
+
throw new UserAbortedRecoveryError(
|
|
3889
|
+
"User abort t\u1EA1i b\u01B0\u1EDBc resolve workspace path. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c."
|
|
3890
|
+
);
|
|
3891
|
+
}
|
|
3892
|
+
if (action === "use-alt" && alternative) {
|
|
3893
|
+
return alternative;
|
|
3894
|
+
}
|
|
3895
|
+
const newName = await input5({
|
|
3896
|
+
message: "T\xEAn workspace m\u1EDBi:",
|
|
3897
|
+
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
3898
|
+
});
|
|
3899
|
+
const newPath = join24(parent, newName.trim());
|
|
3900
|
+
if (await isEmptyOrMissing(newPath)) return newPath;
|
|
3901
|
+
log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
|
|
3781
3902
|
}
|
|
3782
|
-
cachedVersion = "unknown";
|
|
3783
|
-
return cachedVersion;
|
|
3784
|
-
}
|
|
3785
|
-
|
|
3786
|
-
// src/commands/init-scaffold-variable-builders.ts
|
|
3787
|
-
function inferWorkspaceName(repoUrl) {
|
|
3788
|
-
const trimmed = repoUrl.trim().replace(/\/+$/, "");
|
|
3789
|
-
const lastSegment = trimmed.split(/[/:]/).pop() ?? "";
|
|
3790
|
-
const baseName = lastSegment.replace(/\.git$/, "");
|
|
3791
|
-
if (!baseName) return "avatar-client-workspace";
|
|
3792
|
-
const withoutPrefix = baseName.replace(/^avatar-/, "");
|
|
3793
|
-
return `avatar-${withoutPrefix}-workspace`;
|
|
3794
|
-
}
|
|
3795
|
-
function buildGitnexusSection(gitnexusReady) {
|
|
3796
|
-
if (!gitnexusReady) return "";
|
|
3797
|
-
return `
|
|
3798
|
-
### \u{1F9E0} CODEBASE INTELLIGENCE \u2014 GitNexus
|
|
3799
|
-
|
|
3800
|
-
Workspace c\xF3 GitNexus index t\u1EA1i \`.gitnexus/\` cung c\u1EA5p architectural awareness
|
|
3801
|
-
cho Claude Code (impact analysis, call chains, blast radius).
|
|
3802
|
-
|
|
3803
|
-
**H\u01B0\u1EDBng d\u1EABn cho Claude:**
|
|
3804
|
-
|
|
3805
|
-
- Khi c\u1EA7n hi\u1EC3u codebase architecture (vd: tr\u01B0\u1EDBc refactor, debug call chain) \u2192
|
|
3806
|
-
query GitNexus MCP resource \`gitnexus://repo/<name>/context\`
|
|
3807
|
-
- Khi resource tr\u1EA3 staleness hint "\u26A0\uFE0F Index is N commit(s) behind HEAD" \u2192
|
|
3808
|
-
T\u1EF0 ch\u1EA1y \`gitnexus analyze .\` (trong workspace root) tr\u01B0\u1EDBc khi ti\u1EBFp t\u1EE5c task
|
|
3809
|
-
- Sau khi \`gitnexus analyze\` xong: **KH\xD4NG t\u1EF1 ch\u1EA1y \`gitnexus wiki\`**
|
|
3810
|
-
(cost cao, c\xF3 th\u1EC3 block session 2-5 ph\xFAt) \u2014 user s\u1EBD ch\u1EA1y manual khi c\u1EA7n
|
|
3811
|
-
|
|
3812
|
-
**Folders Claude scan auto cho skills:**
|
|
3813
|
-
|
|
3814
|
-
- \`~/.claude/skills/gitnexus-*/\` \u2014 GitNexus global skills (exploring, debugging, ...)
|
|
3815
|
-
- \`.claude/pack/skills/\` \u2014 NAL team-shared skills (qua team-ai-pack submodule)
|
|
3816
|
-
- C\u1EA3 2 \u0111\u1EC1u \u0111\u01B0\u1EE3c scan, kh\xF4ng xung \u0111\u1ED9t (different naming prefix)
|
|
3817
|
-
|
|
3818
|
-
**Manual wiki update:**
|
|
3819
|
-
|
|
3820
|
-
Khi user c\u1EA7n regenerate wiki sau refactor l\u1EDBn \u2014 ch\u1EA1y:
|
|
3821
|
-
|
|
3822
|
-
\`\`\`bash
|
|
3823
|
-
gitnexus wiki . --api-key <key> --base-url <url>
|
|
3824
|
-
\`\`\`
|
|
3825
|
-
`;
|
|
3826
3903
|
}
|
|
3827
|
-
function
|
|
3828
|
-
return {
|
|
3829
|
-
projectName: args.projectName,
|
|
3830
|
-
projectDescription: args.projectDescription,
|
|
3831
|
-
teamOwner: args.teamOwner,
|
|
3832
|
-
avatarVersion: readCliVersion(),
|
|
3833
|
-
packVersion: args.packVersion,
|
|
3834
|
-
lastScan: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3835
|
-
mode: args.mode,
|
|
3836
|
-
gitnexusSection: buildGitnexusSection(args.gitnexusReady ?? false)
|
|
3837
|
-
};
|
|
3904
|
+
async function promptTeamOwner(currentUserEmail) {
|
|
3905
|
+
return await input5({ message: "Team owner email:", default: currentUserEmail });
|
|
3838
3906
|
}
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
// src/lib/google-oauth-device-flow.ts
|
|
3845
|
-
var GOOGLE_CLIENT_ID = "1014766441755-i4jimivh5rd7vt8phuhmepmt45sovtph.apps.googleusercontent.com";
|
|
3846
|
-
var GOOGLE_CLIENT_SECRET = "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";
|
|
3847
|
-
var HOSTED_DOMAIN = "nal.vn";
|
|
3848
|
-
var SCOPES = ["openid", "email", "profile"];
|
|
3849
|
-
var DEVICE_CODE_URL = "https://oauth2.googleapis.com/device/code";
|
|
3850
|
-
var TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
3851
|
-
var REVOKE_URL = "https://oauth2.googleapis.com/revoke";
|
|
3852
|
-
async function requestDeviceCode() {
|
|
3853
|
-
const body = new URLSearchParams({
|
|
3854
|
-
client_id: GOOGLE_CLIENT_ID,
|
|
3855
|
-
scope: SCOPES.join(" ")
|
|
3856
|
-
});
|
|
3857
|
-
const res = await fetch(DEVICE_CODE_URL, {
|
|
3858
|
-
method: "POST",
|
|
3859
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3860
|
-
body
|
|
3861
|
-
});
|
|
3862
|
-
if (!res.ok) {
|
|
3863
|
-
const text = await res.text();
|
|
3864
|
-
throw new Error(`Device code request failed (${res.status}): ${text}`);
|
|
3907
|
+
async function maybeCommitWorkspace(workspacePath, skipCommit) {
|
|
3908
|
+
if (skipCommit) {
|
|
3909
|
+
log.warn("Skip commit (--no-commit). Ch\u1EA1y 'git status' + commit th\u1EE7 c\xF4ng sau.");
|
|
3910
|
+
return;
|
|
3865
3911
|
}
|
|
3866
|
-
|
|
3912
|
+
const g = git(workspacePath);
|
|
3913
|
+
await g.add(["CLAUDE.md", ".claude/", ".gitignore", ".gitmodules", "notes/", "scripts/"]);
|
|
3914
|
+
await g.commit("chore: initialize Avatar workspace");
|
|
3915
|
+
log.success("\u0110\xE3 commit workspace");
|
|
3867
3916
|
}
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
client_secret: GOOGLE_CLIENT_SECRET,
|
|
3872
|
-
device_code: deviceCode,
|
|
3873
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
3874
|
-
});
|
|
3875
|
-
const res = await fetch(TOKEN_URL, {
|
|
3876
|
-
method: "POST",
|
|
3877
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3878
|
-
body
|
|
3879
|
-
});
|
|
3880
|
-
if (res.ok) {
|
|
3881
|
-
return await res.json();
|
|
3882
|
-
}
|
|
3883
|
-
let errorCode = "";
|
|
3884
|
-
try {
|
|
3885
|
-
const data = await res.json();
|
|
3886
|
-
errorCode = data.error ?? "";
|
|
3887
|
-
} catch {
|
|
3888
|
-
errorCode = "";
|
|
3917
|
+
function formatAiStatusLine(aiResult) {
|
|
3918
|
+
if (aiResult === null) {
|
|
3919
|
+
return ` ${chalk.yellow("AI:")} skipped \xB7 ${chalk.cyan("avatar ai setup")} \u0111\u1EC3 config sau`;
|
|
3889
3920
|
}
|
|
3890
|
-
if (
|
|
3891
|
-
|
|
3921
|
+
if (aiResult.ok) {
|
|
3922
|
+
const modelPart = aiResult.model ? ` \xB7 model=${aiResult.model}` : "";
|
|
3923
|
+
return ` ${chalk.green("AI:")} ready \xB7 ${aiResult.provider}${modelPart}`;
|
|
3892
3924
|
}
|
|
3893
|
-
|
|
3894
|
-
|
|
3925
|
+
return ` ${chalk.yellow("AI:")} failed (${aiResult.reason.slice(0, 60)}) \xB7 th\u1EED ${chalk.cyan("avatar ai setup")}`;
|
|
3926
|
+
}
|
|
3927
|
+
function formatGitnexusStatusLine(result) {
|
|
3928
|
+
if (result === null) {
|
|
3929
|
+
return ` ${chalk.yellow("GitNexus:")} skipped \xB7 ${chalk.cyan("avatar gitnexus install")} \u0111\u1EC3 setup sau`;
|
|
3895
3930
|
}
|
|
3896
|
-
if (
|
|
3897
|
-
|
|
3931
|
+
if (result.ok) {
|
|
3932
|
+
const parts = ["ready"];
|
|
3933
|
+
if (result.analyzed) parts.push("indexed");
|
|
3934
|
+
if (result.wikiGenerated) parts.push("wiki");
|
|
3935
|
+
if (result.mcpRegistered) parts.push("mcp");
|
|
3936
|
+
return ` ${chalk.green("GitNexus:")} ${parts.join(" \xB7 ")}`;
|
|
3898
3937
|
}
|
|
3899
|
-
|
|
3938
|
+
return ` ${chalk.yellow("GitNexus:")} skipped (${(result.reason ?? "unknown").slice(0, 40)}) \xB7 th\u1EED ${chalk.cyan("avatar gitnexus install")}`;
|
|
3900
3939
|
}
|
|
3901
|
-
function
|
|
3902
|
-
const
|
|
3903
|
-
|
|
3904
|
-
|
|
3940
|
+
async function printInitSuccessBox(rootPath, flow, aiResult = null, gitnexusResult = null) {
|
|
3941
|
+
const lines = [
|
|
3942
|
+
`${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative3(process.cwd(), rootPath) || rootPath}`,
|
|
3943
|
+
` ${chalk.dim(`(flow: ${flow})`)}`,
|
|
3944
|
+
formatAiStatusLine(aiResult),
|
|
3945
|
+
formatGitnexusStatusLine(gitnexusResult),
|
|
3946
|
+
"",
|
|
3947
|
+
` ${chalk.cyan(`cd ${rootPath}`)}`,
|
|
3948
|
+
` ${chalk.cyan("claude")} M\u1EDF Claude Code \u1EDF workspace root`,
|
|
3949
|
+
"",
|
|
3950
|
+
` ${chalk.cyan("avatar commit src")} Commit code l\xEAn client remote`,
|
|
3951
|
+
` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
|
|
3952
|
+
` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
|
|
3953
|
+
];
|
|
3954
|
+
process.stdout.write(`${boxen5(lines.join("\n"), { padding: 1, borderStyle: "round" })}
|
|
3955
|
+
`);
|
|
3956
|
+
const packDir = join24(rootPath, TEAM_PACK_RELATIVE_PATH);
|
|
3957
|
+
if (await pathExists(packDir)) {
|
|
3958
|
+
process.stdout.write(`
|
|
3959
|
+
${formatPackCommandsCheatsheetBox()}
|
|
3960
|
+
`);
|
|
3905
3961
|
}
|
|
3906
|
-
const payload = parts[1];
|
|
3907
|
-
if (!payload) throw new Error("id_token thi\u1EBFu payload");
|
|
3908
|
-
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
3909
|
-
const json = Buffer.from(base64, "base64").toString("utf8");
|
|
3910
|
-
return JSON.parse(json);
|
|
3911
3962
|
}
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3963
|
+
|
|
3964
|
+
// src/commands/workspace-scaffold-and-finalize-orchestrator.ts
|
|
3965
|
+
async function getOrCreateOriginRemote(folderPath, opts) {
|
|
3966
|
+
const remotes = await git(folderPath).getRemotes(true);
|
|
3967
|
+
const origin = remotes.find((r) => r.name === "origin");
|
|
3968
|
+
if (origin?.refs.push) {
|
|
3969
|
+
log.success(`Folder \u0111\xE3 c\xF3 remote origin: ${origin.refs.push}`);
|
|
3970
|
+
return origin.refs.push;
|
|
3917
3971
|
}
|
|
3918
|
-
|
|
3919
|
-
|
|
3972
|
+
const shouldCreate = opts.createRemote ?? await confirm5({
|
|
3973
|
+
message: "Folder ch\u01B0a c\xF3 remote. T\u1EA1o GitHub repo ngay \u0111\u1EC3 share team?",
|
|
3974
|
+
default: true
|
|
3975
|
+
});
|
|
3976
|
+
if (!shouldCreate) {
|
|
3977
|
+
log.warn("Ti\u1EBFp t\u1EE5c v\u1EDBi local path. Workspace ch\u1EC9 ch\u1EA1y \u0111\u01B0\u1EE3c tr\xEAn m\xE1y b\u1EA1n.");
|
|
3978
|
+
return void 0;
|
|
3920
3979
|
}
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
refresh_token: token.refresh_token,
|
|
3929
|
-
expires_at: expiresAt,
|
|
3930
|
-
id_token: token.id_token
|
|
3931
|
-
};
|
|
3932
|
-
}
|
|
3933
|
-
async function revokeToken(token) {
|
|
3934
|
-
const body = new URLSearchParams({ token });
|
|
3935
|
-
await fetch(REVOKE_URL, {
|
|
3936
|
-
method: "POST",
|
|
3937
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3938
|
-
body
|
|
3939
|
-
}).catch(() => {
|
|
3980
|
+
await ensureGitHubReady();
|
|
3981
|
+
const visibility = opts.repoVisibility ?? await select10({
|
|
3982
|
+
message: "Visibility?",
|
|
3983
|
+
choices: [
|
|
3984
|
+
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
3985
|
+
{ name: "public", value: "public" }
|
|
3986
|
+
]
|
|
3940
3987
|
});
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
url.searchParams.set("user_code", response.user_code);
|
|
3945
|
-
url.searchParams.set("hd", HOSTED_DOMAIN);
|
|
3946
|
-
return url.toString();
|
|
3947
|
-
}
|
|
3948
|
-
|
|
3949
|
-
// src/commands/login.ts
|
|
3950
|
-
function registerLoginCommand(program2) {
|
|
3951
|
-
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) => {
|
|
3952
|
-
try {
|
|
3953
|
-
await runLogin(opts);
|
|
3954
|
-
} catch (err) {
|
|
3955
|
-
log.error(err instanceof Error ? err.message : String(err));
|
|
3956
|
-
process.exit(1);
|
|
3957
|
-
}
|
|
3988
|
+
const repoName = await input6({
|
|
3989
|
+
message: "T\xEAn repo:",
|
|
3990
|
+
default: basename(folderPath)
|
|
3958
3991
|
});
|
|
3992
|
+
const urls = createGithubRemoteFromFolder({
|
|
3993
|
+
folder: folderPath,
|
|
3994
|
+
name: repoName,
|
|
3995
|
+
visibility,
|
|
3996
|
+
org: opts.repoOrg
|
|
3997
|
+
});
|
|
3998
|
+
return urls.sshUrl;
|
|
3959
3999
|
}
|
|
3960
|
-
async function
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
const existing = await readUserConfig();
|
|
3967
|
-
if (existing && !isTokenExpired(existing)) {
|
|
3968
|
-
log.success(`\u0110\xE3 \u0111\u0103ng nh\u1EADp: ${existing.email}`);
|
|
3969
|
-
return;
|
|
3970
|
-
}
|
|
3971
|
-
}
|
|
3972
|
-
const deviceSpinner = spinner("\u0110ang y\xEAu c\u1EA7u device code t\u1EEB Google...");
|
|
3973
|
-
let deviceCode;
|
|
4000
|
+
async function scaffoldWorkspaceWithSrcSubmodule(args) {
|
|
4001
|
+
await ensureDir(args.workspacePath);
|
|
4002
|
+
await git(args.workspacePath).init();
|
|
4003
|
+
const sp = spinner(
|
|
4004
|
+
args.skipTeamPack ? "Add submodule src/..." : "Add submodule src/ + team-ai-pack..."
|
|
4005
|
+
);
|
|
3974
4006
|
try {
|
|
3975
|
-
|
|
3976
|
-
|
|
4007
|
+
await git(args.workspacePath).subModule(["add", args.srcRemoteUrl, "src"]);
|
|
4008
|
+
let pinnedTag = "HEAD";
|
|
4009
|
+
if (!args.skipTeamPack) {
|
|
4010
|
+
sp.stop();
|
|
4011
|
+
const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
|
|
4012
|
+
args.workspacePath,
|
|
4013
|
+
args.packVersion,
|
|
4014
|
+
args.ssoEmail,
|
|
4015
|
+
args.packLatest === true && !args.packVersion
|
|
4016
|
+
// v1.10.0
|
|
4017
|
+
);
|
|
4018
|
+
pinnedTag = result.pinnedTag ?? "HEAD";
|
|
4019
|
+
sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
|
|
4020
|
+
} else {
|
|
4021
|
+
sp.succeed("Skip team-ai-pack (--skip-team-pack)");
|
|
4022
|
+
}
|
|
4023
|
+
await finalizeWorkspaceScaffold({
|
|
4024
|
+
workspacePath: args.workspacePath,
|
|
4025
|
+
workspaceName: args.workspaceName,
|
|
4026
|
+
teamOwner: args.teamOwner,
|
|
4027
|
+
description: args.description,
|
|
4028
|
+
packVersion: pinnedTag,
|
|
4029
|
+
autoYes: args.autoYes,
|
|
4030
|
+
skipCommit: args.skipCommit,
|
|
4031
|
+
createWorkspaceRemote: args.createWorkspaceRemote,
|
|
4032
|
+
repoVisibility: args.repoVisibility,
|
|
4033
|
+
repoOrg: args.repoOrg,
|
|
4034
|
+
flow: args.flow,
|
|
4035
|
+
aiSkip: args.aiSkip,
|
|
4036
|
+
gitnexusSkip: args.gitnexusSkip
|
|
4037
|
+
});
|
|
3977
4038
|
} catch (err) {
|
|
3978
|
-
|
|
4039
|
+
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
3979
4040
|
throw err;
|
|
3980
4041
|
}
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
`);
|
|
3990
|
-
void open(verificationUrl).catch(() => {
|
|
3991
|
-
log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
|
|
4042
|
+
}
|
|
4043
|
+
async function finalizeWorkspaceScaffold(args) {
|
|
4044
|
+
const vars = buildScaffoldVariables({
|
|
4045
|
+
projectName: args.workspaceName,
|
|
4046
|
+
projectDescription: args.description,
|
|
4047
|
+
teamOwner: args.teamOwner,
|
|
4048
|
+
packVersion: args.packVersion,
|
|
4049
|
+
mode: "client"
|
|
3992
4050
|
});
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4051
|
+
await createClaudeDirTree(args.workspacePath);
|
|
4052
|
+
await writeProjectKnowledgeFiles(args.workspacePath, vars);
|
|
4053
|
+
await writeRootClaudeMd(args.workspacePath, vars);
|
|
4054
|
+
await writeProjectSettings(args.workspacePath, vars);
|
|
4055
|
+
await appendGitignoreEntries(args.workspacePath);
|
|
4056
|
+
await ensureDir(join25(args.workspacePath, "notes"));
|
|
4057
|
+
await ensureDir(join25(args.workspacePath, "scripts"));
|
|
4058
|
+
await installGitHook(join25(args.workspacePath, ".git"), "post-merge");
|
|
4059
|
+
await installGitHook(join25(args.workspacePath, ".git", "modules", "src"), "pre-push");
|
|
4060
|
+
log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
|
|
4061
|
+
await autoSyncPackOnInit(args.workspacePath);
|
|
4062
|
+
await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
|
|
4063
|
+
await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
|
|
4064
|
+
await maybeCreateWorkspaceRemote(args);
|
|
4065
|
+
let aiResult = null;
|
|
4066
|
+
if (args.aiSkip) {
|
|
4067
|
+
log.dim("B\u1ECF qua AI setup (--ai-skip). Setup sau qua: avatar ai setup");
|
|
4068
|
+
} else {
|
|
4069
|
+
aiResult = await runAiSetupPhase({ workspacePath: args.workspacePath });
|
|
4070
|
+
}
|
|
4071
|
+
let gitnexusResult = null;
|
|
4072
|
+
const skipGitnexus = args.aiSkip || args.gitnexusSkip;
|
|
4073
|
+
if (skipGitnexus) {
|
|
4074
|
+
if (args.gitnexusSkip) {
|
|
4075
|
+
log.dim("B\u1ECF qua GitNexus setup (--gitnexus-skip). Setup sau: avatar gitnexus install");
|
|
4076
|
+
} else {
|
|
4077
|
+
log.dim("B\u1ECF qua GitNexus setup (auto-skip do --ai-skip).");
|
|
4005
4078
|
}
|
|
4079
|
+
} else {
|
|
4080
|
+
gitnexusResult = await runGitnexusSetupPhase({ workspacePath: args.workspacePath });
|
|
4006
4081
|
}
|
|
4007
|
-
if (
|
|
4008
|
-
|
|
4009
|
-
|
|
4082
|
+
if (gitnexusResult?.ok) {
|
|
4083
|
+
const updatedVars = buildScaffoldVariables({
|
|
4084
|
+
projectName: args.workspaceName,
|
|
4085
|
+
projectDescription: args.description,
|
|
4086
|
+
teamOwner: args.teamOwner,
|
|
4087
|
+
packVersion: args.packVersion,
|
|
4088
|
+
mode: "client",
|
|
4089
|
+
gitnexusReady: true
|
|
4090
|
+
});
|
|
4091
|
+
await writeRootClaudeMd(args.workspacePath, updatedVars);
|
|
4092
|
+
log.dim("Updated CLAUDE.md v\u1EDBi GitNexus section");
|
|
4010
4093
|
}
|
|
4011
|
-
|
|
4012
|
-
|
|
4094
|
+
await printInitSuccessBox(args.workspacePath, args.flow, aiResult, gitnexusResult);
|
|
4095
|
+
}
|
|
4096
|
+
async function autoSyncPackOnInit(workspacePath) {
|
|
4097
|
+
const packDir = join25(workspacePath, TEAM_PACK_RELATIVE_PATH);
|
|
4098
|
+
if (!await pathExists(packDir)) {
|
|
4099
|
+
log.dim("Pack submodule kh\xF4ng t\u1ED3n t\u1EA1i (skip auto-sync). C\xF3 th\u1EC3 ch\u1EA1y `avatar sync` sau.");
|
|
4100
|
+
return;
|
|
4101
|
+
}
|
|
4102
|
+
const claudeDir = join25(workspacePath, ".claude");
|
|
4103
|
+
log.info("Auto-sync pack content v\xE0o .claude/ (symlinks + settings merge)...");
|
|
4013
4104
|
try {
|
|
4014
|
-
|
|
4105
|
+
const results = await syncAllMountDirs(packDir, claudeDir, false);
|
|
4106
|
+
const created = results.filter((r) => r.action === "created" || r.action === "updated").length;
|
|
4107
|
+
const missing = results.filter((r) => r.action === "source-missing").length;
|
|
4108
|
+
log.success(
|
|
4109
|
+
` \u2713 Symlinks: ${created} created${missing > 0 ? `, ${missing} source-missing (pack thi\u1EBFu dir)` : ""}`
|
|
4110
|
+
);
|
|
4111
|
+
const mergeResult = await mergePackSettingsIntoProjectSettings(workspacePath);
|
|
4112
|
+
switch (mergeResult.action) {
|
|
4113
|
+
case "merged":
|
|
4114
|
+
log.success(` \u2713 settings.json merged (${mergeResult.changes.join("; ")})`);
|
|
4115
|
+
break;
|
|
4116
|
+
case "no-change":
|
|
4117
|
+
log.dim(" - settings.json \u0111\xE3 sync, kh\xF4ng c\u1EA7n thay \u0111\u1ED5i.");
|
|
4118
|
+
break;
|
|
4119
|
+
case "no-pack-template":
|
|
4120
|
+
log.dim(" - Pack kh\xF4ng c\xF3 templates/settings.json.tpl, skip merge.");
|
|
4121
|
+
break;
|
|
4122
|
+
}
|
|
4015
4123
|
} catch (err) {
|
|
4016
|
-
|
|
4017
|
-
|
|
4124
|
+
log.warn(
|
|
4125
|
+
`Auto-sync pack fail: ${err instanceof Error ? err.message : err}. Ch\u1EA1y \`avatar sync\` th\u1EE7 c\xF4ng \u0111\u1EC3 retry.`
|
|
4126
|
+
);
|
|
4018
4127
|
}
|
|
4019
|
-
const userConfig = buildUserConfig(token, claims);
|
|
4020
|
-
await writeUserConfig(userConfig);
|
|
4021
|
-
await appendAuditEntry("login", userConfig.email);
|
|
4022
|
-
log.success(`X\xE1c th\u1EF1c th\xE0nh c\xF4ng: ${userConfig.email}`);
|
|
4023
|
-
log.success(`Verify hosted domain: ${claims.hd} \u2713`);
|
|
4024
|
-
log.success(`L\u01B0u credential v\xE0o ${USER_CONFIG_PATH} (chmod 600)`);
|
|
4025
|
-
}
|
|
4026
|
-
function sleep(ms) {
|
|
4027
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4028
4128
|
}
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
if (!opts.bootstrapStrategy) return void 0;
|
|
4034
|
-
const valid = ["stash", "commit-all", "skip", "branch"];
|
|
4035
|
-
if (valid.includes(opts.bootstrapStrategy)) {
|
|
4036
|
-
return opts.bootstrapStrategy;
|
|
4129
|
+
async function maybeCreateWorkspaceRemote(args) {
|
|
4130
|
+
if (args.skipCommit) {
|
|
4131
|
+
log.dim("Skip workspace remote (ch\u01B0a commit). Setup sau qua: gh repo create ...");
|
|
4132
|
+
return;
|
|
4037
4133
|
}
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
)
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4134
|
+
let shouldCreate = args.createWorkspaceRemote;
|
|
4135
|
+
if (shouldCreate === void 0) {
|
|
4136
|
+
if (args.autoYes) return;
|
|
4137
|
+
shouldCreate = await confirm5({
|
|
4138
|
+
message: "T\u1EA1o remote GitHub cho workspace \u0111\u1EC3 share team? (Avatar state)",
|
|
4139
|
+
default: false
|
|
4140
|
+
});
|
|
4141
|
+
}
|
|
4142
|
+
if (!shouldCreate) return;
|
|
4143
|
+
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select10({
|
|
4144
|
+
message: "Workspace visibility?",
|
|
4145
|
+
choices: [
|
|
4146
|
+
{ name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
|
|
4147
|
+
{ name: "public", value: "public" }
|
|
4148
|
+
]
|
|
4149
|
+
}));
|
|
4150
|
+
while (true) {
|
|
4050
4151
|
try {
|
|
4051
|
-
await
|
|
4152
|
+
await createWorkspaceRemoteViaGh({
|
|
4153
|
+
workspacePath: args.workspacePath,
|
|
4154
|
+
workspaceName: args.workspaceName,
|
|
4155
|
+
visibility,
|
|
4156
|
+
org: args.repoOrg
|
|
4157
|
+
});
|
|
4158
|
+
return;
|
|
4052
4159
|
} catch (err) {
|
|
4053
|
-
if (err instanceof
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4160
|
+
if (err instanceof CreateWorkspaceRemoteError && err.reason === "repo-exists") {
|
|
4161
|
+
const fullName = err.fullName;
|
|
4162
|
+
const reuseAction = await select10({
|
|
4163
|
+
message: `Repo '${fullName}' \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. C\xE1ch x\u1EED l\xFD?`,
|
|
4164
|
+
choices: [
|
|
4165
|
+
{
|
|
4166
|
+
name: "D\xF9ng remote \u0111\xE3 c\xF3 (link workspace local v\xE0o repo n\xE0y)",
|
|
4167
|
+
value: "reuse"
|
|
4168
|
+
},
|
|
4169
|
+
{
|
|
4170
|
+
name: "Nh\u1EADp t\xEAn workspace kh\xE1c (t\u1EA1o repo m\u1EDBi)",
|
|
4171
|
+
value: "rename"
|
|
4172
|
+
},
|
|
4173
|
+
{ name: "B\u1ECF qua (workspace local-only)", value: "skip" },
|
|
4174
|
+
{ name: "T\u1EA1m ng\u01B0ng init", value: "abort" }
|
|
4175
|
+
]
|
|
4176
|
+
});
|
|
4177
|
+
if (reuseAction === "abort") {
|
|
4178
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc t\u1EA1o workspace remote.");
|
|
4179
|
+
}
|
|
4180
|
+
if (reuseAction === "skip") {
|
|
4181
|
+
log.warn("Workspace v\u1EABn s\u1EB5n s\xE0ng local-only. Setup remote sau khi c\u1EA7n.");
|
|
4182
|
+
return;
|
|
4183
|
+
}
|
|
4184
|
+
if (reuseAction === "reuse") {
|
|
4185
|
+
linkExistingRemoteToWorkspace({
|
|
4186
|
+
workspacePath: args.workspacePath,
|
|
4187
|
+
fullName
|
|
4188
|
+
});
|
|
4189
|
+
return;
|
|
4190
|
+
}
|
|
4191
|
+
const newName = await input6({
|
|
4192
|
+
message: "T\xEAn workspace m\u1EDBi (s\u1EBD t\u1EA1o repo new):",
|
|
4193
|
+
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
4194
|
+
});
|
|
4195
|
+
args.workspaceName = newName.trim();
|
|
4196
|
+
continue;
|
|
4060
4197
|
}
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4198
|
+
const action = await promptRetryOrSkip({
|
|
4199
|
+
taskName: "T\u1EA1o workspace remote tr\xEAn GitHub",
|
|
4200
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
4201
|
+
allowSkip: true,
|
|
4202
|
+
// Workspace remote OPTIONAL — skip OK, workspace local vẫn dùng được.
|
|
4203
|
+
hint: "Tip: sai org? Pass --repo-org=<your-gh-user>. Ho\u1EB7c switch gh: gh auth login."
|
|
4204
|
+
});
|
|
4205
|
+
if (action === "abort") {
|
|
4206
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc t\u1EA1o workspace remote.");
|
|
4064
4207
|
}
|
|
4065
|
-
if (
|
|
4066
|
-
log.
|
|
4067
|
-
|
|
4208
|
+
if (action === "skip") {
|
|
4209
|
+
log.warn("Workspace v\u1EABn s\u1EB5n s\xE0ng local-only. Setup remote sau khi c\u1EA7n.");
|
|
4210
|
+
return;
|
|
4068
4211
|
}
|
|
4069
|
-
log.error(err instanceof Error ? err.message : String(err));
|
|
4070
|
-
process.exit(1);
|
|
4071
|
-
}
|
|
4072
|
-
});
|
|
4073
|
-
}
|
|
4074
|
-
async function runInit(opts) {
|
|
4075
|
-
if (!opts.yes) printAvatarBanner({ tagline: "Kh\u1EDFi t\u1EA1o Avatar trong d\u1EF1 \xE1n c\u1EE7a b\u1EA1n" });
|
|
4076
|
-
if (opts.mode) {
|
|
4077
|
-
log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
|
|
4078
|
-
}
|
|
4079
|
-
let userConfig = await readUserConfig();
|
|
4080
|
-
while (!userConfig || isTokenExpired(userConfig)) {
|
|
4081
|
-
log.info("Ch\u01B0a \u0111\u0103ng nh\u1EADp \u2014 ch\u1EA1y login flow tr\u01B0\u1EDBc khi init...");
|
|
4082
|
-
try {
|
|
4083
|
-
await runLogin({});
|
|
4084
|
-
} catch (err) {
|
|
4085
|
-
log.warn(`Login fail: ${err.message}`);
|
|
4086
|
-
}
|
|
4087
|
-
userConfig = await readUserConfig();
|
|
4088
|
-
if (userConfig && !isTokenExpired(userConfig)) break;
|
|
4089
|
-
const action = await promptRetryOrSkip({
|
|
4090
|
-
taskName: "\u0110\u0103ng nh\u1EADp SSO Google",
|
|
4091
|
-
reason: "Token ch\u01B0a \u0111\u01B0\u1EE3c t\u1EA1o ho\u1EB7c \u0111\xE3 h\u1EBFt h\u1EA1n.",
|
|
4092
|
-
allowSkip: false,
|
|
4093
|
-
// Login bắt buộc, không skip được.
|
|
4094
|
-
hint: "\u0110\u1EA3m b\u1EA3o b\u1EA1n ch\u1ECDn 'Allow' tr\xEAn browser v\xE0 d\xF9ng email @nal.vn."
|
|
4095
|
-
});
|
|
4096
|
-
if (action === "abort") {
|
|
4097
|
-
throw new UserAbortedRecoveryError(
|
|
4098
|
-
"User abort t\u1EA1i b\u01B0\u1EDBc login. Ch\u1EA1y 'avatar login' tay r\u1ED3i 'avatar init' l\u1EA1i."
|
|
4099
|
-
);
|
|
4100
4212
|
}
|
|
4101
4213
|
}
|
|
4102
|
-
const status = opts.projectStatus ?? await promptProjectStatus();
|
|
4103
|
-
switch (status) {
|
|
4104
|
-
case "existing-remote":
|
|
4105
|
-
await runInitFromExistingRemote(opts, userConfig.email);
|
|
4106
|
-
break;
|
|
4107
|
-
case "existing-folder":
|
|
4108
|
-
await runInitFromExistingFolder(opts, userConfig.email);
|
|
4109
|
-
break;
|
|
4110
|
-
case "new-project":
|
|
4111
|
-
await runInitFromScratch(opts, userConfig.email);
|
|
4112
|
-
break;
|
|
4113
|
-
}
|
|
4114
|
-
}
|
|
4115
|
-
async function promptProjectStatus() {
|
|
4116
|
-
return await select9({
|
|
4117
|
-
message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
|
|
4118
|
-
choices: [
|
|
4119
|
-
{ name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
|
|
4120
|
-
{ name: "2. \u0110\xE3 c\xF3 folder code local", value: "existing-folder" },
|
|
4121
|
-
{ name: "3. D\u1EF1 \xE1n m\u1EDBi ho\xE0n to\xE0n", value: "new-project" }
|
|
4122
|
-
]
|
|
4123
|
-
});
|
|
4124
4214
|
}
|
|
4215
|
+
|
|
4216
|
+
// src/commands/init-flow-handlers-for-each-project-status.ts
|
|
4125
4217
|
async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
4126
|
-
const initialRemoteUrl = opts.clientRepo ?? await
|
|
4218
|
+
const initialRemoteUrl = opts.clientRepo ?? await input7({
|
|
4127
4219
|
message: "URL git c\u1EE7a repo:",
|
|
4128
4220
|
validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
|
|
4129
4221
|
});
|
|
@@ -4131,7 +4223,7 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
|
4131
4223
|
const remoteUrl = resolvedRemoteUrl ?? initialRemoteUrl;
|
|
4132
4224
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
4133
4225
|
const inferredName = inferWorkspaceName(remoteUrl);
|
|
4134
|
-
const workspaceName = opts.workspaceName ?? await
|
|
4226
|
+
const workspaceName = opts.workspaceName ?? await input7({ message: "T\xEAn workspace:", default: inferredName });
|
|
4135
4227
|
const workspaceParent = resolve2(opts.workspaceParent ?? ".");
|
|
4136
4228
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
4137
4229
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -4156,7 +4248,7 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
|
4156
4248
|
}
|
|
4157
4249
|
async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
4158
4250
|
const folderPath = resolve2(
|
|
4159
|
-
opts.folderPath ?? await
|
|
4251
|
+
opts.folderPath ?? await input7({
|
|
4160
4252
|
message: "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3:",
|
|
4161
4253
|
validate: (v) => v.length > 0 ? true : "Path b\u1EAFt bu\u1ED9c"
|
|
4162
4254
|
})
|
|
@@ -4167,8 +4259,8 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
|
4167
4259
|
});
|
|
4168
4260
|
const remoteUrl = await getOrCreateOriginRemote(folderPath, opts);
|
|
4169
4261
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
4170
|
-
const inferredName = opts.workspaceName ?? `${
|
|
4171
|
-
const workspaceName = opts.workspaceName ?? await
|
|
4262
|
+
const inferredName = opts.workspaceName ?? `${basename2(folderPath)}-avatar-workspace`;
|
|
4263
|
+
const workspaceName = opts.workspaceName ?? await input7({ message: "T\xEAn workspace:", default: inferredName });
|
|
4172
4264
|
const workspaceParent = resolve2(opts.workspaceParent ?? ".");
|
|
4173
4265
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
4174
4266
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -4194,11 +4286,11 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
|
4194
4286
|
}
|
|
4195
4287
|
async function runInitFromScratch(opts, ownerEmail) {
|
|
4196
4288
|
await ensureGitHubReady();
|
|
4197
|
-
const projectName = opts.workspaceName ?? await
|
|
4289
|
+
const projectName = opts.workspaceName ?? await input7({
|
|
4198
4290
|
message: "T\xEAn d\u1EF1 \xE1n:",
|
|
4199
4291
|
validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
|
|
4200
4292
|
});
|
|
4201
|
-
const visibility = opts.repoVisibility ?? await
|
|
4293
|
+
const visibility = opts.repoVisibility ?? await select11({
|
|
4202
4294
|
message: "Visibility?",
|
|
4203
4295
|
choices: [
|
|
4204
4296
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
@@ -4208,7 +4300,7 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
4208
4300
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
4209
4301
|
const workspaceParent = resolve2(opts.workspaceParent ?? ".");
|
|
4210
4302
|
const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
|
|
4211
|
-
const srcPath =
|
|
4303
|
+
const srcPath = join26(workspacePath, "src");
|
|
4212
4304
|
await ensureDir(workspacePath);
|
|
4213
4305
|
await ensureDir(srcPath);
|
|
4214
4306
|
await safeBootstrapGitInFolder(srcPath, { autoYes: true });
|
|
@@ -4231,86 +4323,8 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
4231
4323
|
workspacePath,
|
|
4232
4324
|
opts.packVersion,
|
|
4233
4325
|
ownerEmail,
|
|
4234
|
-
opts.latest === true && !opts.packVersion
|
|
4235
|
-
// v1.10.0: latest mode khi flag set + không có explicit tag
|
|
4236
|
-
);
|
|
4237
|
-
pinnedTag = result.pinnedTag ?? "HEAD";
|
|
4238
|
-
sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
|
|
4239
|
-
} else {
|
|
4240
|
-
sp.succeed("Skip team-ai-pack (--skip-team-pack)");
|
|
4241
|
-
}
|
|
4242
|
-
await finalizeWorkspaceScaffold({
|
|
4243
|
-
workspacePath,
|
|
4244
|
-
workspaceName: projectName,
|
|
4245
|
-
teamOwner,
|
|
4246
|
-
description: opts.description ?? `D\u1EF1 \xE1n m\u1EDBi: ${projectName}`,
|
|
4247
|
-
packVersion: pinnedTag,
|
|
4248
|
-
autoYes: opts.yes,
|
|
4249
|
-
skipCommit: opts.commit === false,
|
|
4250
|
-
createWorkspaceRemote: opts.workspaceRemote,
|
|
4251
|
-
repoVisibility: opts.repoVisibility,
|
|
4252
|
-
repoOrg: opts.repoOrg,
|
|
4253
|
-
flow: "new-project",
|
|
4254
|
-
aiSkip: opts.aiSkip,
|
|
4255
|
-
gitnexusSkip: opts.gitnexusSkip
|
|
4256
|
-
});
|
|
4257
|
-
} catch (err) {
|
|
4258
|
-
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
4259
|
-
throw err;
|
|
4260
|
-
}
|
|
4261
|
-
}
|
|
4262
|
-
async function getOrCreateOriginRemote(folderPath, opts) {
|
|
4263
|
-
const remotes = await git(folderPath).getRemotes(true);
|
|
4264
|
-
const origin = remotes.find((r) => r.name === "origin");
|
|
4265
|
-
if (origin?.refs.push) {
|
|
4266
|
-
log.success(`Folder \u0111\xE3 c\xF3 remote origin: ${origin.refs.push}`);
|
|
4267
|
-
return origin.refs.push;
|
|
4268
|
-
}
|
|
4269
|
-
const shouldCreate = opts.createRemote ?? await confirm5({
|
|
4270
|
-
message: "Folder ch\u01B0a c\xF3 remote. T\u1EA1o GitHub repo ngay \u0111\u1EC3 share team?",
|
|
4271
|
-
default: true
|
|
4272
|
-
});
|
|
4273
|
-
if (!shouldCreate) {
|
|
4274
|
-
log.warn("Ti\u1EBFp t\u1EE5c v\u1EDBi local path. Workspace ch\u1EC9 ch\u1EA1y \u0111\u01B0\u1EE3c tr\xEAn m\xE1y b\u1EA1n.");
|
|
4275
|
-
return void 0;
|
|
4276
|
-
}
|
|
4277
|
-
await ensureGitHubReady();
|
|
4278
|
-
const visibility = opts.repoVisibility ?? await select9({
|
|
4279
|
-
message: "Visibility?",
|
|
4280
|
-
choices: [
|
|
4281
|
-
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
4282
|
-
{ name: "public", value: "public" }
|
|
4283
|
-
]
|
|
4284
|
-
});
|
|
4285
|
-
const repoName = await input5({
|
|
4286
|
-
message: "T\xEAn repo:",
|
|
4287
|
-
default: basename(folderPath)
|
|
4288
|
-
});
|
|
4289
|
-
const urls = createGithubRemoteFromFolder({
|
|
4290
|
-
folder: folderPath,
|
|
4291
|
-
name: repoName,
|
|
4292
|
-
visibility,
|
|
4293
|
-
org: opts.repoOrg
|
|
4294
|
-
});
|
|
4295
|
-
return urls.sshUrl;
|
|
4296
|
-
}
|
|
4297
|
-
async function scaffoldWorkspaceWithSrcSubmodule(args) {
|
|
4298
|
-
await ensureDir(args.workspacePath);
|
|
4299
|
-
await git(args.workspacePath).init();
|
|
4300
|
-
const sp = spinner(
|
|
4301
|
-
args.skipTeamPack ? "Add submodule src/..." : "Add submodule src/ + team-ai-pack..."
|
|
4302
|
-
);
|
|
4303
|
-
try {
|
|
4304
|
-
await git(args.workspacePath).subModule(["add", args.srcRemoteUrl, "src"]);
|
|
4305
|
-
let pinnedTag = "HEAD";
|
|
4306
|
-
if (!args.skipTeamPack) {
|
|
4307
|
-
sp.stop();
|
|
4308
|
-
const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
|
|
4309
|
-
args.workspacePath,
|
|
4310
|
-
args.packVersion,
|
|
4311
|
-
args.ssoEmail,
|
|
4312
|
-
args.packLatest === true && !args.packVersion
|
|
4313
|
-
// v1.10.0
|
|
4326
|
+
opts.latest === true && !opts.packVersion
|
|
4327
|
+
// v1.10.0: latest mode khi flag set + không có explicit tag
|
|
4314
4328
|
);
|
|
4315
4329
|
pinnedTag = result.pinnedTag ?? "HEAD";
|
|
4316
4330
|
sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
|
|
@@ -4318,292 +4332,299 @@ async function scaffoldWorkspaceWithSrcSubmodule(args) {
|
|
|
4318
4332
|
sp.succeed("Skip team-ai-pack (--skip-team-pack)");
|
|
4319
4333
|
}
|
|
4320
4334
|
await finalizeWorkspaceScaffold({
|
|
4321
|
-
workspacePath
|
|
4322
|
-
workspaceName:
|
|
4323
|
-
teamOwner
|
|
4324
|
-
description:
|
|
4335
|
+
workspacePath,
|
|
4336
|
+
workspaceName: projectName,
|
|
4337
|
+
teamOwner,
|
|
4338
|
+
description: opts.description ?? `D\u1EF1 \xE1n m\u1EDBi: ${projectName}`,
|
|
4325
4339
|
packVersion: pinnedTag,
|
|
4326
|
-
autoYes:
|
|
4327
|
-
skipCommit:
|
|
4328
|
-
createWorkspaceRemote:
|
|
4329
|
-
repoVisibility:
|
|
4330
|
-
repoOrg:
|
|
4331
|
-
flow:
|
|
4332
|
-
aiSkip:
|
|
4333
|
-
gitnexusSkip:
|
|
4340
|
+
autoYes: opts.yes,
|
|
4341
|
+
skipCommit: opts.commit === false,
|
|
4342
|
+
createWorkspaceRemote: opts.workspaceRemote,
|
|
4343
|
+
repoVisibility: opts.repoVisibility,
|
|
4344
|
+
repoOrg: opts.repoOrg,
|
|
4345
|
+
flow: "new-project",
|
|
4346
|
+
aiSkip: opts.aiSkip,
|
|
4347
|
+
gitnexusSkip: opts.gitnexusSkip
|
|
4334
4348
|
});
|
|
4335
4349
|
} catch (err) {
|
|
4336
4350
|
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
4337
4351
|
throw err;
|
|
4338
4352
|
}
|
|
4339
4353
|
}
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4354
|
+
|
|
4355
|
+
// src/commands/login.ts
|
|
4356
|
+
import boxen6 from "boxen";
|
|
4357
|
+
import open from "open";
|
|
4358
|
+
|
|
4359
|
+
// src/lib/google-oauth-device-flow.ts
|
|
4360
|
+
var GOOGLE_CLIENT_ID = "1014766441755-i4jimivh5rd7vt8phuhmepmt45sovtph.apps.googleusercontent.com";
|
|
4361
|
+
var GOOGLE_CLIENT_SECRET = "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";
|
|
4362
|
+
var HOSTED_DOMAIN = "nal.vn";
|
|
4363
|
+
var SCOPES = ["openid", "email", "profile"];
|
|
4364
|
+
var DEVICE_CODE_URL = "https://oauth2.googleapis.com/device/code";
|
|
4365
|
+
var TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
4366
|
+
var REVOKE_URL = "https://oauth2.googleapis.com/revoke";
|
|
4367
|
+
async function requestDeviceCode() {
|
|
4368
|
+
const body = new URLSearchParams({
|
|
4369
|
+
client_id: GOOGLE_CLIENT_ID,
|
|
4370
|
+
scope: SCOPES.join(" ")
|
|
4347
4371
|
});
|
|
4348
|
-
await
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
await installGitHook(join25(args.workspacePath, ".git", "modules", "src"), "pre-push");
|
|
4357
|
-
log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
|
|
4358
|
-
await autoSyncPackOnInit(args.workspacePath);
|
|
4359
|
-
await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
|
|
4360
|
-
await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
|
|
4361
|
-
await maybeCreateWorkspaceRemote(args);
|
|
4362
|
-
let aiResult = null;
|
|
4363
|
-
if (args.aiSkip) {
|
|
4364
|
-
log.dim("B\u1ECF qua AI setup (--ai-skip). Setup sau qua: avatar ai setup");
|
|
4365
|
-
} else {
|
|
4366
|
-
aiResult = await runAiSetupPhase({ workspacePath: args.workspacePath });
|
|
4372
|
+
const res = await fetch(DEVICE_CODE_URL, {
|
|
4373
|
+
method: "POST",
|
|
4374
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
4375
|
+
body
|
|
4376
|
+
});
|
|
4377
|
+
if (!res.ok) {
|
|
4378
|
+
const text = await res.text();
|
|
4379
|
+
throw new Error(`Device code request failed (${res.status}): ${text}`);
|
|
4367
4380
|
}
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
}
|
|
4377
|
-
|
|
4381
|
+
return await res.json();
|
|
4382
|
+
}
|
|
4383
|
+
async function pollForToken(deviceCode) {
|
|
4384
|
+
const body = new URLSearchParams({
|
|
4385
|
+
client_id: GOOGLE_CLIENT_ID,
|
|
4386
|
+
client_secret: GOOGLE_CLIENT_SECRET,
|
|
4387
|
+
device_code: deviceCode,
|
|
4388
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
4389
|
+
});
|
|
4390
|
+
const res = await fetch(TOKEN_URL, {
|
|
4391
|
+
method: "POST",
|
|
4392
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
4393
|
+
body
|
|
4394
|
+
});
|
|
4395
|
+
if (res.ok) {
|
|
4396
|
+
return await res.json();
|
|
4378
4397
|
}
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
mode: "client",
|
|
4386
|
-
gitnexusReady: true
|
|
4387
|
-
});
|
|
4388
|
-
await writeRootClaudeMd(args.workspacePath, updatedVars);
|
|
4389
|
-
log.dim("Updated CLAUDE.md v\u1EDBi GitNexus section");
|
|
4398
|
+
let errorCode = "";
|
|
4399
|
+
try {
|
|
4400
|
+
const data = await res.json();
|
|
4401
|
+
errorCode = data.error ?? "";
|
|
4402
|
+
} catch {
|
|
4403
|
+
errorCode = "";
|
|
4390
4404
|
}
|
|
4391
|
-
|
|
4405
|
+
if (errorCode === "authorization_pending" || errorCode === "slow_down") {
|
|
4406
|
+
return null;
|
|
4407
|
+
}
|
|
4408
|
+
if (errorCode === "access_denied") {
|
|
4409
|
+
throw new Error("User t\u1EEB ch\u1ED1i quy\u1EC1n truy c\u1EADp");
|
|
4410
|
+
}
|
|
4411
|
+
if (errorCode === "expired_token") {
|
|
4412
|
+
throw new Error("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
|
|
4413
|
+
}
|
|
4414
|
+
throw new Error(`OAuth token endpoint tr\u1EA3 l\u1ED7i: ${errorCode || res.status}`);
|
|
4392
4415
|
}
|
|
4393
|
-
|
|
4394
|
-
const
|
|
4395
|
-
if (
|
|
4396
|
-
|
|
4397
|
-
return;
|
|
4416
|
+
function decodeIdToken(idToken) {
|
|
4417
|
+
const parts = idToken.split(".");
|
|
4418
|
+
if (parts.length !== 3) {
|
|
4419
|
+
throw new Error("id_token format kh\xF4ng h\u1EE3p l\u1EC7");
|
|
4398
4420
|
}
|
|
4399
|
-
const
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4421
|
+
const payload = parts[1];
|
|
4422
|
+
if (!payload) throw new Error("id_token thi\u1EBFu payload");
|
|
4423
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
4424
|
+
const json = Buffer.from(base64, "base64").toString("utf8");
|
|
4425
|
+
return JSON.parse(json);
|
|
4426
|
+
}
|
|
4427
|
+
function verifyHostedDomain(claims) {
|
|
4428
|
+
if (claims.hd !== HOSTED_DOMAIN) {
|
|
4429
|
+
throw new Error(
|
|
4430
|
+
`Email kh\xF4ng thu\u1ED9c workspace NAL (y\xEAu c\u1EA7u @${HOSTED_DOMAIN}). Nh\u1EADn: ${claims.email}`
|
|
4407
4431
|
);
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4432
|
+
}
|
|
4433
|
+
if (!claims.email_verified) {
|
|
4434
|
+
throw new Error("Email ch\u01B0a \u0111\u01B0\u1EE3c Google verify");
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
function buildUserConfig(token, claims) {
|
|
4438
|
+
const expiresAt = new Date(Date.now() + token.expires_in * 1e3).toISOString();
|
|
4439
|
+
return {
|
|
4440
|
+
email: claims.email,
|
|
4441
|
+
name: claims.name ?? claims.email,
|
|
4442
|
+
access_token: token.access_token,
|
|
4443
|
+
refresh_token: token.refresh_token,
|
|
4444
|
+
expires_at: expiresAt,
|
|
4445
|
+
id_token: token.id_token
|
|
4446
|
+
};
|
|
4447
|
+
}
|
|
4448
|
+
async function revokeToken(token) {
|
|
4449
|
+
const body = new URLSearchParams({ token });
|
|
4450
|
+
await fetch(REVOKE_URL, {
|
|
4451
|
+
method: "POST",
|
|
4452
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
4453
|
+
body
|
|
4454
|
+
}).catch(() => {
|
|
4455
|
+
});
|
|
4456
|
+
}
|
|
4457
|
+
function buildVerificationUrl(response) {
|
|
4458
|
+
const url = new URL(response.verification_url);
|
|
4459
|
+
url.searchParams.set("user_code", response.user_code);
|
|
4460
|
+
url.searchParams.set("hd", HOSTED_DOMAIN);
|
|
4461
|
+
return url.toString();
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
// src/commands/login.ts
|
|
4465
|
+
function registerLoginCommand(program2) {
|
|
4466
|
+
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) => {
|
|
4467
|
+
try {
|
|
4468
|
+
await runLogin(opts);
|
|
4469
|
+
} catch (err) {
|
|
4470
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
4471
|
+
process.exit(1);
|
|
4472
|
+
}
|
|
4473
|
+
});
|
|
4474
|
+
}
|
|
4475
|
+
async function runLogin(opts) {
|
|
4476
|
+
printAvatarBanner({ tagline: "\u0110\u0103ng nh\u1EADp Google SSO \xB7 workspace @nal.vn" });
|
|
4477
|
+
if (opts.reset) {
|
|
4478
|
+
await clearUserConfig();
|
|
4479
|
+
await appendAuditEntry("login_reset");
|
|
4480
|
+
} else {
|
|
4481
|
+
const existing = await readUserConfig();
|
|
4482
|
+
if (existing && !isTokenExpired(existing)) {
|
|
4483
|
+
log.success(`\u0110\xE3 \u0111\u0103ng nh\u1EADp: ${existing.email}`);
|
|
4484
|
+
return;
|
|
4419
4485
|
}
|
|
4486
|
+
}
|
|
4487
|
+
const deviceSpinner = spinner("\u0110ang y\xEAu c\u1EA7u device code t\u1EEB Google...");
|
|
4488
|
+
let deviceCode;
|
|
4489
|
+
try {
|
|
4490
|
+
deviceCode = await requestDeviceCode();
|
|
4491
|
+
deviceSpinner.succeed("Nh\u1EADn device code");
|
|
4420
4492
|
} catch (err) {
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4493
|
+
deviceSpinner.fail("Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c Google");
|
|
4494
|
+
throw err;
|
|
4495
|
+
}
|
|
4496
|
+
const verificationUrl = buildVerificationUrl(deviceCode);
|
|
4497
|
+
const instructions = [
|
|
4498
|
+
`1. Truy c\u1EADp: ${chalk.cyan(deviceCode.verification_url)}`,
|
|
4499
|
+
`2. Nh\u1EADp code: ${chalk.bold.yellow(deviceCode.user_code)}`,
|
|
4500
|
+
"",
|
|
4501
|
+
`Ho\u1EB7c Avatar t\u1EF1 m\u1EDF browser, click ${chalk.green("Allow")}...`
|
|
4502
|
+
].join("\n");
|
|
4503
|
+
process.stdout.write(`${boxen6(instructions, { padding: 1, borderStyle: "round" })}
|
|
4504
|
+
`);
|
|
4505
|
+
void open(verificationUrl).catch(() => {
|
|
4506
|
+
log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
|
|
4507
|
+
});
|
|
4508
|
+
const waitSpinner = spinner("\u0110ang ch\u1EDD x\xE1c nh\u1EADn trong browser...");
|
|
4509
|
+
const intervalMs = deviceCode.interval * 1e3;
|
|
4510
|
+
const deadline = Date.now() + deviceCode.expires_in * 1e3;
|
|
4511
|
+
let token = null;
|
|
4512
|
+
while (Date.now() < deadline) {
|
|
4513
|
+
await sleep(intervalMs);
|
|
4514
|
+
try {
|
|
4515
|
+
token = await pollForToken(deviceCode.device_code);
|
|
4516
|
+
if (token) break;
|
|
4517
|
+
} catch (err) {
|
|
4518
|
+
waitSpinner.fail("X\xE1c th\u1EF1c th\u1EA5t b\u1EA1i");
|
|
4519
|
+
throw err;
|
|
4520
|
+
}
|
|
4424
4521
|
}
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
log.dim("Skip workspace remote (ch\u01B0a commit). Setup sau qua: gh repo create ...");
|
|
4429
|
-
return;
|
|
4522
|
+
if (!token) {
|
|
4523
|
+
waitSpinner.fail("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
|
|
4524
|
+
process.exit(1);
|
|
4430
4525
|
}
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4526
|
+
waitSpinner.succeed("\u0110\xE3 nh\u1EADn token t\u1EEB Google");
|
|
4527
|
+
const claims = decodeIdToken(token.id_token);
|
|
4528
|
+
try {
|
|
4529
|
+
verifyHostedDomain(claims);
|
|
4530
|
+
} catch (err) {
|
|
4531
|
+
await revokeToken(token.access_token);
|
|
4532
|
+
throw err;
|
|
4438
4533
|
}
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4534
|
+
const userConfig = buildUserConfig(token, claims);
|
|
4535
|
+
await writeUserConfig(userConfig);
|
|
4536
|
+
await appendAuditEntry("login", userConfig.email);
|
|
4537
|
+
log.success(`X\xE1c th\u1EF1c th\xE0nh c\xF4ng: ${userConfig.email}`);
|
|
4538
|
+
log.success(`Verify hosted domain: ${claims.hd} \u2713`);
|
|
4539
|
+
log.success(`L\u01B0u credential v\xE0o ${USER_CONFIG_PATH} (chmod 600)`);
|
|
4540
|
+
}
|
|
4541
|
+
function sleep(ms) {
|
|
4542
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
// src/commands/init.ts
|
|
4546
|
+
function registerInitCommand(program2) {
|
|
4547
|
+
program2.command("init").description("Kh\u1EDFi t\u1EA1o Avatar \u2014 3 flow t\u1EF1 nh\u1EADn di\u1EC7n (repo / folder / new)").option("--project-status <val>", "existing-remote | existing-folder | new-project").option("--folder-path <path>", "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3 (flow existing-folder)").option("--create-remote", "Force t\u1EA1o remote qua gh (flow existing-folder ho\u1EB7c new-project)").option("--repo-visibility <val>", "private (m\u1EB7c \u0111\u1ECBnh) | public").option("--repo-org <name>", "GitHub org/owner cho repo m\u1EDBi").option("--client-repo <url>", "URL git remote (flow existing-remote)").option("--workspace-name <name>", "T\xEAn workspace").option("--workspace-parent <path>", "Th\u01B0 m\u1EE5c cha t\u1EA1o workspace (m\u1EB7c \u0111\u1ECBnh . \u2014 CWD)").option("--pack-version <tag>", "Pin team-ai-pack v\xE0o tag c\u1EE5 th\u1EC3").option("--latest", "Pull HEAD c\u1EE7a team-ai-pack main branch (b\u1ECF qua tag SemVer, bleeding-edge)").option("--team-owner <email>", "Email team owner (b\u1ECF qua prompt)").option("--description <text>", "M\xF4 t\u1EA3 1 d\xF2ng c\u1EE7a d\u1EF1 \xE1n").option("--skip-scan", "B\u1ECF qua project-scanner sau scaffold").option("--skip-team-pack", "B\u1ECF qua submodule team-ai-pack (test mode)").option("--force", "B\u1ECF qua prompt khi workspace path \u0111\xE3 t\u1ED3n t\u1EA1i").option("--yes", "Auto-confirm t\u1EA5t c\u1EA3 prompt").option("--no-commit", "Skip commit workspace initial state (m\u1EB7c \u0111\u1ECBnh LU\xD4N commit)").option("--workspace-remote", "T\u1EA1o GitHub remote cho workspace root (default: prompt)").option("--ai-skip", "B\u1ECF qua phase AI setup (CI/test mode \u2014 ch\u1EA1y `avatar ai setup` sau)").option(
|
|
4548
|
+
"--gitnexus-skip",
|
|
4549
|
+
"B\u1ECF qua phase GitNexus setup (M10 \u2014 ch\u1EA1y `avatar gitnexus install` sau)"
|
|
4550
|
+
).option(
|
|
4551
|
+
"--bootstrap-strategy <s>",
|
|
4552
|
+
"X\u1EED l\xFD folder dirty: stash | commit-all | skip | branch (default: prompt)"
|
|
4553
|
+
).option("--preserve-uncommitted", "Alias cho --bootstrap-strategy=stash (gi\u1EEF changes user)").option("--mode <mode>", "[DEPRECATED] D\xF9ng --project-status thay th\u1EBF").action(async (opts) => {
|
|
4448
4554
|
try {
|
|
4449
|
-
await
|
|
4450
|
-
workspacePath: args.workspacePath,
|
|
4451
|
-
workspaceName: args.workspaceName,
|
|
4452
|
-
visibility,
|
|
4453
|
-
org: args.repoOrg
|
|
4454
|
-
});
|
|
4455
|
-
return;
|
|
4555
|
+
await runInit(opts);
|
|
4456
4556
|
} catch (err) {
|
|
4457
|
-
if (err instanceof
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
message: `Repo '${fullName}' \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. C\xE1ch x\u1EED l\xFD?`,
|
|
4461
|
-
choices: [
|
|
4462
|
-
{
|
|
4463
|
-
name: "D\xF9ng remote \u0111\xE3 c\xF3 (link workspace local v\xE0o repo n\xE0y)",
|
|
4464
|
-
value: "reuse"
|
|
4465
|
-
},
|
|
4466
|
-
{
|
|
4467
|
-
name: "Nh\u1EADp t\xEAn workspace kh\xE1c (t\u1EA1o repo m\u1EDBi)",
|
|
4468
|
-
value: "rename"
|
|
4469
|
-
},
|
|
4470
|
-
{ name: "B\u1ECF qua (workspace local-only)", value: "skip" },
|
|
4471
|
-
{ name: "T\u1EA1m ng\u01B0ng init", value: "abort" }
|
|
4472
|
-
]
|
|
4473
|
-
});
|
|
4474
|
-
if (reuseAction === "abort") {
|
|
4475
|
-
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc t\u1EA1o workspace remote.");
|
|
4476
|
-
}
|
|
4477
|
-
if (reuseAction === "skip") {
|
|
4478
|
-
log.warn("Workspace v\u1EABn s\u1EB5n s\xE0ng local-only. Setup remote sau khi c\u1EA7n.");
|
|
4479
|
-
return;
|
|
4480
|
-
}
|
|
4481
|
-
if (reuseAction === "reuse") {
|
|
4482
|
-
linkExistingRemoteToWorkspace({
|
|
4483
|
-
workspacePath: args.workspacePath,
|
|
4484
|
-
fullName
|
|
4485
|
-
});
|
|
4486
|
-
return;
|
|
4487
|
-
}
|
|
4488
|
-
const newName = await input5({
|
|
4489
|
-
message: "T\xEAn workspace m\u1EDBi (s\u1EBD t\u1EA1o repo new):",
|
|
4490
|
-
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
4491
|
-
});
|
|
4492
|
-
args.workspaceName = newName.trim();
|
|
4493
|
-
continue;
|
|
4557
|
+
if (err instanceof InitAbortedByUserError) {
|
|
4558
|
+
log.dim(err.message);
|
|
4559
|
+
process.exit(0);
|
|
4494
4560
|
}
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
allowSkip: true,
|
|
4499
|
-
// Workspace remote OPTIONAL — skip OK, workspace local vẫn dùng được.
|
|
4500
|
-
hint: "Tip: sai org? Pass --repo-org=<your-gh-user>. Ho\u1EB7c switch gh: gh auth login."
|
|
4501
|
-
});
|
|
4502
|
-
if (action === "abort") {
|
|
4503
|
-
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc t\u1EA1o workspace remote.");
|
|
4561
|
+
if (err instanceof TeamPackAccessAbortedError) {
|
|
4562
|
+
log.dim(err.message);
|
|
4563
|
+
process.exit(0);
|
|
4504
4564
|
}
|
|
4505
|
-
if (
|
|
4506
|
-
log.
|
|
4507
|
-
|
|
4565
|
+
if (err instanceof RemoteAccessAbortedError) {
|
|
4566
|
+
log.dim(err.message);
|
|
4567
|
+
process.exit(0);
|
|
4568
|
+
}
|
|
4569
|
+
if (err instanceof UserAbortedRecoveryError) {
|
|
4570
|
+
log.dim(err.message);
|
|
4571
|
+
process.exit(0);
|
|
4508
4572
|
}
|
|
4573
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
4574
|
+
process.exit(1);
|
|
4509
4575
|
}
|
|
4510
|
-
}
|
|
4576
|
+
});
|
|
4511
4577
|
}
|
|
4512
|
-
async function
|
|
4513
|
-
|
|
4514
|
-
if (
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
choices.push({ name: `D\xF9ng "${alternative}" (suggest)`, value: "use-alt" });
|
|
4578
|
+
async function runInit(opts) {
|
|
4579
|
+
if (!opts.yes) printAvatarBanner({ tagline: "Kh\u1EDFi t\u1EA1o Avatar trong d\u1EF1 \xE1n c\u1EE7a b\u1EA1n" });
|
|
4580
|
+
if (opts.mode) {
|
|
4581
|
+
log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
|
|
4582
|
+
}
|
|
4583
|
+
let userConfig = await readUserConfig();
|
|
4584
|
+
while (!userConfig || isTokenExpired(userConfig)) {
|
|
4585
|
+
log.info("Ch\u01B0a \u0111\u0103ng nh\u1EADp \u2014 ch\u1EA1y login flow tr\u01B0\u1EDBc khi init...");
|
|
4586
|
+
try {
|
|
4587
|
+
await runLogin({});
|
|
4588
|
+
} catch (err) {
|
|
4589
|
+
log.warn(`Login fail: ${err.message}`);
|
|
4525
4590
|
}
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
const action = await
|
|
4529
|
-
|
|
4530
|
-
|
|
4591
|
+
userConfig = await readUserConfig();
|
|
4592
|
+
if (userConfig && !isTokenExpired(userConfig)) break;
|
|
4593
|
+
const action = await promptRetryOrSkip({
|
|
4594
|
+
taskName: "\u0110\u0103ng nh\u1EADp SSO Google",
|
|
4595
|
+
reason: "Token ch\u01B0a \u0111\u01B0\u1EE3c t\u1EA1o ho\u1EB7c \u0111\xE3 h\u1EBFt h\u1EA1n.",
|
|
4596
|
+
allowSkip: false,
|
|
4597
|
+
// Login bắt buộc, không skip được.
|
|
4598
|
+
hint: "\u0110\u1EA3m b\u1EA3o b\u1EA1n ch\u1ECDn 'Allow' tr\xEAn browser v\xE0 d\xF9ng email @nal.vn."
|
|
4531
4599
|
});
|
|
4532
4600
|
if (action === "abort") {
|
|
4533
4601
|
throw new UserAbortedRecoveryError(
|
|
4534
|
-
"User abort t\u1EA1i b\u01B0\u1EDBc
|
|
4602
|
+
"User abort t\u1EA1i b\u01B0\u1EDBc login. Ch\u1EA1y 'avatar login' tay r\u1ED3i 'avatar init' l\u1EA1i."
|
|
4535
4603
|
);
|
|
4536
4604
|
}
|
|
4537
|
-
if (action === "use-alt" && alternative) {
|
|
4538
|
-
return alternative;
|
|
4539
|
-
}
|
|
4540
|
-
const newName = await input5({
|
|
4541
|
-
message: "T\xEAn workspace m\u1EDBi:",
|
|
4542
|
-
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
4543
|
-
});
|
|
4544
|
-
const newPath = join25(parent, newName.trim());
|
|
4545
|
-
if (await isEmptyOrMissing(newPath)) return newPath;
|
|
4546
|
-
log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
|
|
4547
|
-
}
|
|
4548
|
-
}
|
|
4549
|
-
async function promptTeamOwner(currentUserEmail) {
|
|
4550
|
-
return await input5({ message: "Team owner email:", default: currentUserEmail });
|
|
4551
|
-
}
|
|
4552
|
-
async function maybeCommitWorkspace(workspacePath, skipCommit) {
|
|
4553
|
-
if (skipCommit) {
|
|
4554
|
-
log.warn("Skip commit (--no-commit). Ch\u1EA1y 'git status' + commit th\u1EE7 c\xF4ng sau.");
|
|
4555
|
-
return;
|
|
4556
|
-
}
|
|
4557
|
-
const g = git(workspacePath);
|
|
4558
|
-
await g.add(["CLAUDE.md", ".claude/", ".gitignore", ".gitmodules", "notes/", "scripts/"]);
|
|
4559
|
-
await g.commit("chore: initialize Avatar workspace");
|
|
4560
|
-
log.success("\u0110\xE3 commit workspace");
|
|
4561
|
-
}
|
|
4562
|
-
function formatAiStatusLine(aiResult) {
|
|
4563
|
-
if (aiResult === null) {
|
|
4564
|
-
return ` ${chalk.yellow("AI:")} skipped \xB7 ${chalk.cyan("avatar ai setup")} \u0111\u1EC3 config sau`;
|
|
4565
|
-
}
|
|
4566
|
-
if (aiResult.ok) {
|
|
4567
|
-
const modelPart = aiResult.model ? ` \xB7 model=${aiResult.model}` : "";
|
|
4568
|
-
return ` ${chalk.green("AI:")} ready \xB7 ${aiResult.provider}${modelPart}`;
|
|
4569
|
-
}
|
|
4570
|
-
return ` ${chalk.yellow("AI:")} failed (${aiResult.reason.slice(0, 60)}) \xB7 th\u1EED ${chalk.cyan("avatar ai setup")}`;
|
|
4571
|
-
}
|
|
4572
|
-
function formatGitnexusStatusLine(result) {
|
|
4573
|
-
if (result === null) {
|
|
4574
|
-
return ` ${chalk.yellow("GitNexus:")} skipped \xB7 ${chalk.cyan("avatar gitnexus install")} \u0111\u1EC3 setup sau`;
|
|
4575
4605
|
}
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4606
|
+
const status = opts.projectStatus ?? await promptProjectStatus();
|
|
4607
|
+
switch (status) {
|
|
4608
|
+
case "existing-remote":
|
|
4609
|
+
await runInitFromExistingRemote(opts, userConfig.email);
|
|
4610
|
+
break;
|
|
4611
|
+
case "existing-folder":
|
|
4612
|
+
await runInitFromExistingFolder(opts, userConfig.email);
|
|
4613
|
+
break;
|
|
4614
|
+
case "new-project":
|
|
4615
|
+
await runInitFromScratch(opts, userConfig.email);
|
|
4616
|
+
break;
|
|
4582
4617
|
}
|
|
4583
|
-
return ` ${chalk.yellow("GitNexus:")} skipped (${(result.reason ?? "unknown").slice(0, 40)}) \xB7 th\u1EED ${chalk.cyan("avatar gitnexus install")}`;
|
|
4584
4618
|
}
|
|
4585
|
-
async function
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
"",
|
|
4595
|
-
` ${chalk.cyan("avatar commit src")} Commit code l\xEAn client remote`,
|
|
4596
|
-
` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
|
|
4597
|
-
` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
|
|
4598
|
-
];
|
|
4599
|
-
process.stdout.write(`${boxen6(lines.join("\n"), { padding: 1, borderStyle: "round" })}
|
|
4600
|
-
`);
|
|
4601
|
-
const packDir = join25(rootPath, TEAM_PACK_RELATIVE_PATH);
|
|
4602
|
-
if (await pathExists(packDir)) {
|
|
4603
|
-
process.stdout.write(`
|
|
4604
|
-
${formatPackCommandsCheatsheetBox()}
|
|
4605
|
-
`);
|
|
4606
|
-
}
|
|
4619
|
+
async function promptProjectStatus() {
|
|
4620
|
+
return await select12({
|
|
4621
|
+
message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
|
|
4622
|
+
choices: [
|
|
4623
|
+
{ name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
|
|
4624
|
+
{ name: "2. \u0110\xE3 c\xF3 folder code local", value: "existing-folder" },
|
|
4625
|
+
{ name: "3. D\u1EF1 \xE1n m\u1EDBi ho\xE0n to\xE0n", value: "new-project" }
|
|
4626
|
+
]
|
|
4627
|
+
});
|
|
4607
4628
|
}
|
|
4608
4629
|
|
|
4609
4630
|
// src/lib/not-implemented-stub.ts
|
|
@@ -4628,7 +4649,7 @@ function registerMcpRunCommand(program2) {
|
|
|
4628
4649
|
}
|
|
4629
4650
|
|
|
4630
4651
|
// src/commands/pack-status-and-version-check.ts
|
|
4631
|
-
import { join as
|
|
4652
|
+
import { join as join27 } from "path";
|
|
4632
4653
|
import boxen7 from "boxen";
|
|
4633
4654
|
var PACK_RELATIVE_PATH = ".claude/pack";
|
|
4634
4655
|
function registerPackCommand(program2) {
|
|
@@ -4649,7 +4670,7 @@ function registerPackCommand(program2) {
|
|
|
4649
4670
|
});
|
|
4650
4671
|
}
|
|
4651
4672
|
async function gatherPackStatus(cwd, doFetch) {
|
|
4652
|
-
const packDir =
|
|
4673
|
+
const packDir = join27(cwd, PACK_RELATIVE_PATH);
|
|
4653
4674
|
if (!await pathExists(packDir) || !await isGitRepo(packDir)) {
|
|
4654
4675
|
return {
|
|
4655
4676
|
installed: false,
|
|
@@ -4739,15 +4760,15 @@ function registerSecretsCommand(program2) {
|
|
|
4739
4760
|
|
|
4740
4761
|
// src/commands/status.ts
|
|
4741
4762
|
import { promises as fs13 } from "fs";
|
|
4742
|
-
import { join as
|
|
4763
|
+
import { join as join29 } from "path";
|
|
4743
4764
|
import boxen8 from "boxen";
|
|
4744
4765
|
|
|
4745
4766
|
// src/lib/pack-backup-manager.ts
|
|
4746
4767
|
import { promises as fs12 } from "fs";
|
|
4747
|
-
import { join as
|
|
4768
|
+
import { join as join28 } from "path";
|
|
4748
4769
|
var BACKUP_DIR_NAME = "_backup";
|
|
4749
4770
|
async function listBackups(projectRoot) {
|
|
4750
|
-
const dir =
|
|
4771
|
+
const dir = join28(projectRoot, ".claude", BACKUP_DIR_NAME);
|
|
4751
4772
|
if (!await pathExists(dir)) return [];
|
|
4752
4773
|
const entries = await fs12.readdir(dir, { withFileTypes: true });
|
|
4753
4774
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
|
|
@@ -4772,7 +4793,7 @@ function registerStatusCommand(program2) {
|
|
|
4772
4793
|
}
|
|
4773
4794
|
async function gatherStatus(cwd) {
|
|
4774
4795
|
const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
|
|
4775
|
-
const claudeRoot =
|
|
4796
|
+
const claudeRoot = join29(cwd, ".claude");
|
|
4776
4797
|
const hasAvatar = await pathExists(claudeRoot);
|
|
4777
4798
|
if (!hasAvatar) {
|
|
4778
4799
|
return {
|
|
@@ -4785,8 +4806,8 @@ async function gatherStatus(cwd) {
|
|
|
4785
4806
|
hasAvatar: false
|
|
4786
4807
|
};
|
|
4787
4808
|
}
|
|
4788
|
-
const packVersion = await isGitRepo(
|
|
4789
|
-
const pendingDir =
|
|
4809
|
+
const packVersion = await isGitRepo(join29(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
|
|
4810
|
+
const pendingDir = join29(claudeRoot, "_pending");
|
|
4790
4811
|
const pendingCount = await pathExists(pendingDir) ? (await fs13.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
|
|
4791
4812
|
const backupCount = (await listBackups(cwd)).length;
|
|
4792
4813
|
const techStackSummary = await readTechStackFirstLine(claudeRoot);
|
|
@@ -4801,7 +4822,7 @@ async function gatherStatus(cwd) {
|
|
|
4801
4822
|
};
|
|
4802
4823
|
}
|
|
4803
4824
|
async function readTechStackFirstLine(claudeRoot) {
|
|
4804
|
-
const techStackPath =
|
|
4825
|
+
const techStackPath = join29(claudeRoot, "project", "tech-stack.md");
|
|
4805
4826
|
if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
|
|
4806
4827
|
const content = await readText(techStackPath);
|
|
4807
4828
|
const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
|
|
@@ -4822,13 +4843,13 @@ function renderStatusBox(s) {
|
|
|
4822
4843
|
}
|
|
4823
4844
|
|
|
4824
4845
|
// src/commands/sync.ts
|
|
4825
|
-
import { join as
|
|
4846
|
+
import { join as join31 } from "path";
|
|
4826
4847
|
|
|
4827
4848
|
// src/lib/preview-team-pack-sync-changes-for-dry-run.ts
|
|
4828
|
-
import { join as
|
|
4849
|
+
import { join as join30 } from "path";
|
|
4829
4850
|
async function inspectMountDir(packDir, claudeDir, dir) {
|
|
4830
|
-
const source =
|
|
4831
|
-
const dest =
|
|
4851
|
+
const source = join30(packDir, dir);
|
|
4852
|
+
const dest = join30(claudeDir, dir);
|
|
4832
4853
|
if (!await pathExists(source)) return "source-missing";
|
|
4833
4854
|
if (!await pathExists(dest)) return "needs-creation";
|
|
4834
4855
|
const { promises: fs14 } = await import("fs");
|
|
@@ -4870,15 +4891,14 @@ async function buildSyncPreview(packDir, claudeDir, targetVersion) {
|
|
|
4870
4891
|
var DEFAULT_PACK_BRANCH2 = "main";
|
|
4871
4892
|
async function syncAction(opts) {
|
|
4872
4893
|
const projectRoot = process.cwd();
|
|
4873
|
-
const claudeDir =
|
|
4874
|
-
const packDir =
|
|
4894
|
+
const claudeDir = join31(projectRoot, ".claude");
|
|
4895
|
+
const packDir = join31(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
4875
4896
|
if (!await pathExists(packDir)) {
|
|
4876
4897
|
log.error(
|
|
4877
4898
|
`team-ai-pack submodule ch\u01B0a \u0111\u01B0\u1EE3c kh\u1EDFi t\u1EA1o \u1EDF ${TEAM_PACK_RELATIVE_PATH}/.
|
|
4878
4899
|
Ch\u1EA1y 'avatar init' \u0111\u1EC3 add submodule, ho\u1EB7c 'git submodule update --init' n\u1EBFu \u0111\xE3 clone repo.`
|
|
4879
4900
|
);
|
|
4880
4901
|
process.exit(1);
|
|
4881
|
-
return;
|
|
4882
4902
|
}
|
|
4883
4903
|
try {
|
|
4884
4904
|
await git(packDir).fetch(["--tags", "origin"]);
|
|
@@ -4901,7 +4921,6 @@ async function syncAction(opts) {
|
|
|
4901
4921
|
Pass --version <tag> r\xF5 r\xE0ng, ho\u1EB7c d\xF9ng --latest \u0111\u1EC3 pull HEAD branch ${DEFAULT_PACK_BRANCH2}.`
|
|
4902
4922
|
);
|
|
4903
4923
|
process.exit(1);
|
|
4904
|
-
return;
|
|
4905
4924
|
}
|
|
4906
4925
|
targetVersion = picked;
|
|
4907
4926
|
}
|
|
@@ -5014,27 +5033,27 @@ import boxen9 from "boxen";
|
|
|
5014
5033
|
// src/lib/create-uninstall-backup-snapshot.ts
|
|
5015
5034
|
import { cp, mkdir, writeFile } from "fs/promises";
|
|
5016
5035
|
import { homedir as homedir4 } from "os";
|
|
5017
|
-
import { basename as
|
|
5018
|
-
var UNINSTALL_BACKUPS_DIR =
|
|
5036
|
+
import { basename as basename3, join as join32 } from "path";
|
|
5037
|
+
var UNINSTALL_BACKUPS_DIR = join32(homedir4(), ".avatar", "uninstall-backups");
|
|
5019
5038
|
async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersion) {
|
|
5020
|
-
const projectName =
|
|
5039
|
+
const projectName = basename3(projectRoot);
|
|
5021
5040
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5022
|
-
const backupDir =
|
|
5041
|
+
const backupDir = join32(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp2}`);
|
|
5023
5042
|
await mkdir(backupDir, { recursive: true, mode: 448 });
|
|
5024
5043
|
if (artifacts.claudeDir) {
|
|
5025
|
-
await cp(artifacts.claudeDir,
|
|
5044
|
+
await cp(artifacts.claudeDir, join32(backupDir, ".claude"), { recursive: true });
|
|
5026
5045
|
}
|
|
5027
5046
|
if (artifacts.claudeMd) {
|
|
5028
|
-
await cp(artifacts.claudeMd,
|
|
5047
|
+
await cp(artifacts.claudeMd, join32(backupDir, "CLAUDE.md"));
|
|
5029
5048
|
}
|
|
5030
5049
|
if (artifacts.postMergeHook || artifacts.prePushHook) {
|
|
5031
|
-
const hooksBackupDir =
|
|
5050
|
+
const hooksBackupDir = join32(backupDir, "hooks");
|
|
5032
5051
|
await mkdir(hooksBackupDir, { recursive: true });
|
|
5033
5052
|
if (artifacts.postMergeHook) {
|
|
5034
|
-
await cp(artifacts.postMergeHook,
|
|
5053
|
+
await cp(artifacts.postMergeHook, join32(hooksBackupDir, "post-merge"));
|
|
5035
5054
|
}
|
|
5036
5055
|
if (artifacts.prePushHook) {
|
|
5037
|
-
await cp(artifacts.prePushHook,
|
|
5056
|
+
await cp(artifacts.prePushHook, join32(hooksBackupDir, "pre-push"));
|
|
5038
5057
|
}
|
|
5039
5058
|
}
|
|
5040
5059
|
const manifest = {
|
|
@@ -5049,27 +5068,27 @@ async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersi
|
|
|
5049
5068
|
prePushHook: !!artifacts.prePushHook
|
|
5050
5069
|
}
|
|
5051
5070
|
};
|
|
5052
|
-
await writeFile(
|
|
5071
|
+
await writeFile(join32(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
5053
5072
|
return backupDir;
|
|
5054
5073
|
}
|
|
5055
5074
|
|
|
5056
5075
|
// src/lib/detect-avatar-project-artifacts.ts
|
|
5057
5076
|
import { existsSync as existsSync9 } from "fs";
|
|
5058
|
-
import { join as
|
|
5077
|
+
import { join as join33 } from "path";
|
|
5059
5078
|
function existsOrNull(path) {
|
|
5060
5079
|
return existsSync9(path) ? path : null;
|
|
5061
5080
|
}
|
|
5062
5081
|
function detectAvatarProjectArtifacts(projectRoot) {
|
|
5063
|
-
const claudeDir = existsOrNull(
|
|
5064
|
-
const claudeMd = existsOrNull(
|
|
5065
|
-
const postMergeHook = existsOrNull(
|
|
5082
|
+
const claudeDir = existsOrNull(join33(projectRoot, ".claude"));
|
|
5083
|
+
const claudeMd = existsOrNull(join33(projectRoot, "CLAUDE.md"));
|
|
5084
|
+
const postMergeHook = existsOrNull(join33(projectRoot, ".git", "hooks", "post-merge"));
|
|
5066
5085
|
const prePushHook = existsOrNull(
|
|
5067
|
-
|
|
5086
|
+
join33(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
|
|
5068
5087
|
);
|
|
5069
|
-
const gitignorePath = existsOrNull(
|
|
5070
|
-
const gitmodulesPath = existsOrNull(
|
|
5071
|
-
const notesDir = existsOrNull(
|
|
5072
|
-
const scriptsDir = existsOrNull(
|
|
5088
|
+
const gitignorePath = existsOrNull(join33(projectRoot, ".gitignore"));
|
|
5089
|
+
const gitmodulesPath = existsOrNull(join33(projectRoot, ".gitmodules"));
|
|
5090
|
+
const notesDir = existsOrNull(join33(projectRoot, "notes"));
|
|
5091
|
+
const scriptsDir = existsOrNull(join33(projectRoot, "scripts"));
|
|
5073
5092
|
const hasAnyArtifact = !!(claudeDir || claudeMd || postMergeHook || prePushHook);
|
|
5074
5093
|
return {
|
|
5075
5094
|
hasAnyArtifact,
|
|
@@ -5090,11 +5109,11 @@ async function executeUninstallDeletion(artifacts, flags) {
|
|
|
5090
5109
|
if (artifacts.claudeDir) {
|
|
5091
5110
|
if (flags.keepSubmodule) {
|
|
5092
5111
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
5093
|
-
const { join:
|
|
5112
|
+
const { join: join34 } = await import("path");
|
|
5094
5113
|
const entries = await readdir2(artifacts.claudeDir);
|
|
5095
5114
|
for (const entry of entries) {
|
|
5096
5115
|
if (entry === "pack") continue;
|
|
5097
|
-
await rm(
|
|
5116
|
+
await rm(join34(artifacts.claudeDir, entry), { recursive: true, force: true });
|
|
5098
5117
|
}
|
|
5099
5118
|
} else {
|
|
5100
5119
|
await rm(artifacts.claudeDir, { recursive: true, force: true });
|