@nalvietnam/avatar-cli 1.11.0 → 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 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" && stderr.trim()) {
307
- log.dim(`[debug] claude --print stderr: ${stderr.slice(0, 500)}`);
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 ? match[1] : null;
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
- log.info(`Auto-pick model: ${models[0]} (ch\u1EC9 1 model available)`);
560
- return models[0];
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
- log.info(`Auto-pick model: ${claudeAliases[0]} (ch\u1EC9 1 claude alias tr\xEAn endpoint)`);
649
- return claudeAliases[0];
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, input6) {
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 (input6.provider) {
755
+ switch (input8.provider) {
741
756
  case "subscription":
742
- merged = applySubscription(existing, input6.model);
757
+ merged = applySubscription(existing, input8.model);
743
758
  break;
744
759
  case "llmlite":
745
- merged = applyLLMLite(existing, input6.apiKey, input6.baseUrl, input6.model);
760
+ merged = applyLLMLite(existing, input8.apiKey, input8.baseUrl, input8.model);
746
761
  break;
747
762
  case "anthropic":
748
- merged = applyAnthropic(existing, input6.apiKey, input6.baseUrl, input6.model);
763
+ merged = applyAnthropic(existing, input8.apiKey, input8.baseUrl, input8.model);
749
764
  break;
750
765
  case "use-global":
751
- merged = applyUseGlobal(existing, input6.sourceSettings);
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
- log.dim(`\u2192 ${getQuotaErrorHint(reason)}`);
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, {
@@ -1572,7 +1590,7 @@ async function runChecks(cwd) {
1572
1590
  if (settings.statusLine?.command) {
1573
1591
  const cmd = settings.statusLine.command.trim();
1574
1592
  const match = cmd.match(/^(node|python|python3|bash|sh)\s+([^\s]+)/);
1575
- if (match) {
1593
+ if (match?.[2]) {
1576
1594
  const refFile = match[2];
1577
1595
  const fullPath = refFile.startsWith("/") ? refFile : join11(cwd, refFile);
1578
1596
  const fileExists = await pathExists(fullPath);
@@ -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 ? match[1] : null;
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 { basename, join as join25, relative as relative3, resolve as resolve2 } from "path";
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/add-team-pack-submodule-with-retry-on-network-fail.ts
2319
- import { spawnSync as spawnSync13 } from "child_process";
2320
- import { select as select6 } from "@inquirer/prompts";
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
- // src/lib/team-pack-submodule-manager.ts
2323
- import { join as join16 } from "path";
2392
+ `);
2393
+ }
2324
2394
 
2325
- // src/lib/check-team-pack-access-with-retry-loop.ts
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
- import { confirm as confirm4, select as select5 } from "@inquirer/prompts";
2328
- import boxen3 from "boxen";
2329
- function parseRepoSlugFromGitUrl(url) {
2330
- const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
2331
- if (httpsMatch) return httpsMatch[1];
2332
- return null;
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 checkRepoAccess(repoSlug) {
2335
- const r = spawnSync12("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
2336
- return r.status === 0;
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 = spawnSync12("gh", ["api", "user", "--jq", ".login"], {
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 = spawnSync12("gh", ["auth", "login", "--web"], { stdio: "inherit" });
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}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
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
- async function copyInfoToClipboardWithConsent(info) {
2354
- const ok = await confirm4({
2355
- message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
2356
- default: true
2357
- });
2358
- if (!ok) return;
2359
- try {
2360
- const { default: clipboardy } = await import("clipboardy");
2361
- await clipboardy.write(info);
2362
- log.success("\u0110\xE3 copy v\xE0o clipboard");
2363
- } catch (err) {
2364
- log.dim(`Copy clipboard fail: ${err.message}`);
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 printAccessWarningBox(repoSlug, ghUser, ssoEmail) {
2368
- const lines = [
2369
- `${chalk.red("\u26D4 KH\xD4NG C\xD3 QUY\u1EC0N ACCESS")}`,
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 ensureTeamPackAccessWithRetry(args) {
2397
- if (checkRepoAccess(args.repoSlug)) return true;
2398
- const initialGhUser = getCurrentGhUser();
2399
- printAccessWarningBox(args.repoSlug, initialGhUser, args.ssoEmail ?? null);
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
- const ghUserDisplay = ghUser ?? "(ch\u01B0a gh auth)";
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: `\u0110\xE3 \u0111\u01B0\u1EE3c grant access v\u1EDBi GitHub username '${ghUserDisplay}' \u2014 ki\u1EC3m tra l\u1EA1i`,
2411
- value: "retry-same"
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: "\u0110\xE3 grant v\u1EDBi GitHub account kh\xE1c \u2014 switch gh (m\u1EDF browser)",
2415
- value: "switch-account"
2489
+ name: "Switch GitHub account (gh auth login \u2014 m\u1EDF browser)",
2490
+ value: "switch"
2416
2491
  },
2417
2492
  {
2418
- name: "T\u1EA1m ng\u01B0ng \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau",
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
- log.dim("T\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\xE3 accept invite t\u1EEB GitHub.");
2425
- return false;
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 === "switch-account") {
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("Ki\u1EC3m tra access...");
2431
- if (checkRepoAccess(args.repoSlug)) {
2432
- const finalUser = getCurrentGhUser();
2433
- log.success(`\u0110\xE3 c\xF3 access v\u1EDBi '${finalUser ?? "(unknown)"}' \u2014 ti\u1EBFp t\u1EE5c.`);
2434
- return true;
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
- log.warn(
2437
- `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.`
2438
- );
2524
+ reason = result.reason ?? "unknown";
2525
+ detail = result.detail;
2439
2526
  }
2440
2527
  }
2441
2528
 
2442
- // src/lib/pick-latest-stable-semver-tag.ts
2443
- var SEMVER_REGEX3 = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
2444
- function parseSemVerTag(tag) {
2445
- const match = tag.match(SEMVER_REGEX3);
2446
- if (!match) return null;
2447
- const [, major, minor, patch, prerelease] = match;
2448
- return {
2449
- raw: tag,
2450
- major: Number.parseInt(major ?? "0", 10),
2451
- minor: Number.parseInt(minor ?? "0", 10),
2452
- patch: Number.parseInt(patch ?? "0", 10),
2453
- prerelease: prerelease ?? null
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/resolve-team-pack-repo-url.ts
2468
- var ORG_DEFAULT = "git@github.com:nalvn/team-ai-pack.git";
2469
- function resolveTeamPackRepoUrl() {
2470
- if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
2471
- return process.env.AVATAR_TEAM_PACK_REPO_URL;
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/team-pack-submodule-manager.ts
2477
- var TEAM_PACK_REPO_URL = resolveTeamPackRepoUrl();
2478
- var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
2479
- var TeamPackAccessAbortedError = class extends Error {
2480
- constructor(message) {
2481
- super(message);
2482
- this.name = "TeamPackAccessAbortedError";
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
- var DEFAULT_PACK_BRANCH = "main";
2486
- async function addTeamPackSubmodule(projectRoot, tag, ssoEmail, latest = false) {
2487
- const url = resolveTeamPackRepoUrl();
2488
- const repoSlug = parseRepoSlugFromGitUrl(url);
2489
- if (repoSlug) {
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
- try {
2498
- await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
2499
- } catch (err) {
2500
- const msg = err instanceof Error ? err.message : String(err);
2501
- if (msg.includes("Repository not found") || msg.includes("not found")) {
2502
- log.error(
2503
- `Repo team-ai-pack kh\xF4ng t\u1ED3n t\u1EA1i: ${url}
2504
- C\xE1ch fix:
2505
- 1. T\u1EA1o repo: gh repo create <owner>/team-ai-pack --private --add-readme
2506
- 2. Ho\u1EB7c override URL: export AVATAR_TEAM_PACK_REPO_URL=<url-repo-c\u1EE7a-b\u1EA1n>
2507
- 3. Ho\u1EB7c d\xF9ng flag --skip-team-pack`
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
- return { pinnedTag: target };
2613
+ throw new Error(`Kh\xF4ng t\xECm th\u1EA5y template gitignore cho stack "${stack}"`);
2527
2614
  }
2528
- async function readPinnedPackVersion(projectRoot) {
2529
- const submoduleRoot = join16(projectRoot, TEAM_PACK_RELATIVE_PATH);
2530
- const tag = await tagAtHead(submoduleRoot);
2531
- if (tag) return tag;
2532
- const sha = await currentCommitSha(submoduleRoot);
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/add-team-pack-submodule-with-retry-on-network-fail.ts
2537
- function isSshPermissionError(message) {
2538
- const text = message.toLowerCase();
2539
- return text.includes("permission denied (publickey)") || text.includes("publickey)") || text.includes("ssh: could not resolve") || text.includes("host key verification failed");
2540
- }
2541
- function triggerGhAuthLoginInteractive2() {
2542
- log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
2543
- const r = spawnSync13("gh", ["auth", "login", "--web"], { stdio: "inherit" });
2544
- if (r.status !== 0) {
2545
- log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
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
- function openGithubSshKeysPage() {
2549
- log.info("M\u1EDF trang GitHub Settings \u2192 SSH Keys...");
2550
- const r = spawnSync13("open", ["https://github.com/settings/keys"], { stdio: "ignore" });
2551
- if (r.status !== 0) {
2552
- log.info("URL: https://github.com/settings/keys");
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 handleSshPermissionError() {
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: "SSH permission denied. C\xE1ch x\u1EED l\xFD?",
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
- name: "Switch GitHub account (gh auth login \u2014 m\u1EDF browser)",
2561
- value: "switch"
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
- name: "T\xF4i v\u1EEBa add SSH key l\xEAn GitHub \u2014 retry",
2569
- value: "retry"
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
- name: "B\u1ECF qua team-ai-pack (d\xF9ng avatar sync sau)",
2573
- value: "skip"
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
- name: "T\u1EA1m ng\u01B0ng init \u2014 fix SSH key tay r\u1ED3i ch\u1EA1y l\u1EA1i",
2577
- value: "abort"
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 addTeamPackSubmoduleWithRetryOnNetworkFail(projectRoot, tag, ssoEmail, latest = false) {
2583
- while (true) {
2584
- try {
2585
- const result = await addTeamPackSubmodule(projectRoot, tag, ssoEmail, latest);
2586
- return { pinnedTag: result.pinnedTag, skipped: false };
2587
- } catch (err) {
2588
- if (err instanceof TeamPackAccessAbortedError) throw err;
2589
- const message = err instanceof Error ? err.message : String(err);
2590
- if (isSshPermissionError(message)) {
2591
- log.warn("Pull team-ai-pack th\u1EA5t b\u1EA1i: SSH permission denied (publickey).");
2592
- log.dim(
2593
- " \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."
2594
- );
2595
- const action2 = await handleSshPermissionError();
2596
- if (action2 === "abort") {
2597
- throw new UserAbortedRecoveryError(
2598
- "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'."
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
- // src/lib/avatar-ascii-banner.ts
2639
- import chalk2 from "chalk";
2640
- var BANNER_LINES = [
2641
- " \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 ",
2642
- "\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",
2643
- "\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",
2644
- "\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",
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 gradientAt(t) {
2662
- const clamped = Math.max(0, Math.min(1, t));
2663
- const scaled = clamped * (GRADIENT_STOPS.length - 1);
2664
- const lo = Math.floor(scaled);
2665
- const hi = Math.min(GRADIENT_STOPS.length - 1, lo + 1);
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 renderAvatarBanner(opts) {
2676
- const isTty = process.stdout.isTTY ?? false;
2677
- const supportsColor = isTty && chalk2.level > 0;
2678
- if (!supportsColor) {
2679
- return [...BANNER_LINES, ...opts?.tagline ? ["", opts.tagline] : []].join("\n");
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
- const colored = BANNER_LINES.map((line, idx) => {
2682
- const t = BANNER_LINES.length === 1 ? 0 : idx / (BANNER_LINES.length - 1);
2683
- const [r, g, b] = gradientAt(t);
2684
- return chalk2.rgb(r, g, b).bold(line);
2685
- });
2686
- if (opts?.tagline) {
2687
- colored.push("");
2688
- colored.push(chalk2.dim(opts.tagline));
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
- return colored.join("\n");
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/execute-gh-repo-create.ts
2804
+ // src/lib/check-team-pack-access-with-retry-loop.ts
2700
2805
  import { spawnSync as spawnSync14 } from "child_process";
2701
- var RepoAlreadyExistsError = class extends Error {
2702
- constructor(fullName) {
2703
- super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
2704
- this.name = "RepoAlreadyExistsError";
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
- // src/lib/resolve-github-username-default.ts
2734
- import { spawnSync as spawnSync15 } from "child_process";
2735
- function resolveGithubUsernameDefault() {
2736
- const r = spawnSync15("gh", ["api", "user", "--jq", ".login"], {
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
- throw new Error(`Kh\xF4ng l\u1EA5y \u0111\u01B0\u1EE3c GitHub username: ${r.stderr?.trim()}`);
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 validateRepoVisibility(v) {
2762
- if (v !== "private" && v !== "public") {
2763
- throw new Error(`Visibility ph\u1EA3i l\xE0 "private" ho\u1EB7c "public", nh\u1EADn: "${v}"`);
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
- // src/lib/create-github-remote-from-folder.ts
2768
- function createGithubRemoteFromFolder(input6) {
2769
- validateRepoName(input6.name);
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
- log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
2780
- return urls;
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
- // src/lib/create-workspace-remote-via-gh.ts
2784
- import { spawnSync as spawnSync23 } from "child_process";
2785
-
2786
- // src/lib/check-gh-cli-auth-status.ts
2787
- import { spawnSync as spawnSync16 } from "child_process";
2788
- function checkGhCliAuthStatus() {
2789
- const r = spawnSync16("gh", ["auth", "status"], { stdio: "ignore" });
2790
- if (r.error && r.error.code === "ENOENT") {
2791
- return "not-installed";
2792
- }
2793
- return r.status === 0 ? "authenticated" : "not-authenticated";
2794
- }
2795
-
2796
- // src/lib/detect-package-manager.ts
2797
- import { spawnSync as spawnSync17 } from "child_process";
2798
- function hasBinary(name) {
2799
- const platform2 = detectHostPlatform();
2800
- const probe = platform2 === "win32" ? "where" : "command";
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 isValidGitUrl(url) {
2889
- const trimmed = url.trim();
2890
- if (!trimmed) return false;
2891
- return /^https?:\/\/[\w.@/-]+$/.test(trimmed) || /^git@[\w.-]+:[\w./-]+\.git$/.test(trimmed) || /^[\w.-]+\/[\w.-]+$/.test(trimmed);
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 handleRemoteAccessFailureWithAccountSwitch(args) {
2894
- let currentUrl = args.url;
2895
- let reason = args.initialReason;
2896
- let detail = args.initialDetail;
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
- log.warn(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c ${currentUrl}`);
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: "Nh\u1EADp l\u1EA1i URL \u0111\xFAng (recommended khi URL sai ch\xEDnh t\u1EA3)",
2908
- value: "re-input-url"
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: "T\xF4i v\u1EEBa fix (accept invite / s\u1EEDa permission) \u2014 retry verify",
2916
- value: "retry"
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 init \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau",
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
- throw new RemoteAccessAbortedError(
2926
- `User ch\u1ECDn t\u1EA1m ng\u01B0ng. Fix access ${currentUrl} r\u1ED3i ch\u1EA1y l\u1EA1i 'avatar init'.`
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
- triggerGhAuthLoginInteractive3();
2905
+ if (action === "switch-account") {
2906
+ triggerGhAuthLoginInteractive2();
2939
2907
  }
2940
- log.info(`Verify remote l\u1EA1i: ${currentUrl}...`);
2941
- const result = tryVerifyGitRemoteAccessible(currentUrl);
2942
- if (result.ok) {
2943
- log.success(`Remote accessible: ${currentUrl}`);
2944
- return { resolvedUrl: currentUrl };
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
- reason = result.reason ?? "unknown";
2947
- detail = result.detail;
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/install-gh-cli-via-package-manager.ts
2952
- import { spawnSync as spawnSync20 } from "child_process";
2953
- var INSTALL_COMMANDS = {
2954
- brew: { cmd: "brew", args: ["install", "gh"] },
2955
- apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
2956
- dnf: { cmd: "sudo", args: ["dnf", "install", "-y", "gh"] },
2957
- pacman: { cmd: "sudo", args: ["pacman", "-S", "--noconfirm", "github-cli"] },
2958
- winget: { cmd: "winget", args: ["install", "--id", "GitHub.cli", "-e", "--silent"] }
2959
- };
2960
- function installGhCliViaPackageManager(pm) {
2961
- const spec = INSTALL_COMMANDS[pm];
2962
- log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
2963
- const r = spawnSync20(spec.cmd, spec.args, { stdio: "inherit" });
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
- // src/lib/setup-git-credential-via-gh.ts
2971
- import { spawnSync as spawnSync21 } from "child_process";
2972
- function setupGitCredentialViaGh() {
2973
- const r = spawnSync21("gh", ["auth", "setup-git"], { stdio: "ignore" });
2974
- if (r.status !== 0) {
2975
- log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
2976
- return;
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/trigger-gh-cli-auth-login.ts
2982
- import { spawnSync as spawnSync22 } from "child_process";
2983
- function triggerGhCliAuthLogin() {
2984
- log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
2985
- const r = spawnSync22(
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
- log.success("\u0110\xE3 \u0111\u0103ng nh\u1EADp GitHub");
2951
+ return ORG_DEFAULT;
2994
2952
  }
2995
2953
 
2996
- // src/lib/git-auth-and-install-orchestrator.ts
2997
- async function ensureGitHubReady(remoteUrl) {
2998
- while (checkGhCliAuthStatus() === "not-installed") {
2999
- log.warn("gh CLI ch\u01B0a c\xE0i. Avatar s\u1EBD t\u1EF1 c\xE0i.");
3000
- const pm = detectPackageManager();
3001
- if (!pm) {
3002
- const action = await promptRetryOrSkip({
3003
- taskName: "Ph\xE1t hi\u1EC7n package manager",
3004
- reason: "Kh\xF4ng t\xECm th\u1EA5y brew/apt/dnf/pacman/winget tr\xEAn m\xE1y.",
3005
- allowSkip: false,
3006
- hint: "C\xE0i gh CLI tay (https://cli.github.com) r\u1ED3i ch\u1ECDn Retry."
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/create-workspace-remote-via-gh.ts
3074
- var CreateWorkspaceRemoteError = class extends Error {
3075
- reason;
3076
- fullName;
3077
- stderr;
3078
- constructor(reason, fullName, message, stderr) {
3079
- super(message);
3080
- this.name = "CreateWorkspaceRemoteError";
3081
- this.reason = reason;
3082
- this.fullName = fullName;
3083
- this.stderr = stderr;
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
- if (text.includes("could not resolve") || text.includes("network") || text.includes("connection refused")) {
3098
- return "network";
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 repoExistsOnGitHub(fullName) {
3103
- const r = spawnSync23("gh", ["repo", "view", fullName, "--json", "name"], {
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(input6) {
3134
- validateRepoName(input6.workspaceName);
3135
- validateRepoVisibility(input6.visibility);
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 = input6.org ?? ghUser;
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}/${input6.workspaceName}`;
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} (${input6.visibility})...`);
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
- `--${input6.visibility}`,
3541
+ `--${input8.visibility}`,
3159
3542
  "--source",
3160
- input6.workspacePath,
3543
+ input8.workspacePath,
3161
3544
  "--remote",
3162
3545
  "origin",
3163
3546
  "--push"
@@ -3207,53 +3590,17 @@ 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 join17 } from "path";
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]+)/);
3252
- if (!match) {
3599
+ if (!match?.[2]) {
3253
3600
  return true;
3254
3601
  }
3255
3602
  const filePath = match[2];
3256
- const fullPath = filePath.startsWith("/") ? filePath : join17(workspacePath, 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 = join17(workspacePath, ".claude", "pack", "templates", "settings.json.tpl");
3291
- const projectSettingsPath = join17(workspacePath, ".claude", "settings.json");
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/safe-bootstrap-for-dirty-folder.ts
3392
- import { readdirSync } from "fs";
3393
- import { select as select8 } from "@inquirer/prompts";
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(dirname5(dest), dest) || dest;
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(dirname5(dest), source);
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(dirname5(dest), source);
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-conflict-detection-helpers.ts
3730
- import { readdir } from "fs/promises";
3731
- import { join as join23 } from "path";
3732
- async function isEmptyOrMissing(path) {
3733
- if (!await pathExists(path)) return true;
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/lib/read-cli-version-from-package-json.ts
3751
- import { readFileSync as readFileSync5 } from "fs";
3752
- import { dirname as dirname6, join as join24, resolve } from "path";
3753
- import { fileURLToPath as fileURLToPath3 } from "url";
3754
- var cachedVersion = null;
3755
- function readCliVersion() {
3756
- if (cachedVersion !== null) return cachedVersion;
3757
- const here = dirname6(fileURLToPath3(import.meta.url));
3758
- for (let i = 0; i < 5; i++) {
3759
- const candidate = resolve(here, ...Array(i).fill(".."), "package.json");
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
- if (i === 0) {
3770
- const sibling = join24(here, "..", "package.json");
3771
- try {
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 buildScaffoldVariables(args) {
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
- // src/commands/login.ts
3841
- import boxen5 from "boxen";
3842
- import open from "open";
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
- return await res.json();
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
- async function pollForToken(deviceCode) {
3869
- const body = new URLSearchParams({
3870
- client_id: GOOGLE_CLIENT_ID,
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 (errorCode === "authorization_pending" || errorCode === "slow_down") {
3891
- return null;
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
- if (errorCode === "access_denied") {
3894
- throw new Error("User t\u1EEB ch\u1ED1i quy\u1EC1n truy c\u1EADp");
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 (errorCode === "expired_token") {
3897
- throw new Error("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
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
- throw new Error(`OAuth token endpoint tr\u1EA3 l\u1ED7i: ${errorCode || res.status}`);
3938
+ return ` ${chalk.yellow("GitNexus:")} skipped (${(result.reason ?? "unknown").slice(0, 40)}) \xB7 th\u1EED ${chalk.cyan("avatar gitnexus install")}`;
3900
3939
  }
3901
- function decodeIdToken(idToken) {
3902
- const parts = idToken.split(".");
3903
- if (parts.length !== 3) {
3904
- throw new Error("id_token format kh\xF4ng h\u1EE3p l\u1EC7");
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
- function verifyHostedDomain(claims) {
3913
- if (claims.hd !== HOSTED_DOMAIN) {
3914
- throw new Error(
3915
- `Email kh\xF4ng thu\u1ED9c workspace NAL (y\xEAu c\u1EA7u @${HOSTED_DOMAIN}). Nh\u1EADn: ${claims.email}`
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
- if (!claims.email_verified) {
3919
- throw new Error("Email ch\u01B0a \u0111\u01B0\u1EE3c Google verify");
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
- function buildUserConfig(token, claims) {
3923
- const expiresAt = new Date(Date.now() + token.expires_in * 1e3).toISOString();
3924
- return {
3925
- email: claims.email,
3926
- name: claims.name ?? claims.email,
3927
- access_token: token.access_token,
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
- function buildVerificationUrl(response) {
3943
- const url = new URL(response.verification_url);
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 runLogin(opts) {
3961
- printAvatarBanner({ tagline: "\u0110\u0103ng nh\u1EADp Google SSO \xB7 workspace @nal.vn" });
3962
- if (opts.reset) {
3963
- await clearUserConfig();
3964
- await appendAuditEntry("login_reset");
3965
- } else {
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
- deviceCode = await requestDeviceCode();
3976
- deviceSpinner.succeed("Nh\u1EADn device code");
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
- deviceSpinner.fail("Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c Google");
4039
+ sp.fail("Init workspace th\u1EA5t b\u1EA1i");
3979
4040
  throw err;
3980
4041
  }
3981
- const verificationUrl = buildVerificationUrl(deviceCode);
3982
- const instructions = [
3983
- `1. Truy c\u1EADp: ${chalk.cyan(deviceCode.verification_url)}`,
3984
- `2. Nh\u1EADp code: ${chalk.bold.yellow(deviceCode.user_code)}`,
3985
- "",
3986
- `Ho\u1EB7c Avatar t\u1EF1 m\u1EDF browser, click ${chalk.green("Allow")}...`
3987
- ].join("\n");
3988
- process.stdout.write(`${boxen5(instructions, { padding: 1, borderStyle: "round" })}
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
- const waitSpinner = spinner("\u0110ang ch\u1EDD x\xE1c nh\u1EADn trong browser...");
3994
- const intervalMs = deviceCode.interval * 1e3;
3995
- const deadline = Date.now() + deviceCode.expires_in * 1e3;
3996
- let token = null;
3997
- while (Date.now() < deadline) {
3998
- await sleep(intervalMs);
3999
- try {
4000
- token = await pollForToken(deviceCode.device_code);
4001
- if (token) break;
4002
- } catch (err) {
4003
- waitSpinner.fail("X\xE1c th\u1EF1c th\u1EA5t b\u1EA1i");
4004
- throw err;
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 (!token) {
4008
- waitSpinner.fail("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
4009
- process.exit(1);
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
- waitSpinner.succeed("\u0110\xE3 nh\u1EADn token t\u1EEB Google");
4012
- const claims = decodeIdToken(token.id_token);
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
- verifyHostedDomain(claims);
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
- await revokeToken(token.access_token);
4017
- throw err;
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
- // src/commands/init.ts
4031
- function parseBootstrapStrategyOpts(opts) {
4032
- if (opts.preserveUncommitted) return "stash";
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
- throw new Error(
4039
- `--bootstrap-strategy kh\xF4ng h\u1EE3p l\u1EC7: ${opts.bootstrapStrategy}. Ch\u1ECDn: ${valid.join(" | ")}`
4040
- );
4041
- }
4042
- function registerInitCommand(program2) {
4043
- 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(
4044
- "--gitnexus-skip",
4045
- "B\u1ECF qua phase GitNexus setup (M10 \u2014 ch\u1EA1y `avatar gitnexus install` sau)"
4046
- ).option(
4047
- "--bootstrap-strategy <s>",
4048
- "X\u1EED l\xFD folder dirty: stash | commit-all | skip | branch (default: prompt)"
4049
- ).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) => {
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 runInit(opts);
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 InitAbortedByUserError) {
4054
- log.dim(err.message);
4055
- process.exit(0);
4056
- }
4057
- if (err instanceof TeamPackAccessAbortedError) {
4058
- log.dim(err.message);
4059
- process.exit(0);
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
- if (err instanceof RemoteAccessAbortedError) {
4062
- log.dim(err.message);
4063
- process.exit(0);
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 (err instanceof UserAbortedRecoveryError) {
4066
- log.dim(err.message);
4067
- process.exit(0);
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 input5({
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 input5({ message: "T\xEAn workspace:", default: inferredName });
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 input5({
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 ?? `${basename(folderPath)}-avatar-workspace`;
4171
- const workspaceName = opts.workspaceName ?? await input5({ message: "T\xEAn workspace:", default: inferredName });
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 input5({
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 select9({
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 = join25(workspacePath, "src");
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: args.workspacePath,
4322
- workspaceName: args.workspaceName,
4323
- teamOwner: args.teamOwner,
4324
- description: args.description,
4335
+ workspacePath,
4336
+ workspaceName: projectName,
4337
+ teamOwner,
4338
+ description: opts.description ?? `D\u1EF1 \xE1n m\u1EDBi: ${projectName}`,
4325
4339
  packVersion: pinnedTag,
4326
- autoYes: args.autoYes,
4327
- skipCommit: args.skipCommit,
4328
- createWorkspaceRemote: args.createWorkspaceRemote,
4329
- repoVisibility: args.repoVisibility,
4330
- repoOrg: args.repoOrg,
4331
- flow: args.flow,
4332
- aiSkip: args.aiSkip,
4333
- gitnexusSkip: args.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
- async function finalizeWorkspaceScaffold(args) {
4341
- const vars = buildScaffoldVariables({
4342
- projectName: args.workspaceName,
4343
- projectDescription: args.description,
4344
- teamOwner: args.teamOwner,
4345
- packVersion: args.packVersion,
4346
- mode: "client"
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 createClaudeDirTree(args.workspacePath);
4349
- await writeProjectKnowledgeFiles(args.workspacePath, vars);
4350
- await writeRootClaudeMd(args.workspacePath, vars);
4351
- await writeProjectSettings(args.workspacePath, vars);
4352
- await appendGitignoreEntries(args.workspacePath);
4353
- await ensureDir(join25(args.workspacePath, "notes"));
4354
- await ensureDir(join25(args.workspacePath, "scripts"));
4355
- await installGitHook(join25(args.workspacePath, ".git"), "post-merge");
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
- let gitnexusResult = null;
4369
- const skipGitnexus = args.aiSkip || args.gitnexusSkip;
4370
- if (skipGitnexus) {
4371
- if (args.gitnexusSkip) {
4372
- log.dim("B\u1ECF qua GitNexus setup (--gitnexus-skip). Setup sau: avatar gitnexus install");
4373
- } else {
4374
- log.dim("B\u1ECF qua GitNexus setup (auto-skip do --ai-skip).");
4375
- }
4376
- } else {
4377
- gitnexusResult = await runGitnexusSetupPhase({ workspacePath: args.workspacePath });
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
- if (gitnexusResult?.ok) {
4380
- const updatedVars = buildScaffoldVariables({
4381
- projectName: args.workspaceName,
4382
- projectDescription: args.description,
4383
- teamOwner: args.teamOwner,
4384
- packVersion: args.packVersion,
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
- await printInitSuccessBox(args.workspacePath, args.flow, aiResult, gitnexusResult);
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
- async function autoSyncPackOnInit(workspacePath) {
4394
- const packDir = join25(workspacePath, TEAM_PACK_RELATIVE_PATH);
4395
- if (!await pathExists(packDir)) {
4396
- log.dim("Pack submodule kh\xF4ng t\u1ED3n t\u1EA1i (skip auto-sync). C\xF3 th\u1EC3 ch\u1EA1y `avatar sync` sau.");
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 claudeDir = join25(workspacePath, ".claude");
4400
- log.info("Auto-sync pack content v\xE0o .claude/ (symlinks + settings merge)...");
4401
- try {
4402
- const results = await syncAllMountDirs(packDir, claudeDir, false);
4403
- const created = results.filter((r) => r.action === "created" || r.action === "updated").length;
4404
- const missing = results.filter((r) => r.action === "source-missing").length;
4405
- log.success(
4406
- ` \u2713 Symlinks: ${created} created${missing > 0 ? `, ${missing} source-missing (pack thi\u1EBFu dir)` : ""}`
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
- const mergeResult = await mergePackSettingsIntoProjectSettings(workspacePath);
4409
- switch (mergeResult.action) {
4410
- case "merged":
4411
- log.success(` \u2713 settings.json merged (${mergeResult.changes.join("; ")})`);
4412
- break;
4413
- case "no-change":
4414
- log.dim(" - settings.json \u0111\xE3 sync, kh\xF4ng c\u1EA7n thay \u0111\u1ED5i.");
4415
- break;
4416
- case "no-pack-template":
4417
- log.dim(" - Pack kh\xF4ng c\xF3 templates/settings.json.tpl, skip merge.");
4418
- break;
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
- log.warn(
4422
- `Auto-sync pack fail: ${err instanceof Error ? err.message : err}. Ch\u1EA1y \`avatar sync\` th\u1EE7 c\xF4ng \u0111\u1EC3 retry.`
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
- async function maybeCreateWorkspaceRemote(args) {
4427
- if (args.skipCommit) {
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
- let shouldCreate = args.createWorkspaceRemote;
4432
- if (shouldCreate === void 0) {
4433
- if (args.autoYes) return;
4434
- shouldCreate = await confirm5({
4435
- message: "T\u1EA1o remote GitHub cho workspace \u0111\u1EC3 share team? (Avatar state)",
4436
- default: false
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
- if (!shouldCreate) return;
4440
- const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select9({
4441
- message: "Workspace visibility?",
4442
- choices: [
4443
- { name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
4444
- { name: "public", value: "public" }
4445
- ]
4446
- }));
4447
- while (true) {
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 createWorkspaceRemoteViaGh({
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 CreateWorkspaceRemoteError && err.reason === "repo-exists") {
4458
- const fullName = err.fullName;
4459
- const reuseAction = await select9({
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
- const action = await promptRetryOrSkip({
4496
- taskName: "T\u1EA1o workspace remote tr\xEAn GitHub",
4497
- reason: err instanceof Error ? err.message : String(err),
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 (action === "skip") {
4506
- log.warn("Workspace v\u1EABn s\u1EB5n s\xE0ng local-only. Setup remote sau khi c\u1EA7n.");
4507
- return;
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 resolveWorkspacePath(parent, desiredName, force) {
4513
- const desired = join25(parent, desiredName);
4514
- if (await isEmptyOrMissing(desired)) return desired;
4515
- log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
4516
- while (true) {
4517
- const alternative = await findAlternativeWorkspaceName(parent, desiredName);
4518
- if (force && alternative) {
4519
- log.info(`--force: d\xF9ng ${alternative}`);
4520
- return alternative;
4521
- }
4522
- const choices = [];
4523
- if (alternative) {
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
- choices.push({ name: "Nh\u1EADp t\xEAn workspace kh\xE1c (manual)", value: "manual" });
4527
- choices.push({ name: "T\u1EA1m ng\u01B0ng init", value: "abort" });
4528
- const action = await select9({
4529
- message: "C\xE1ch x\u1EED l\xFD workspace path conflict?",
4530
- choices
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 resolve workspace path. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c."
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
- if (result.ok) {
4577
- const parts = ["ready"];
4578
- if (result.analyzed) parts.push("indexed");
4579
- if (result.wikiGenerated) parts.push("wiki");
4580
- if (result.mcpRegistered) parts.push("mcp");
4581
- return ` ${chalk.green("GitNexus:")} ${parts.join(" \xB7 ")}`;
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 printInitSuccessBox(rootPath, flow, aiResult = null, gitnexusResult = null) {
4586
- const lines = [
4587
- `${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative3(process.cwd(), rootPath) || rootPath}`,
4588
- ` ${chalk.dim(`(flow: ${flow})`)}`,
4589
- formatAiStatusLine(aiResult),
4590
- formatGitnexusStatusLine(gitnexusResult),
4591
- "",
4592
- ` ${chalk.cyan(`cd ${rootPath}`)}`,
4593
- ` ${chalk.cyan("claude")} M\u1EDF Claude Code \u1EDF workspace root`,
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 join26 } from "path";
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 = join26(cwd, PACK_RELATIVE_PATH);
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 join28 } from "path";
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 join27 } from "path";
4768
+ import { join as join28 } from "path";
4748
4769
  var BACKUP_DIR_NAME = "_backup";
4749
4770
  async function listBackups(projectRoot) {
4750
- const dir = join27(projectRoot, ".claude", BACKUP_DIR_NAME);
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 = join28(cwd, ".claude");
4796
+ const claudeRoot = join29(cwd, ".claude");
4776
4797
  const hasAvatar = await pathExists(claudeRoot);
4777
4798
  if (!hasAvatar) {
4778
4799
  return {
@@ -4785,14 +4806,14 @@ async function gatherStatus(cwd) {
4785
4806
  hasAvatar: false
4786
4807
  };
4787
4808
  }
4788
- const packVersion = await isGitRepo(join28(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
4789
- const pendingDir = join28(claudeRoot, "_pending");
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);
4793
4814
  return {
4794
4815
  projectName,
4795
- cliVersion: AVATAR_CLI_VERSION,
4816
+ cliVersion: readCliVersion(),
4796
4817
  packVersion,
4797
4818
  pendingCount,
4798
4819
  backupCount,
@@ -4801,7 +4822,7 @@ async function gatherStatus(cwd) {
4801
4822
  };
4802
4823
  }
4803
4824
  async function readTechStackFirstLine(claudeRoot) {
4804
- const techStackPath = join28(claudeRoot, "project", "tech-stack.md");
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 join30 } from "path";
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 join29 } from "path";
4849
+ import { join as join30 } from "path";
4829
4850
  async function inspectMountDir(packDir, claudeDir, dir) {
4830
- const source = join29(packDir, dir);
4831
- const dest = join29(claudeDir, dir);
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 = join30(projectRoot, ".claude");
4874
- const packDir = join30(projectRoot, TEAM_PACK_RELATIVE_PATH);
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 basename2, join as join31 } from "path";
5018
- var UNINSTALL_BACKUPS_DIR = join31(homedir4(), ".avatar", "uninstall-backups");
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 = basename2(projectRoot);
5039
+ const projectName = basename3(projectRoot);
5021
5040
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5022
- const backupDir = join31(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp2}`);
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, join31(backupDir, ".claude"), { recursive: true });
5044
+ await cp(artifacts.claudeDir, join32(backupDir, ".claude"), { recursive: true });
5026
5045
  }
5027
5046
  if (artifacts.claudeMd) {
5028
- await cp(artifacts.claudeMd, join31(backupDir, "CLAUDE.md"));
5047
+ await cp(artifacts.claudeMd, join32(backupDir, "CLAUDE.md"));
5029
5048
  }
5030
5049
  if (artifacts.postMergeHook || artifacts.prePushHook) {
5031
- const hooksBackupDir = join31(backupDir, "hooks");
5050
+ const hooksBackupDir = join32(backupDir, "hooks");
5032
5051
  await mkdir(hooksBackupDir, { recursive: true });
5033
5052
  if (artifacts.postMergeHook) {
5034
- await cp(artifacts.postMergeHook, join31(hooksBackupDir, "post-merge"));
5053
+ await cp(artifacts.postMergeHook, join32(hooksBackupDir, "post-merge"));
5035
5054
  }
5036
5055
  if (artifacts.prePushHook) {
5037
- await cp(artifacts.prePushHook, join31(hooksBackupDir, "pre-push"));
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(join31(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
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 join32 } from "path";
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(join32(projectRoot, ".claude"));
5064
- const claudeMd = existsOrNull(join32(projectRoot, "CLAUDE.md"));
5065
- const postMergeHook = existsOrNull(join32(projectRoot, ".git", "hooks", "post-merge"));
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
- join32(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
5086
+ join33(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
5068
5087
  );
5069
- const gitignorePath = existsOrNull(join32(projectRoot, ".gitignore"));
5070
- const gitmodulesPath = existsOrNull(join32(projectRoot, ".gitmodules"));
5071
- const notesDir = existsOrNull(join32(projectRoot, "notes"));
5072
- const scriptsDir = existsOrNull(join32(projectRoot, "scripts"));
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: join33 } = await import("path");
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(join33(artifacts.claudeDir, entry), { recursive: true, force: true });
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 });