@nalvietnam/avatar-cli 1.0.0 → 1.1.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
@@ -124,6 +124,7 @@ import { promises as fs2 } from "fs";
124
124
  import { join as join4 } from "path";
125
125
 
126
126
  // src/lib/template-bundle-loader.ts
127
+ import { existsSync } from "fs";
127
128
  import { dirname as dirname2, join as join3 } from "path";
128
129
  import { fileURLToPath } from "url";
129
130
 
@@ -139,8 +140,20 @@ function renderTemplate(source, variables) {
139
140
 
140
141
  // src/lib/template-bundle-loader.ts
141
142
  var HERE = dirname2(fileURLToPath(import.meta.url));
142
- var TEMPLATES_ROOT = join3(HERE, "..", "src", "templates");
143
- var HOOKS_ROOT = join3(HERE, "..", "src", "hooks");
143
+ var PACKAGE_ROOT = findPackageRoot(HERE);
144
+ var TEMPLATES_ROOT = join3(PACKAGE_ROOT, "src", "templates");
145
+ var HOOKS_ROOT = join3(PACKAGE_ROOT, "src", "hooks");
146
+ function findPackageRoot(startDir) {
147
+ let dir = startDir;
148
+ while (true) {
149
+ if (existsSync(join3(dir, "package.json"))) return dir;
150
+ const parent = dirname2(dir);
151
+ if (parent === dir) {
152
+ throw new Error(`Cannot locate package root from ${startDir}`);
153
+ }
154
+ dir = parent;
155
+ }
156
+ }
144
157
  async function loadTemplate(name) {
145
158
  return await readText(join3(TEMPLATES_ROOT, `${name}.tpl`));
146
159
  }
@@ -153,6 +166,27 @@ async function loadHook(name) {
153
166
  }
154
167
 
155
168
  // src/lib/project-tree-scaffolder.ts
169
+ async function backupIfExists(path) {
170
+ if (!await pathExists(path)) return null;
171
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
172
+ const basePath = `${path}.avatar-backup-${ts}`;
173
+ let backupPath = basePath;
174
+ let counter = 1;
175
+ while (await pathExists(backupPath)) {
176
+ backupPath = `${basePath}-${counter}`;
177
+ counter++;
178
+ if (counter > 5) {
179
+ throw new Error(`Could not find free backup name for ${path}`);
180
+ }
181
+ }
182
+ await fs2.rename(path, backupPath);
183
+ return backupPath;
184
+ }
185
+ async function writeWithBackup(path, content, mode) {
186
+ const backup = await backupIfExists(path);
187
+ await writeTextAtomic(path, content, mode);
188
+ return backup;
189
+ }
156
190
  var CLAUDE_SUBDIRS = ["project", "state", "_pending", "_backup"];
157
191
  var PROJECT_KNOWLEDGE_TEMPLATES = [
158
192
  "project/tech-stack.md",
@@ -194,20 +228,23 @@ async function writeProjectKnowledgeFiles(projectRoot, vars) {
194
228
  primaryUseCases: "(ch\u01B0a scan)",
195
229
  domainGlossary: "(ch\u01B0a scan)"
196
230
  };
231
+ const backups = [];
197
232
  for (const tpl of PROJECT_KNOWLEDGE_TEMPLATES) {
198
233
  const content = await renderTemplateByName(tpl, baseVars);
199
- const relative2 = tpl.replace(/^project\//, "");
200
- const outPath = join4(projectRoot, ".claude", "project", relative2);
201
- await writeTextAtomic(outPath, content);
234
+ const relative4 = tpl.replace(/^project\//, "");
235
+ const outPath = join4(projectRoot, ".claude", "project", relative4);
236
+ const backup = await writeWithBackup(outPath, content);
237
+ if (backup) backups.push(backup);
202
238
  }
239
+ return backups;
203
240
  }
204
241
  async function writeRootClaudeMd(projectRoot, vars) {
205
242
  const content = await renderTemplateByName("CLAUDE.md", vars);
206
- await writeTextAtomic(join4(projectRoot, "CLAUDE.md"), content);
243
+ return await writeWithBackup(join4(projectRoot, "CLAUDE.md"), content);
207
244
  }
208
245
  async function writeProjectSettings(projectRoot, vars) {
209
246
  const content = await renderTemplateByName("settings.json", vars);
210
- await writeTextAtomic(join4(projectRoot, ".claude", "settings.json"), content);
247
+ return await writeWithBackup(join4(projectRoot, ".claude", "settings.json"), content);
211
248
  }
212
249
  async function appendGitignoreEntries(projectRoot) {
213
250
  const path = join4(projectRoot, ".gitignore");
@@ -441,7 +478,7 @@ async function applyFixes(checks) {
441
478
  }
442
479
 
443
480
  // src/commands/init.ts
444
- import { join as join8, resolve } from "path";
481
+ import { basename, join as join13, relative as relative2, resolve } from "path";
445
482
  import { confirm, input, select } from "@inquirer/prompts";
446
483
  import boxen2 from "boxen";
447
484
 
@@ -459,15 +496,406 @@ async function appendAuditEntry(action, detail) {
459
496
  await fs4.appendFile(AUDIT_LOG_PATH, line, "utf8");
460
497
  }
461
498
 
462
- // src/lib/team-pack-submodule-manager.ts
499
+ // src/lib/avatar-ascii-banner.ts
500
+ import chalk2 from "chalk";
501
+ var BANNER_LINES = [
502
+ " \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 ",
503
+ "\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",
504
+ "\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",
505
+ "\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",
506
+ "\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",
507
+ "\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"
508
+ ];
509
+ var GRADIENT_STOPS = [
510
+ [217, 79, 30],
511
+ // cam-cháy (#d94f1e)
512
+ [200, 70, 80],
513
+ // cam-hồng
514
+ [170, 70, 140],
515
+ // hồng-tím
516
+ [125, 88, 217]
517
+ // tím (#7d58d9)
518
+ ];
519
+ function lerpChannel(a, b, t) {
520
+ return Math.round(a + (b - a) * t);
521
+ }
522
+ function gradientAt(t) {
523
+ const clamped = Math.max(0, Math.min(1, t));
524
+ const scaled = clamped * (GRADIENT_STOPS.length - 1);
525
+ const lo = Math.floor(scaled);
526
+ const hi = Math.min(GRADIENT_STOPS.length - 1, lo + 1);
527
+ const localT = scaled - lo;
528
+ const a = GRADIENT_STOPS[lo];
529
+ const b = GRADIENT_STOPS[hi];
530
+ return [
531
+ lerpChannel(a[0], b[0], localT),
532
+ lerpChannel(a[1], b[1], localT),
533
+ lerpChannel(a[2], b[2], localT)
534
+ ];
535
+ }
536
+ function renderAvatarBanner(opts) {
537
+ const isTty = process.stdout.isTTY ?? false;
538
+ const supportsColor = isTty && chalk2.level > 0;
539
+ if (!supportsColor) {
540
+ return [...BANNER_LINES, ...opts?.tagline ? ["", opts.tagline] : []].join("\n");
541
+ }
542
+ const colored = BANNER_LINES.map((line, idx) => {
543
+ const t = BANNER_LINES.length === 1 ? 0 : idx / (BANNER_LINES.length - 1);
544
+ const [r, g, b] = gradientAt(t);
545
+ return chalk2.rgb(r, g, b).bold(line);
546
+ });
547
+ if (opts?.tagline) {
548
+ colored.push("");
549
+ colored.push(chalk2.dim(opts.tagline));
550
+ }
551
+ return colored.join("\n");
552
+ }
553
+ function printAvatarBanner(opts) {
554
+ process.stdout.write(`
555
+ ${renderAvatarBanner(opts)}
556
+
557
+ `);
558
+ }
559
+
560
+ // src/lib/execute-gh-repo-create.ts
561
+ import { spawnSync as spawnSync2 } from "child_process";
562
+ var RepoAlreadyExistsError = class extends Error {
563
+ constructor(fullName) {
564
+ super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
565
+ this.name = "RepoAlreadyExistsError";
566
+ }
567
+ };
568
+ function executeGhRepoCreate(input2) {
569
+ const fullName = `${input2.org}/${input2.name}`;
570
+ const args = [
571
+ "repo",
572
+ "create",
573
+ fullName,
574
+ `--${input2.visibility}`,
575
+ "--source",
576
+ input2.folder,
577
+ "--remote",
578
+ "origin",
579
+ "--push"
580
+ ];
581
+ const r = spawnSync2("gh", args, { stdio: "inherit" });
582
+ if (r.status !== 0) {
583
+ if (r.status === 1) {
584
+ throw new RepoAlreadyExistsError(fullName);
585
+ }
586
+ throw new Error(`gh repo create th\u1EA5t b\u1EA1i (exit ${r.status})`);
587
+ }
588
+ return {
589
+ sshUrl: `git@github.com:${fullName}.git`,
590
+ httpsUrl: `https://github.com/${fullName}.git`
591
+ };
592
+ }
593
+
594
+ // src/lib/resolve-github-username-default.ts
595
+ import { spawnSync as spawnSync3 } from "child_process";
596
+ function resolveGithubUsernameDefault() {
597
+ const r = spawnSync3("gh", ["api", "user", "--jq", ".login"], {
598
+ encoding: "utf8",
599
+ stdio: ["ignore", "pipe", "pipe"]
600
+ });
601
+ if (r.status !== 0) {
602
+ throw new Error(`Kh\xF4ng l\u1EA5y \u0111\u01B0\u1EE3c GitHub username: ${r.stderr?.trim()}`);
603
+ }
604
+ return r.stdout.trim();
605
+ }
606
+
607
+ // src/lib/validate-repo-name-and-visibility.ts
608
+ var REPO_NAME_REGEX = /^[a-zA-Z0-9._-]{1,100}$/;
609
+ var InvalidRepoNameError = class extends Error {
610
+ constructor(name) {
611
+ super(
612
+ `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.`
613
+ );
614
+ this.name = "InvalidRepoNameError";
615
+ }
616
+ };
617
+ function validateRepoName(name) {
618
+ if (!REPO_NAME_REGEX.test(name)) {
619
+ throw new InvalidRepoNameError(name);
620
+ }
621
+ }
622
+ function validateRepoVisibility(v) {
623
+ if (v !== "private" && v !== "public") {
624
+ throw new Error(`Visibility ph\u1EA3i l\xE0 "private" ho\u1EB7c "public", nh\u1EADn: "${v}"`);
625
+ }
626
+ }
627
+
628
+ // src/lib/create-github-remote-from-folder.ts
629
+ function createGithubRemoteFromFolder(input2) {
630
+ validateRepoName(input2.name);
631
+ validateRepoVisibility(input2.visibility);
632
+ const org = input2.org ?? resolveGithubUsernameDefault();
633
+ log.info(`T\u1EA1o GitHub repo ${org}/${input2.name} (${input2.visibility})...`);
634
+ const urls = executeGhRepoCreate({
635
+ folder: input2.folder,
636
+ org,
637
+ name: input2.name,
638
+ visibility: input2.visibility
639
+ });
640
+ log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
641
+ return urls;
642
+ }
643
+
644
+ // src/lib/check-gh-cli-auth-status.ts
645
+ import { spawnSync as spawnSync4 } from "child_process";
646
+ function checkGhCliAuthStatus() {
647
+ const r = spawnSync4("gh", ["auth", "status"], { stdio: "ignore" });
648
+ if (r.error && r.error.code === "ENOENT") {
649
+ return "not-installed";
650
+ }
651
+ return r.status === 0 ? "authenticated" : "not-authenticated";
652
+ }
653
+
654
+ // src/lib/detect-package-manager.ts
655
+ import { spawnSync as spawnSync5 } from "child_process";
656
+
657
+ // src/lib/detect-host-platform.ts
658
+ import { platform } from "os";
659
+ function detectHostPlatform() {
660
+ const p = platform();
661
+ if (p === "darwin" || p === "linux" || p === "win32") return p;
662
+ return "unsupported";
663
+ }
664
+
665
+ // src/lib/detect-package-manager.ts
666
+ function hasBinary(name) {
667
+ const platform2 = detectHostPlatform();
668
+ const probe = platform2 === "win32" ? "where" : "command";
669
+ const args = platform2 === "win32" ? [name] : ["-v", name];
670
+ const r = spawnSync5(probe, args, {
671
+ shell: platform2 !== "win32",
672
+ stdio: "ignore"
673
+ });
674
+ return r.status === 0;
675
+ }
676
+ function detectPackageManager() {
677
+ const platform2 = detectHostPlatform();
678
+ const candidates = platform2 === "darwin" ? ["brew"] : platform2 === "win32" ? ["winget"] : platform2 === "linux" ? ["apt", "dnf", "pacman"] : [];
679
+ for (const pm of candidates) {
680
+ if (hasBinary(pm)) return pm;
681
+ }
682
+ return null;
683
+ }
684
+
685
+ // src/lib/install-gh-cli-via-package-manager.ts
686
+ import { spawnSync as spawnSync6 } from "child_process";
687
+ var INSTALL_COMMANDS = {
688
+ brew: { cmd: "brew", args: ["install", "gh"] },
689
+ apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
690
+ dnf: { cmd: "sudo", args: ["dnf", "install", "-y", "gh"] },
691
+ pacman: { cmd: "sudo", args: ["pacman", "-S", "--noconfirm", "github-cli"] },
692
+ winget: { cmd: "winget", args: ["install", "--id", "GitHub.cli", "-e", "--silent"] }
693
+ };
694
+ function installGhCliViaPackageManager(pm) {
695
+ const spec = INSTALL_COMMANDS[pm];
696
+ log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
697
+ const r = spawnSync6(spec.cmd, spec.args, { stdio: "inherit" });
698
+ if (r.status !== 0) {
699
+ throw new Error(`C\xE0i gh CLI th\u1EA5t b\u1EA1i qua ${pm} (exit ${r.status}). C\xE0i tay r\u1ED3i ch\u1EA1y l\u1EA1i.`);
700
+ }
701
+ log.success("\u0110\xE3 c\xE0i gh CLI");
702
+ }
703
+
704
+ // src/lib/trigger-gh-cli-auth-login.ts
705
+ import { spawnSync as spawnSync7 } from "child_process";
706
+ function triggerGhCliAuthLogin() {
707
+ log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
708
+ const r = spawnSync7(
709
+ "gh",
710
+ ["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
711
+ { stdio: "inherit" }
712
+ );
713
+ if (r.status !== 0) {
714
+ throw new Error(`gh auth login th\u1EA5t b\u1EA1i (exit ${r.status}). Th\u1EED 'gh auth login' tay.`);
715
+ }
716
+ log.success("\u0110\xE3 \u0111\u0103ng nh\u1EADp GitHub");
717
+ }
718
+
719
+ // src/lib/verify-git-remote-accessible.ts
720
+ import { spawnSync as spawnSync8 } from "child_process";
721
+ var TIMEOUT_MS = 5e3;
722
+ var RemoteNotAccessibleError = class extends Error {
723
+ constructor(url, reason) {
724
+ super(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c remote ${url}: ${reason}`);
725
+ this.name = "RemoteNotAccessibleError";
726
+ }
727
+ };
728
+ function verifyGitRemoteAccessible(url) {
729
+ const r = spawnSync8("git", ["ls-remote", "--exit-code", url, "HEAD"], {
730
+ stdio: "ignore",
731
+ timeout: TIMEOUT_MS
732
+ });
733
+ if (r.status === 0) return;
734
+ if (r.signal === "SIGTERM") throw new RemoteNotAccessibleError(url, "timeout 5s");
735
+ throw new RemoteNotAccessibleError(url, `git ls-remote exit ${r.status}`);
736
+ }
737
+
738
+ // src/lib/git-auth-and-install-orchestrator.ts
739
+ async function ensureGitHubReady(remoteUrl) {
740
+ let state = checkGhCliAuthStatus();
741
+ if (state === "not-installed") {
742
+ log.warn("gh CLI ch\u01B0a c\xE0i. Avatar s\u1EBD t\u1EF1 c\xE0i.");
743
+ const pm = detectPackageManager();
744
+ if (!pm) {
745
+ throw new Error(
746
+ "Kh\xF4ng ph\xE1t hi\u1EC7n package manager (brew/apt/dnf/pacman/winget). C\xE0i gh CLI tay r\u1ED3i ch\u1EA1y l\u1EA1i: https://cli.github.com"
747
+ );
748
+ }
749
+ installGhCliViaPackageManager(pm);
750
+ state = checkGhCliAuthStatus();
751
+ }
752
+ if (state === "not-authenticated") {
753
+ log.warn("Ch\u01B0a \u0111\u0103ng nh\u1EADp GitHub.");
754
+ triggerGhCliAuthLogin();
755
+ state = checkGhCliAuthStatus();
756
+ if (state !== "authenticated") {
757
+ throw new Error("Sau gh auth login v\u1EABn ch\u01B0a authenticated. Th\u1EED l\u1EA1i.");
758
+ }
759
+ }
760
+ log.success("gh CLI s\u1EB5n s\xE0ng");
761
+ if (remoteUrl) {
762
+ verifyGitRemoteAccessible(remoteUrl);
763
+ log.success(`Remote accessible: ${remoteUrl}`);
764
+ }
765
+ }
766
+
767
+ // src/lib/check-folder-has-git.ts
768
+ import { existsSync as existsSync2, statSync } from "fs";
463
769
  import { join as join7 } from "path";
464
- var TEAM_PACK_REPO_URL = "https://github.com/LukeNALS/team-ai-pack.git";
770
+ function checkFolderHasGit(folderPath) {
771
+ const gitPath = join7(folderPath, ".git");
772
+ if (!existsSync2(gitPath)) return false;
773
+ const stat = statSync(gitPath);
774
+ return stat.isDirectory() || stat.isFile();
775
+ }
776
+
777
+ // src/lib/create-initial-git-commit.ts
778
+ import { simpleGit as simpleGit2 } from "simple-git";
779
+ var INITIAL_COMMIT_MESSAGE = "chore: initial commit";
780
+ async function createInitialGitCommit(folderPath) {
781
+ const g = simpleGit2({ baseDir: folderPath });
782
+ const isRepo = await g.checkIsRepo().catch(() => false);
783
+ if (!isRepo) {
784
+ await g.init();
785
+ }
786
+ try {
787
+ await g.branch(["-M", "main"]);
788
+ } catch {
789
+ }
790
+ await g.add(".");
791
+ const status = await g.status();
792
+ const hasCommits = (await g.raw(["rev-list", "-n", "1", "--all"]).catch(() => "")).trim();
793
+ if (hasCommits) return;
794
+ if (status.files.length === 0) {
795
+ await g.commit(INITIAL_COMMIT_MESSAGE, void 0, { "--allow-empty": null });
796
+ } else {
797
+ await g.commit(INITIAL_COMMIT_MESSAGE);
798
+ }
799
+ }
800
+
801
+ // src/lib/detect-folder-tech-stack.ts
802
+ import { existsSync as existsSync3 } from "fs";
803
+ import { join as join8 } from "path";
804
+ var SIGNATURES = {
805
+ node: ["package.json"],
806
+ python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
807
+ go: ["go.mod"],
808
+ rust: ["Cargo.toml"],
809
+ java: ["pom.xml", "build.gradle", "build.gradle.kts"],
810
+ ruby: ["Gemfile"]
811
+ };
812
+ function detectFolderTechStack(folderPath) {
813
+ const matched = [];
814
+ for (const [stack, files] of Object.entries(SIGNATURES)) {
815
+ if (files.some((f) => existsSync3(join8(folderPath, f)))) {
816
+ matched.push(stack);
817
+ }
818
+ }
819
+ return matched.length > 0 ? matched : ["generic"];
820
+ }
821
+
822
+ // src/lib/gitignore-template-loader.ts
823
+ import { readFileSync } from "fs";
824
+ import { dirname as dirname3, join as join9 } from "path";
825
+ import { fileURLToPath as fileURLToPath2 } from "url";
826
+ var __dirname = dirname3(fileURLToPath2(import.meta.url));
827
+ var CANDIDATE_DIRS = [
828
+ join9(__dirname, "..", "templates", "gitignore"),
829
+ join9(__dirname, "..", "..", "src", "templates", "gitignore")
830
+ ];
831
+ var AVATAR_MARKER_START = "# === avatar ===";
832
+ var AVATAR_MARKER_END = "# === /avatar ===";
833
+ function readTemplate(stack) {
834
+ for (const dir of CANDIDATE_DIRS) {
835
+ try {
836
+ return readFileSync(join9(dir, `${stack}.txt`), "utf8");
837
+ } catch {
838
+ }
839
+ }
840
+ throw new Error(`Kh\xF4ng t\xECm th\u1EA5y template gitignore cho stack "${stack}"`);
841
+ }
842
+ function composeGitignoreContent(stacks) {
843
+ const all = ["generic", ...stacks.filter((s) => s !== "generic")];
844
+ const sections = all.map((s) => `# --- ${s} ---
845
+ ${readTemplate(s).trim()}`);
846
+ return [AVATAR_MARKER_START, ...sections, AVATAR_MARKER_END, ""].join("\n");
847
+ }
848
+
849
+ // src/lib/write-or-merge-gitignore.ts
850
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync } from "fs";
851
+ import { join as join10 } from "path";
852
+ function writeOrMergeGitignore(folderPath, avatarBlock) {
853
+ const path = join10(folderPath, ".gitignore");
854
+ if (!existsSync4(path)) {
855
+ writeFileSync(path, avatarBlock, "utf8");
856
+ return;
857
+ }
858
+ const existing = readFileSync2(path, "utf8");
859
+ const startIdx = existing.indexOf(AVATAR_MARKER_START);
860
+ const endIdx = existing.indexOf(AVATAR_MARKER_END);
861
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
862
+ const before = existing.slice(0, startIdx);
863
+ const after = existing.slice(endIdx + AVATAR_MARKER_END.length);
864
+ writeFileSync(path, `${before.trimEnd()}
865
+
866
+ ${avatarBlock}${after.trimStart()}`, "utf8");
867
+ return;
868
+ }
869
+ writeFileSync(path, `${existing.trimEnd()}
870
+
871
+ ${avatarBlock}`, "utf8");
872
+ }
873
+
874
+ // src/lib/git-bootstrap-orchestrator.ts
875
+ async function bootstrapGitInFolder(folderPath) {
876
+ const hadGit = checkFolderHasGit(folderPath);
877
+ const stacks = detectFolderTechStack(folderPath);
878
+ log.info(`Tech stack detected: ${stacks.join(", ")}`);
879
+ writeOrMergeGitignore(folderPath, composeGitignoreContent(stacks));
880
+ log.success(".gitignore \u0111\xE3 ghi (Avatar block)");
881
+ if (!hadGit) {
882
+ log.info(`Bootstrap git cho ${folderPath}...`);
883
+ await createInitialGitCommit(folderPath);
884
+ log.success("\u0110\xE3 git init + initial commit");
885
+ } else {
886
+ log.dim("Folder \u0111\xE3 c\xF3 .git \u2014 skip init.");
887
+ }
888
+ }
889
+
890
+ // src/lib/team-pack-submodule-manager.ts
891
+ import { join as join11 } from "path";
892
+ var TEAM_PACK_REPO_URL = process.env.AVATAR_TEAM_PACK_REPO_URL ?? "https://github.com/LukeNALS/team-ai-pack.git";
465
893
  var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
466
894
  async function addTeamPackSubmodule(projectRoot, tag) {
467
895
  await addSubmodule(TEAM_PACK_REPO_URL, TEAM_PACK_RELATIVE_PATH, projectRoot);
468
896
  let target = tag ?? null;
469
897
  if (!target) {
470
- target = await latestTag(join7(projectRoot, TEAM_PACK_RELATIVE_PATH));
898
+ target = await latestTag(join11(projectRoot, TEAM_PACK_RELATIVE_PATH));
471
899
  }
472
900
  if (target) {
473
901
  await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
@@ -475,17 +903,56 @@ async function addTeamPackSubmodule(projectRoot, tag) {
475
903
  return { pinnedTag: target };
476
904
  }
477
905
  async function readPinnedPackVersion(projectRoot) {
478
- const submoduleRoot = join7(projectRoot, TEAM_PACK_RELATIVE_PATH);
906
+ const submoduleRoot = join11(projectRoot, TEAM_PACK_RELATIVE_PATH);
479
907
  const tag = await latestTag(submoduleRoot);
480
908
  if (tag) return tag;
481
909
  const sha = await currentCommitSha(submoduleRoot);
482
910
  return sha.slice(0, 7);
483
911
  }
484
912
 
913
+ // src/commands/init-conflict-detection-helpers.ts
914
+ import { readdir } from "fs/promises";
915
+ import { join as join12 } from "path";
916
+ async function isEmptyOrMissing(path) {
917
+ if (!await pathExists(path)) return true;
918
+ try {
919
+ const entries = await readdir(path);
920
+ const meaningful = entries.filter((e) => !e.startsWith(".") && e !== "Thumbs.db");
921
+ return meaningful.length === 0;
922
+ } catch {
923
+ return false;
924
+ }
925
+ }
926
+ async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
927
+ for (let i = 2; i < maxAttempts; i++) {
928
+ const candidate = join12(parent, `${desiredName}-${i}`);
929
+ if (await isEmptyOrMissing(candidate)) return candidate;
930
+ }
931
+ return null;
932
+ }
933
+
934
+ // src/commands/init-scaffold-variable-builders.ts
935
+ var AVATAR_CLI_VERSION = "1.0.1";
936
+ function inferWorkspaceName(repoUrl) {
937
+ const m = repoUrl.match(/[/:]([^/]+?)(\.git)?$/);
938
+ const base = m?.[1] ?? "client";
939
+ return `avatar-${base}-workspace`;
940
+ }
941
+ function buildScaffoldVariables(args) {
942
+ return {
943
+ projectName: args.projectName,
944
+ projectDescription: args.projectDescription,
945
+ teamOwner: args.teamOwner,
946
+ avatarVersion: AVATAR_CLI_VERSION,
947
+ packVersion: args.packVersion,
948
+ lastScan: (/* @__PURE__ */ new Date()).toISOString(),
949
+ mode: args.mode
950
+ };
951
+ }
952
+
485
953
  // src/commands/init.ts
486
- var AVATAR_CLI_VERSION = "1.0.0";
487
954
  function registerInitCommand(program2) {
488
- program2.command("init").description("Kh\u1EDFi t\u1EA1o Avatar trong d\u1EF1 \xE1n (3 mode: internal/client/library)").option("--mode <mode>", "internal | client | library").option("--skip-scan", "B\u1ECF qua b\u01B0\u1EDBc project-scanner").option("--pack-version <tag>", "Pin team-ai-pack v\xE0o version c\u1EE5 th\u1EC3").option("--client-repo <url>", "URL git c\u1EE7a client repo (mode=client)").option("--workspace-name <name>", "T\xEAn workspace (mode=client)").option("--workspace-parent <path>", "Th\u01B0 m\u1EE5c cha t\u1EA1o workspace (mode=client)").action(async (opts) => {
955
+ 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 ..)").option("--pack-version <tag>", "Pin team-ai-pack v\xE0o tag c\u1EE5 th\u1EC3").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("--force", "B\u1ECF qua prompt khi workspace path \u0111\xE3 t\u1ED3n t\u1EA1i").option("--yes", "Auto-confirm t\u1EA5t c\u1EA3 prompt").option("--mode <mode>", "[DEPRECATED] D\xF9ng --project-status thay th\u1EBF").action(async (opts) => {
489
956
  try {
490
957
  await runInit(opts);
491
958
  } catch (err) {
@@ -495,190 +962,251 @@ function registerInitCommand(program2) {
495
962
  });
496
963
  }
497
964
  async function runInit(opts) {
965
+ if (!opts.yes) printAvatarBanner({ tagline: "Kh\u1EDFi t\u1EA1o Avatar trong d\u1EF1 \xE1n c\u1EE7a b\u1EA1n" });
966
+ if (opts.mode) {
967
+ log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
968
+ }
498
969
  const userConfig = await readUserConfig();
499
970
  if (!userConfig || isTokenExpired(userConfig)) {
500
- log.error("Ch\u01B0a \u0111\u0103ng nh\u1EADp ho\u1EB7c token \u0111\xE3 h\u1EBFt h\u1EA1n. Ch\u1EA1y 'avatar login' tr\u01B0\u1EDBc.");
971
+ log.error("Ch\u01B0a \u0111\u0103ng nh\u1EADp ho\u1EB7c token h\u1EBFt h\u1EA1n. Ch\u1EA1y 'avatar login' tr\u01B0\u1EDBc.");
501
972
  process.exit(1);
502
973
  }
503
- const mode = opts.mode ?? await promptMode();
504
- if (mode === "internal") {
505
- await runInitInternal(opts, userConfig.email);
506
- } else {
507
- await runInitClientOrLibrary(opts, mode, userConfig.email);
974
+ const status = opts.projectStatus ?? await promptProjectStatus();
975
+ switch (status) {
976
+ case "existing-remote":
977
+ await runInitFromExistingRemote(opts, userConfig.email);
978
+ break;
979
+ case "existing-folder":
980
+ await runInitFromExistingFolder(opts, userConfig.email);
981
+ break;
982
+ case "new-project":
983
+ await runInitFromScratch(opts, userConfig.email);
984
+ break;
508
985
  }
509
986
  }
510
- async function promptMode() {
987
+ async function promptProjectStatus() {
511
988
  return await select({
512
- message: "\u0110\xE2y l\xE0 lo\u1EA1i d\u1EF1 \xE1n g\xEC?",
989
+ message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
513
990
  choices: [
514
- {
515
- name: "N\u1ED9i b\u1ED9 NAL (Avatar files commit c\xF9ng code)",
516
- value: "internal"
517
- },
518
- { name: "Client (Pattern A \u2014 t\xE1ch workspace)", value: "client" },
519
- { name: "Library/SDK public (t\xE1ch workspace)", value: "library" }
991
+ { name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
992
+ { name: "2. \u0110\xE3 c\xF3 folder code local", value: "existing-folder" },
993
+ { name: "3. D\u1EF1 \xE1n m\u1EDBi ho\xE0n to\xE0n", value: "new-project" }
520
994
  ]
521
995
  });
522
996
  }
523
- async function runInitInternal(opts, ownerEmail) {
524
- const projectRoot = process.cwd();
525
- if (!await isGitRepo(projectRoot)) {
526
- throw new Error("Mode internal c\u1EA7n d\u1EF1 \xE1n \u0111\xE3 l\xE0 git repo. Ch\u1EA1y 'git init' tr\u01B0\u1EDBc r\u1ED3i th\u1EED l\u1EA1i.");
527
- }
528
- if (await pathExists(join8(projectRoot, ".claude"))) {
529
- throw new Error(
530
- ".claude/ \u0111\xE3 t\u1ED3n t\u1EA1i. Avatar kh\xF4ng override. X\xF3a th\u1EE7 c\xF4ng ho\u1EB7c d\xF9ng mode kh\xE1c."
531
- );
532
- }
533
- const teamOwner = await promptTeamOwner(ownerEmail);
534
- const projectName = projectNameOf(projectRoot);
535
- const projectDescription = await input({
536
- message: "M\xF4 t\u1EA3 ng\u1EAFn 1 d\xF2ng c\u1EE7a d\u1EF1 \xE1n:",
537
- default: `Avatar-managed project: ${projectName}`
997
+ async function runInitFromExistingRemote(opts, ownerEmail) {
998
+ const remoteUrl = opts.clientRepo ?? await input({
999
+ message: "URL git c\u1EE7a repo:",
1000
+ validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
538
1001
  });
539
- const sp = spinner(`Th\xEAm submodule team-ai-pack t\u1EEB ${TEAM_PACK_REPO_URL}...`);
540
- let pinnedTag = null;
541
- try {
542
- const result = await addTeamPackSubmodule(projectRoot, opts.packVersion);
543
- pinnedTag = result.pinnedTag;
544
- sp.succeed(`\u0110\xE3 pin team-ai-pack v\xE0o ${pinnedTag ?? "HEAD"}`);
545
- } catch (err) {
546
- sp.fail("Add submodule th\u1EA5t b\u1EA1i");
547
- throw err;
548
- }
549
- const vars = buildScaffoldVariables({
550
- projectName,
551
- projectDescription,
1002
+ await ensureGitHubReady(remoteUrl);
1003
+ const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
1004
+ const inferredName = inferWorkspaceName(remoteUrl);
1005
+ const workspaceName = opts.workspaceName ?? await input({ message: "T\xEAn workspace:", default: inferredName });
1006
+ const workspaceParent = resolve(opts.workspaceParent ?? "..");
1007
+ const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
1008
+ await scaffoldWorkspaceWithSrcSubmodule({
1009
+ workspacePath,
1010
+ workspaceName,
1011
+ srcRemoteUrl: remoteUrl,
552
1012
  teamOwner,
553
- packVersion: pinnedTag ?? "HEAD",
554
- mode: "internal"
1013
+ description: opts.description ?? `Avatar workspace cho ${remoteUrl}`,
1014
+ packVersion: opts.packVersion,
1015
+ autoYes: opts.yes,
1016
+ flow: "existing-remote"
555
1017
  });
556
- await createClaudeDirTree(projectRoot);
557
- await writeProjectKnowledgeFiles(projectRoot, vars);
558
- await writeRootClaudeMd(projectRoot, vars);
559
- await writeProjectSettings(projectRoot, vars);
560
- await appendGitignoreEntries(projectRoot);
561
- await installGitHook(join8(projectRoot, ".git"), "post-merge");
562
- log.success("C\xE0i git hook post-merge");
563
- await appendAuditEntry("init", `mode=internal,project=${projectName}`);
564
- await maybeCommit(projectRoot, "internal");
565
- printInitSuccessBox(projectRoot, "internal");
566
- }
567
- async function runInitClientOrLibrary(opts, mode, ownerEmail) {
568
- const teamOwner = await promptTeamOwner(ownerEmail);
569
- const clientRepoUrl = opts.clientRepo ?? await input({
570
- message: "URL git c\u1EE7a client repo:",
571
- validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
1018
+ }
1019
+ async function runInitFromExistingFolder(opts, ownerEmail) {
1020
+ const folderPath = resolve(
1021
+ opts.folderPath ?? await input({
1022
+ message: "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3:",
1023
+ validate: (v) => v.length > 0 ? true : "Path b\u1EAFt bu\u1ED9c"
1024
+ })
1025
+ );
1026
+ await bootstrapGitInFolder(folderPath);
1027
+ const remoteUrl = await getOrCreateOriginRemote(folderPath, opts);
1028
+ const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
1029
+ const inferredName = opts.workspaceName ?? `${basename(folderPath)}-avatar-workspace`;
1030
+ const workspaceName = opts.workspaceName ?? await input({ message: "T\xEAn workspace:", default: inferredName });
1031
+ const workspaceParent = resolve(opts.workspaceParent ?? "..");
1032
+ const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
1033
+ await scaffoldWorkspaceWithSrcSubmodule({
1034
+ workspacePath,
1035
+ workspaceName,
1036
+ srcRemoteUrl: remoteUrl ?? folderPath,
1037
+ // fallback local path nếu user từ chối tạo remote
1038
+ teamOwner,
1039
+ description: opts.description ?? `Avatar workspace cho folder ${folderPath}`,
1040
+ packVersion: opts.packVersion,
1041
+ autoYes: opts.yes,
1042
+ flow: "existing-folder"
572
1043
  });
573
- const inferredName = inferWorkspaceName(clientRepoUrl);
574
- const workspaceName = opts.workspaceName ?? await input({
575
- message: "T\xEAn workspace:",
576
- default: inferredName
1044
+ }
1045
+ async function runInitFromScratch(opts, ownerEmail) {
1046
+ await ensureGitHubReady();
1047
+ const projectName = opts.workspaceName ?? await input({
1048
+ message: "T\xEAn d\u1EF1 \xE1n:",
1049
+ validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
577
1050
  });
578
- const workspaceParent = resolve(opts.workspaceParent ?? "..");
579
- const workspacePath = join8(workspaceParent, workspaceName);
580
- if (await pathExists(workspacePath)) {
581
- throw new Error(`Workspace path \u0111\xE3 t\u1ED3n t\u1EA1i: ${workspacePath}`);
582
- }
1051
+ const visibility = opts.repoVisibility ?? await select({
1052
+ message: "Visibility?",
1053
+ choices: [
1054
+ { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
1055
+ { name: "public", value: "public" }
1056
+ ]
1057
+ });
1058
+ const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
1059
+ const workspaceParent = resolve(opts.workspaceParent ?? ".");
1060
+ const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
1061
+ const srcPath = join13(workspacePath, "src");
583
1062
  await ensureDir(workspacePath);
1063
+ await ensureDir(srcPath);
1064
+ await bootstrapGitInFolder(srcPath);
1065
+ const urls = createGithubRemoteFromFolder({
1066
+ folder: srcPath,
1067
+ name: projectName,
1068
+ visibility,
1069
+ org: opts.repoOrg
1070
+ });
584
1071
  await git(workspacePath).init();
585
- const sp = spinner("Add submodule client repo + team-ai-pack...");
1072
+ const sp = spinner("Add submodule src/ + team-ai-pack...");
586
1073
  try {
587
- await git(workspacePath).subModule(["add", clientRepoUrl, "src"]);
1074
+ await git(workspacePath).subModule(["add", urls.sshUrl, "src"]);
588
1075
  const result = await addTeamPackSubmodule(workspacePath, opts.packVersion);
589
- sp.succeed(`Workspace pin team-ai-pack v\xE0o ${result.pinnedTag ?? "HEAD"}`);
590
- const vars = buildScaffoldVariables({
591
- projectName: workspaceName,
592
- projectDescription: `Avatar ${mode} workspace for ${clientRepoUrl}`,
1076
+ sp.succeed(`Pin team-ai-pack v\xE0o ${result.pinnedTag ?? "HEAD"}`);
1077
+ await finalizeWorkspaceScaffold({
1078
+ workspacePath,
1079
+ workspaceName: projectName,
593
1080
  teamOwner,
1081
+ description: opts.description ?? `D\u1EF1 \xE1n m\u1EDBi: ${projectName}`,
594
1082
  packVersion: result.pinnedTag ?? "HEAD",
595
- mode
1083
+ autoYes: opts.yes,
1084
+ flow: "new-project"
596
1085
  });
597
- await createClaudeDirTree(workspacePath);
598
- await writeProjectKnowledgeFiles(workspacePath, vars);
599
- await writeRootClaudeMd(workspacePath, vars);
600
- await writeProjectSettings(workspacePath, vars);
601
- await appendGitignoreEntries(workspacePath);
602
- await ensureDir(join8(workspacePath, "notes"));
603
- await ensureDir(join8(workspacePath, "scripts"));
604
- await installGitHook(join8(workspacePath, ".git"), "post-merge");
605
- await installGitHook(join8(workspacePath, "src", ".git"), "pre-push");
606
- log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
607
- await appendAuditEntry("init", `mode=${mode},workspace=${workspaceName}`);
608
- await maybeCommitWorkspace(workspacePath);
609
- printInitSuccessBox(workspacePath, mode);
610
1086
  } catch (err) {
611
1087
  sp.fail("Init workspace th\u1EA5t b\u1EA1i");
612
1088
  throw err;
613
1089
  }
614
1090
  }
615
- function projectNameOf(projectRoot) {
616
- return projectRoot.split("/").filter(Boolean).pop() ?? "avatar-project";
617
- }
618
- function inferWorkspaceName(repoUrl) {
619
- const m = repoUrl.match(/[/:]([^/]+?)(\.git)?$/);
620
- const base = m?.[1] ?? "client";
621
- return `avatar-${base}-workspace`;
622
- }
623
- async function promptTeamOwner(currentUserEmail) {
624
- return await input({
625
- message: "Team owner email:",
626
- default: currentUserEmail
1091
+ async function getOrCreateOriginRemote(folderPath, opts) {
1092
+ const remotes = await git(folderPath).getRemotes(true);
1093
+ const origin = remotes.find((r) => r.name === "origin");
1094
+ if (origin?.refs.push) {
1095
+ log.success(`Folder \u0111\xE3 c\xF3 remote origin: ${origin.refs.push}`);
1096
+ return origin.refs.push;
1097
+ }
1098
+ const shouldCreate = opts.createRemote ?? await confirm({
1099
+ message: "Folder ch\u01B0a c\xF3 remote. T\u1EA1o GitHub repo ngay \u0111\u1EC3 share team?",
1100
+ default: true
1101
+ });
1102
+ if (!shouldCreate) {
1103
+ log.warn("Ti\u1EBFp t\u1EE5c v\u1EDBi local path. Workspace ch\u1EC9 ch\u1EA1y \u0111\u01B0\u1EE3c tr\xEAn m\xE1y b\u1EA1n.");
1104
+ return void 0;
1105
+ }
1106
+ await ensureGitHubReady();
1107
+ const visibility = opts.repoVisibility ?? await select({
1108
+ message: "Visibility?",
1109
+ choices: [
1110
+ { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
1111
+ { name: "public", value: "public" }
1112
+ ]
1113
+ });
1114
+ const repoName = await input({
1115
+ message: "T\xEAn repo:",
1116
+ default: basename(folderPath)
1117
+ });
1118
+ const urls = createGithubRemoteFromFolder({
1119
+ folder: folderPath,
1120
+ name: repoName,
1121
+ visibility,
1122
+ org: opts.repoOrg
627
1123
  });
1124
+ return urls.sshUrl;
628
1125
  }
629
- function buildScaffoldVariables(args) {
630
- return {
631
- projectName: args.projectName,
632
- projectDescription: args.projectDescription,
1126
+ async function scaffoldWorkspaceWithSrcSubmodule(args) {
1127
+ await ensureDir(args.workspacePath);
1128
+ await git(args.workspacePath).init();
1129
+ const sp = spinner("Add submodule src/ + team-ai-pack...");
1130
+ try {
1131
+ await git(args.workspacePath).subModule(["add", args.srcRemoteUrl, "src"]);
1132
+ const result = await addTeamPackSubmodule(args.workspacePath, args.packVersion);
1133
+ sp.succeed(`Pin team-ai-pack v\xE0o ${result.pinnedTag ?? "HEAD"}`);
1134
+ await finalizeWorkspaceScaffold({
1135
+ workspacePath: args.workspacePath,
1136
+ workspaceName: args.workspaceName,
1137
+ teamOwner: args.teamOwner,
1138
+ description: args.description,
1139
+ packVersion: result.pinnedTag ?? "HEAD",
1140
+ autoYes: args.autoYes,
1141
+ flow: args.flow
1142
+ });
1143
+ } catch (err) {
1144
+ sp.fail("Init workspace th\u1EA5t b\u1EA1i");
1145
+ throw err;
1146
+ }
1147
+ }
1148
+ async function finalizeWorkspaceScaffold(args) {
1149
+ const vars = buildScaffoldVariables({
1150
+ projectName: args.workspaceName,
1151
+ projectDescription: args.description,
633
1152
  teamOwner: args.teamOwner,
634
- avatarVersion: AVATAR_CLI_VERSION,
635
1153
  packVersion: args.packVersion,
636
- lastScan: (/* @__PURE__ */ new Date()).toISOString(),
637
- mode: args.mode
638
- };
639
- }
640
- async function maybeCommit(projectRoot, mode) {
641
- const wantCommit = await confirm({
642
- message: "Commit ngay c\xE1c file Avatar \u0111\xE3 t\u1EA1o?",
643
- default: true
644
- });
645
- if (!wantCommit) return;
646
- const g = git(projectRoot);
647
- await g.add([".claude/", "CLAUDE.md", ".gitignore", ".gitmodules"]);
648
- await g.commit(`chore: initialize Avatar in ${mode} mode`);
649
- log.success("\u0110\xE3 commit");
650
- }
651
- async function maybeCommitWorkspace(workspacePath) {
652
- const wantCommit = await confirm({
653
- message: "Commit workspace ngay?",
654
- default: true
1154
+ mode: "client"
655
1155
  });
1156
+ await createClaudeDirTree(args.workspacePath);
1157
+ await writeProjectKnowledgeFiles(args.workspacePath, vars);
1158
+ await writeRootClaudeMd(args.workspacePath, vars);
1159
+ await writeProjectSettings(args.workspacePath, vars);
1160
+ await appendGitignoreEntries(args.workspacePath);
1161
+ await ensureDir(join13(args.workspacePath, "notes"));
1162
+ await ensureDir(join13(args.workspacePath, "scripts"));
1163
+ await installGitHook(join13(args.workspacePath, ".git"), "post-merge");
1164
+ await installGitHook(join13(args.workspacePath, ".git", "modules", "src"), "pre-push");
1165
+ log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
1166
+ await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
1167
+ await maybeCommitWorkspace(args.workspacePath, args.autoYes);
1168
+ printInitSuccessBox(args.workspacePath, args.flow);
1169
+ }
1170
+ async function resolveWorkspacePath(parent, desiredName, force) {
1171
+ const desired = join13(parent, desiredName);
1172
+ if (await isEmptyOrMissing(desired)) return desired;
1173
+ const alternative = await findAlternativeWorkspaceName(parent, desiredName);
1174
+ if (!alternative) {
1175
+ throw new Error(`Kh\xF4ng t\xECm \u0111\u01B0\u1EE3c workspace path kh\u1EA3 d\u1EE5ng trong ${parent}`);
1176
+ }
1177
+ log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
1178
+ if (force) {
1179
+ log.info(`--force: d\xF9ng ${alternative}`);
1180
+ return alternative;
1181
+ }
1182
+ const useAlt = await confirm({ message: `D\xF9ng "${alternative}" thay th\u1EBF?`, default: true });
1183
+ if (!useAlt) throw new Error("H\u1EE7y init. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c.");
1184
+ return alternative;
1185
+ }
1186
+ async function promptTeamOwner(currentUserEmail) {
1187
+ return await input({ message: "Team owner email:", default: currentUserEmail });
1188
+ }
1189
+ async function maybeCommitWorkspace(workspacePath, autoYes) {
1190
+ const wantCommit = autoYes ?? await confirm({ message: "Commit workspace ngay?", default: true });
656
1191
  if (!wantCommit) return;
657
1192
  const g = git(workspacePath);
658
1193
  await g.add(["CLAUDE.md", ".claude/", ".gitignore", ".gitmodules", "notes/", "scripts/"]);
659
- await g.commit("chore: initialize Avatar workspace for client");
1194
+ await g.commit("chore: initialize Avatar workspace");
660
1195
  log.success("\u0110\xE3 commit workspace");
661
1196
  }
662
- function printInitSuccessBox(rootPath, mode) {
663
- const lines = [];
664
- if (mode === "internal") {
665
- lines.push(`${chalk.green("\u2713")} Avatar \u0111\xE3 s\u1EB5n s\xE0ng trong d\u1EF1 \xE1n`);
666
- lines.push("");
667
- lines.push(` ${chalk.cyan("claude")} M\u1EDF Claude Code`);
668
- lines.push(` ${chalk.cyan("avatar status")} Xem snapshot`);
669
- lines.push(` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`);
670
- } else {
671
- lines.push(`${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${rootPath}`);
672
- lines.push("");
673
- lines.push(` ${chalk.cyan(`cd ${rootPath}`)}`);
674
- lines.push(` ${chalk.cyan("claude")} M\u1EDF Claude Code \u1EDF workspace root`);
675
- lines.push("");
676
- lines.push(` ${chalk.cyan("avatar commit --src")} Commit code kh\xE1ch l\xEAn client remote`);
677
- lines.push(
678
- ` ${chalk.cyan("avatar commit --avatar")} Commit Avatar state l\xEAn workspace remote`
679
- );
680
- lines.push(` ${chalk.cyan("avatar sync")} Sync team pack`);
681
- }
1197
+ function printInitSuccessBox(rootPath, flow) {
1198
+ const lines = [
1199
+ `${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative2(process.cwd(), rootPath) || rootPath}`,
1200
+ ` ${chalk.dim(`(flow: ${flow})`)}`,
1201
+ "",
1202
+ ` ${chalk.cyan(`cd ${rootPath}`)}`,
1203
+ ` ${chalk.cyan("claude")} M\u1EDF Claude Code \u1EDF workspace root`,
1204
+ "",
1205
+ ` ${chalk.cyan("avatar commit --src")} Commit code l\xEAn remote src`,
1206
+ ` ${chalk.cyan("avatar commit --avatar")} Commit Avatar state`,
1207
+ ` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
1208
+ ` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
1209
+ ];
682
1210
  process.stdout.write(`${boxen2(lines.join("\n"), { padding: 1, borderStyle: "round" })}
683
1211
  `);
684
1212
  }
@@ -804,6 +1332,7 @@ function registerLoginCommand(program2) {
804
1332
  });
805
1333
  }
806
1334
  async function runLogin(opts) {
1335
+ printAvatarBanner({ tagline: "\u0110\u0103ng nh\u1EADp Google SSO \xB7 workspace @nal.vn" });
807
1336
  if (opts.reset) {
808
1337
  await clearUserConfig();
809
1338
  await appendAuditEntry("login_reset");
@@ -904,22 +1433,22 @@ function registerSecretsCommand(program2) {
904
1433
 
905
1434
  // src/commands/status.ts
906
1435
  import { promises as fs6 } from "fs";
907
- import { join as join10 } from "path";
1436
+ import { join as join15 } from "path";
908
1437
  import boxen4 from "boxen";
909
1438
 
910
1439
  // src/lib/pack-backup-manager.ts
911
1440
  import { promises as fs5 } from "fs";
912
- import { join as join9 } from "path";
1441
+ import { join as join14 } from "path";
913
1442
  var BACKUP_DIR_NAME = "_backup";
914
1443
  async function listBackups(projectRoot) {
915
- const dir = join9(projectRoot, ".claude", BACKUP_DIR_NAME);
1444
+ const dir = join14(projectRoot, ".claude", BACKUP_DIR_NAME);
916
1445
  if (!await pathExists(dir)) return [];
917
1446
  const entries = await fs5.readdir(dir, { withFileTypes: true });
918
1447
  return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
919
1448
  }
920
1449
 
921
1450
  // src/commands/status.ts
922
- var AVATAR_CLI_VERSION2 = "1.0.0";
1451
+ var AVATAR_CLI_VERSION2 = "1.0.1";
923
1452
  function registerStatusCommand(program2) {
924
1453
  program2.command("status").description("Snapshot t\u1EE9c th\xEC: project, pack version, pending, backup").option("--json", "Output JSON cho script").action(async (opts) => {
925
1454
  try {
@@ -938,7 +1467,7 @@ function registerStatusCommand(program2) {
938
1467
  }
939
1468
  async function gatherStatus(cwd) {
940
1469
  const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
941
- const claudeRoot = join10(cwd, ".claude");
1470
+ const claudeRoot = join15(cwd, ".claude");
942
1471
  const hasAvatar = await pathExists(claudeRoot);
943
1472
  if (!hasAvatar) {
944
1473
  return {
@@ -951,8 +1480,8 @@ async function gatherStatus(cwd) {
951
1480
  hasAvatar: false
952
1481
  };
953
1482
  }
954
- const packVersion = await isGitRepo(join10(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
955
- const pendingDir = join10(claudeRoot, "_pending");
1483
+ const packVersion = await isGitRepo(join15(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
1484
+ const pendingDir = join15(claudeRoot, "_pending");
956
1485
  const pendingCount = await pathExists(pendingDir) ? (await fs6.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
957
1486
  const backupCount = (await listBackups(cwd)).length;
958
1487
  const techStackSummary = await readTechStackFirstLine(claudeRoot);
@@ -967,7 +1496,7 @@ async function gatherStatus(cwd) {
967
1496
  };
968
1497
  }
969
1498
  async function readTechStackFirstLine(claudeRoot) {
970
- const techStackPath = join10(claudeRoot, "project", "tech-stack.md");
1499
+ const techStackPath = join15(claudeRoot, "project", "tech-stack.md");
971
1500
  if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
972
1501
  const content = await readText(techStackPath);
973
1502
  const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
@@ -1000,10 +1529,259 @@ function registerToolsCommand(program2) {
1000
1529
  tools.command("remove <tool-id>").description("G\u1EE1 tool kh\u1ECFi ~/.claude.json (optional uninstall binary)").option("--keep-secrets", "Kh\xF4ng x\xF3a secrets kh\u1ECFi keychain").option("--keep-binary", "Kh\xF4ng uninstall npm global binary").action(notImplementedYet("tools remove", "Milestone 09"));
1001
1530
  }
1002
1531
 
1532
+ // src/commands/uninstall.ts
1533
+ import { relative as relative3 } from "path";
1534
+ import { confirm as confirm2 } from "@inquirer/prompts";
1535
+ import boxen5 from "boxen";
1536
+
1537
+ // src/lib/create-uninstall-backup-snapshot.ts
1538
+ import { cp, mkdir, writeFile } from "fs/promises";
1539
+ import { homedir as homedir2 } from "os";
1540
+ import { basename as basename2, join as join16 } from "path";
1541
+ var UNINSTALL_BACKUPS_DIR = join16(homedir2(), ".avatar", "uninstall-backups");
1542
+ async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersion) {
1543
+ const projectName = basename2(projectRoot);
1544
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1545
+ const backupDir = join16(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
1546
+ await mkdir(backupDir, { recursive: true, mode: 448 });
1547
+ if (artifacts.claudeDir) {
1548
+ await cp(artifacts.claudeDir, join16(backupDir, ".claude"), { recursive: true });
1549
+ }
1550
+ if (artifacts.claudeMd) {
1551
+ await cp(artifacts.claudeMd, join16(backupDir, "CLAUDE.md"));
1552
+ }
1553
+ if (artifacts.postMergeHook || artifacts.prePushHook) {
1554
+ const hooksBackupDir = join16(backupDir, "hooks");
1555
+ await mkdir(hooksBackupDir, { recursive: true });
1556
+ if (artifacts.postMergeHook) {
1557
+ await cp(artifacts.postMergeHook, join16(hooksBackupDir, "post-merge"));
1558
+ }
1559
+ if (artifacts.prePushHook) {
1560
+ await cp(artifacts.prePushHook, join16(hooksBackupDir, "pre-push"));
1561
+ }
1562
+ }
1563
+ const manifest = {
1564
+ projectName,
1565
+ projectPath: projectRoot,
1566
+ timestamp,
1567
+ avatarVersion,
1568
+ artifacts: {
1569
+ claudeDir: !!artifacts.claudeDir,
1570
+ claudeMd: !!artifacts.claudeMd,
1571
+ postMergeHook: !!artifacts.postMergeHook,
1572
+ prePushHook: !!artifacts.prePushHook
1573
+ }
1574
+ };
1575
+ await writeFile(join16(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
1576
+ return backupDir;
1577
+ }
1578
+
1579
+ // src/lib/detect-avatar-project-artifacts.ts
1580
+ import { existsSync as existsSync5 } from "fs";
1581
+ import { join as join17 } from "path";
1582
+ function existsOrNull(path) {
1583
+ return existsSync5(path) ? path : null;
1584
+ }
1585
+ function detectAvatarProjectArtifacts(projectRoot) {
1586
+ const claudeDir = existsOrNull(join17(projectRoot, ".claude"));
1587
+ const claudeMd = existsOrNull(join17(projectRoot, "CLAUDE.md"));
1588
+ const postMergeHook = existsOrNull(join17(projectRoot, ".git", "hooks", "post-merge"));
1589
+ const prePushHook = existsOrNull(
1590
+ join17(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
1591
+ );
1592
+ const gitignorePath = existsOrNull(join17(projectRoot, ".gitignore"));
1593
+ const gitmodulesPath = existsOrNull(join17(projectRoot, ".gitmodules"));
1594
+ const notesDir = existsOrNull(join17(projectRoot, "notes"));
1595
+ const scriptsDir = existsOrNull(join17(projectRoot, "scripts"));
1596
+ const hasAnyArtifact = !!(claudeDir || claudeMd || postMergeHook || prePushHook);
1597
+ return {
1598
+ hasAnyArtifact,
1599
+ claudeDir,
1600
+ claudeMd,
1601
+ postMergeHook,
1602
+ prePushHook,
1603
+ gitignorePath,
1604
+ gitmodulesPath,
1605
+ notesDir,
1606
+ scriptsDir
1607
+ };
1608
+ }
1609
+
1610
+ // src/lib/execute-uninstall-deletion.ts
1611
+ import { readFile, rm, writeFile as writeFile2 } from "fs/promises";
1612
+ async function executeUninstallDeletion(artifacts, flags) {
1613
+ if (artifacts.claudeDir) {
1614
+ if (flags.keepSubmodule) {
1615
+ const { readdir: readdir2 } = await import("fs/promises");
1616
+ const { join: join18 } = await import("path");
1617
+ const entries = await readdir2(artifacts.claudeDir);
1618
+ for (const entry of entries) {
1619
+ if (entry === "pack") continue;
1620
+ await rm(join18(artifacts.claudeDir, entry), { recursive: true, force: true });
1621
+ }
1622
+ } else {
1623
+ await rm(artifacts.claudeDir, { recursive: true, force: true });
1624
+ }
1625
+ }
1626
+ if (artifacts.claudeMd) {
1627
+ await rm(artifacts.claudeMd, { force: true });
1628
+ }
1629
+ if (!flags.keepHooks) {
1630
+ if (artifacts.postMergeHook) await rm(artifacts.postMergeHook, { force: true });
1631
+ if (artifacts.prePushHook) await rm(artifacts.prePushHook, { force: true });
1632
+ }
1633
+ if (artifacts.gitignorePath) {
1634
+ await stripAvatarBlockFromGitignore(artifacts.gitignorePath);
1635
+ }
1636
+ if (artifacts.gitmodulesPath && !flags.keepSubmodule) {
1637
+ await removeSubmoduleEntry(artifacts.gitmodulesPath, ".claude/pack");
1638
+ }
1639
+ for (const dir of [artifacts.notesDir, artifacts.scriptsDir]) {
1640
+ if (!dir) continue;
1641
+ const { readdir: readdir2 } = await import("fs/promises");
1642
+ const entries = await readdir2(dir);
1643
+ if (entries.length === 0) {
1644
+ await rm(dir, { recursive: true, force: true });
1645
+ }
1646
+ }
1647
+ }
1648
+ async function stripAvatarBlockFromGitignore(path) {
1649
+ const content = await readFile(path, "utf8");
1650
+ const startIdx = content.indexOf(AVATAR_MARKER_START);
1651
+ const endIdx = content.indexOf(AVATAR_MARKER_END);
1652
+ if (startIdx === -1 || endIdx === -1) return;
1653
+ const before = content.slice(0, startIdx);
1654
+ const after = content.slice(endIdx + AVATAR_MARKER_END.length);
1655
+ const cleaned = `${before.trimEnd()}
1656
+ ${after.trimStart()}`.trim();
1657
+ if (cleaned.length === 0) {
1658
+ await rm(path, { force: true });
1659
+ } else {
1660
+ await writeFile2(path, `${cleaned}
1661
+ `, "utf8");
1662
+ }
1663
+ }
1664
+ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
1665
+ const content = await readFile(gitmodulesPath, "utf8");
1666
+ const lines = content.split("\n");
1667
+ const result = [];
1668
+ let skip = false;
1669
+ for (const line of lines) {
1670
+ if (line.trim().startsWith("[submodule") && line.includes(submodulePath)) {
1671
+ skip = true;
1672
+ continue;
1673
+ }
1674
+ if (skip && line.trim().startsWith("[submodule")) {
1675
+ skip = false;
1676
+ }
1677
+ if (!skip) result.push(line);
1678
+ }
1679
+ const cleaned = result.join("\n").trim();
1680
+ if (cleaned.length === 0) {
1681
+ await rm(gitmodulesPath, { force: true });
1682
+ } else {
1683
+ await writeFile2(gitmodulesPath, `${cleaned}
1684
+ `, "utf8");
1685
+ }
1686
+ }
1687
+
1688
+ // src/commands/uninstall.ts
1689
+ var CLI_VERSION = "1.1.0";
1690
+ function registerUninstallCommand(program2) {
1691
+ program2.command("uninstall").description("G\u1EE1 Avatar kh\u1ECFi project \u2014 backup t\u1EF1 \u0111\u1ED9ng (M11)").option("--yes", "Skip confirm prompt").option("--no-backup", "Kh\xF4ng t\u1EA1o backup tr\u01B0\u1EDBc khi x\xF3a (nguy hi\u1EC3m)").option("--keep-submodule", "Gi\u1EEF submodule .claude/pack/").option("--keep-hooks", "Gi\u1EEF git hooks post-merge, pre-push").option("--dry-run", "Hi\u1EC3n th\u1ECB danh s\xE1ch s\u1EBD x\xF3a, kh\xF4ng th\u1EF1c thi").action(async (opts) => {
1692
+ try {
1693
+ await runUninstall(opts);
1694
+ } catch (err) {
1695
+ log.error(err instanceof Error ? err.message : String(err));
1696
+ process.exit(1);
1697
+ }
1698
+ });
1699
+ }
1700
+ async function runUninstall(opts) {
1701
+ const projectRoot = process.cwd();
1702
+ const artifacts = detectAvatarProjectArtifacts(projectRoot);
1703
+ if (!artifacts.hasAnyArtifact) {
1704
+ log.info("Project ch\u01B0a c\xE0i Avatar \u2014 kh\xF4ng c\xF3 g\xEC \u0111\u1EC3 g\u1EE1.");
1705
+ return;
1706
+ }
1707
+ printUninstallSummary(projectRoot, artifacts, opts);
1708
+ if (opts.dryRun) {
1709
+ log.dim("--dry-run: k\u1EBFt th\xFAc, kh\xF4ng x\xF3a.");
1710
+ return;
1711
+ }
1712
+ if (!opts.yes) {
1713
+ const ok = await confirm2({
1714
+ message: "Ti\u1EBFp t\u1EE5c g\u1EE1 Avatar?",
1715
+ default: false
1716
+ });
1717
+ if (!ok) {
1718
+ log.info("\u0110\xE3 h\u1EE7y.");
1719
+ return;
1720
+ }
1721
+ }
1722
+ let backupPath = null;
1723
+ if (!opts.noBackup) {
1724
+ backupPath = await createUninstallBackupSnapshot(projectRoot, artifacts, CLI_VERSION);
1725
+ log.success(`Backup t\u1EA1o t\u1EA1i: ${backupPath}`);
1726
+ }
1727
+ await executeUninstallDeletion(artifacts, {
1728
+ keepSubmodule: opts.keepSubmodule,
1729
+ keepHooks: opts.keepHooks
1730
+ });
1731
+ await appendAuditEntry("uninstall", `project=${projectRoot},backup=${backupPath ?? "skipped"}`);
1732
+ printUninstallSuccessBox(backupPath);
1733
+ }
1734
+ function printUninstallSummary(projectRoot, artifacts, opts) {
1735
+ log.info(`Project: ${projectRoot}`);
1736
+ log.plain("");
1737
+ log.plain("C\xE1c artifact s\u1EBD g\u1EE1:");
1738
+ if (artifacts.claudeDir)
1739
+ log.plain(` ${chalk.red("\u2717")} ${relative3(projectRoot, artifacts.claudeDir) || ".claude/"}`);
1740
+ if (artifacts.claudeMd) log.plain(` ${chalk.red("\u2717")} CLAUDE.md`);
1741
+ if (artifacts.postMergeHook && !opts.keepHooks) {
1742
+ log.plain(` ${chalk.red("\u2717")} .git/hooks/post-merge`);
1743
+ }
1744
+ if (artifacts.prePushHook && !opts.keepHooks) {
1745
+ log.plain(` ${chalk.red("\u2717")} .git/modules/src/hooks/pre-push`);
1746
+ }
1747
+ if (artifacts.gitignorePath) log.plain(` ${chalk.yellow("\u270E")} .gitignore (g\u1EE1 Avatar block)`);
1748
+ if (artifacts.gitmodulesPath && !opts.keepSubmodule) {
1749
+ log.plain(` ${chalk.yellow("\u270E")} .gitmodules (g\u1EE1 entry .claude/pack)`);
1750
+ }
1751
+ log.plain("");
1752
+ log.plain("Kh\xF4ng \u0111\u1EE5ng:");
1753
+ log.plain(` ${chalk.green("\u2713")} src/ (code kh\xE1ch)`);
1754
+ log.plain(` ${chalk.green("\u2713")} Git history`);
1755
+ log.plain(` ${chalk.green("\u2713")} ~/.avatar/config.json (token SSO)`);
1756
+ log.plain(` ${chalk.green("\u2713")} Secrets trong keychain`);
1757
+ log.plain("");
1758
+ }
1759
+ function printUninstallSuccessBox(backupPath) {
1760
+ const lines = [`${chalk.green("\u2713")} Avatar \u0111\xE3 \u0111\u01B0\u1EE3c g\u1EE1 kh\u1ECFi project`];
1761
+ if (backupPath) {
1762
+ lines.push("");
1763
+ lines.push(` ${chalk.dim("Backup:")} ${backupPath}`);
1764
+ lines.push(` ${chalk.dim("Restore:")} ${chalk.cyan(`cp -r "${backupPath}"/* .`)}`);
1765
+ }
1766
+ process.stdout.write(`${boxen5(lines.join("\n"), { padding: 1, borderStyle: "round" })}
1767
+ `);
1768
+ }
1769
+
1003
1770
  // src/index.ts
1004
- var CLI_VERSION = "1.0.0";
1771
+ var CLI_VERSION2 = "1.1.0";
1005
1772
  var program = new Command();
1006
- program.name("avatar").description("AI harness CLI for NAL Vietnam engineering").version(CLI_VERSION, "-v, --version", "Hi\u1EC3n th\u1ECB phi\xEAn b\u1EA3n Avatar CLI");
1773
+ program.name("avatar").description("AI harness CLI for NAL Vietnam engineering").version(CLI_VERSION2, "-v, --version", "Hi\u1EC3n th\u1ECB phi\xEAn b\u1EA3n Avatar CLI").addHelpText(
1774
+ "beforeAll",
1775
+ () => `
1776
+ ${renderAvatarBanner({ tagline: `v${CLI_VERSION2} \xB7 AI harness CLI for NAL Vietnam` })}
1777
+
1778
+ `
1779
+ );
1780
+ var isVersionCall = process.argv.includes("-v") || process.argv.includes("--version");
1781
+ if (isVersionCall) {
1782
+ printAvatarBanner({ tagline: `v${CLI_VERSION2} \xB7 AI harness CLI for NAL Vietnam` });
1783
+ process.exit(0);
1784
+ }
1007
1785
  registerLoginCommand(program);
1008
1786
  registerInitCommand(program);
1009
1787
  registerSyncCommand(program);
@@ -1016,6 +1794,7 @@ registerCommitCommand(program);
1016
1794
  registerToolsCommand(program);
1017
1795
  registerSecretsCommand(program);
1018
1796
  registerMcpRunCommand(program);
1797
+ registerUninstallCommand(program);
1019
1798
  program.parseAsync(process.argv).catch((err) => {
1020
1799
  const msg = err instanceof Error ? err.message : String(err);
1021
1800
  process.stderr.write(`\u2717 L\u1ED7i kh\xF4ng x\u1EED l\xFD \u0111\u01B0\u1EE3c: ${msg}