@nalvietnam/avatar-cli 1.11.1 → 1.12.0

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