@nalvietnam/avatar-cli 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { Command } from "commander";
5
5
 
6
6
  // src/commands/ai.ts
7
7
  import { promises as fs4 } from "fs";
8
- import { join as join5 } from "path";
8
+ import { join as join6 } from "path";
9
9
  import { confirm } from "@inquirer/prompts";
10
10
 
11
11
  // src/lib/filesystem-helpers.ts
@@ -42,12 +42,44 @@ async function writeJsonAtomic(path, data, mode) {
42
42
  `, mode);
43
43
  }
44
44
 
45
+ // src/lib/resolve-avatar-workspace-root-from-cwd.ts
46
+ import { existsSync, readFileSync } from "fs";
47
+ import { dirname as dirname2, join as join2 } from "path";
48
+ var MAX_WALKUP_LEVELS = 5;
49
+ function isAvatarWorkspace(dir) {
50
+ const hasClaudeDir = existsSync(join2(dir, ".claude"));
51
+ const hasClaudeMd = existsSync(join2(dir, "CLAUDE.md"));
52
+ if (!hasClaudeDir || !hasClaudeMd) return false;
53
+ const hasSrcDir = existsSync(join2(dir, "src"));
54
+ const gitmodulesPath = join2(dir, ".gitmodules");
55
+ if (hasSrcDir) return true;
56
+ if (existsSync(gitmodulesPath)) {
57
+ try {
58
+ const content = readFileSync(gitmodulesPath, "utf8");
59
+ return content.includes("path = src") || content.includes("path = ./src");
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+ function resolveAvatarWorkspaceRootFromCwd(startDir) {
67
+ let current = startDir;
68
+ for (let i = 0; i < MAX_WALKUP_LEVELS; i++) {
69
+ if (isAvatarWorkspace(current)) return current;
70
+ const parent = dirname2(current);
71
+ if (parent === current) return null;
72
+ current = parent;
73
+ }
74
+ return null;
75
+ }
76
+
45
77
  // src/lib/audit-log-appender.ts
46
78
  import { promises as fs2 } from "fs";
47
79
 
48
80
  // src/lib/user-config-store.ts
49
81
  import { homedir } from "os";
50
- import { join as join2 } from "path";
82
+ import { join as join3 } from "path";
51
83
 
52
84
  // src/types/config-schema.ts
53
85
  import { z } from "zod";
@@ -80,11 +112,11 @@ var projectSettingsSchema = z.object({
80
112
  var initModeSchema = z.enum(["internal", "client", "library"]);
81
113
 
82
114
  // src/lib/user-config-store.ts
83
- var AVATAR_HOME = join2(homedir(), ".avatar");
84
- var USER_CONFIG_PATH = join2(AVATAR_HOME, "config.json");
85
- var USER_STATE_PATH = join2(AVATAR_HOME, "state.json");
86
- var AUDIT_LOG_PATH = join2(AVATAR_HOME, "audit.log");
87
- var BACKUPS_DIR = join2(AVATAR_HOME, "backups");
115
+ var AVATAR_HOME = join3(homedir(), ".avatar");
116
+ var USER_CONFIG_PATH = join3(AVATAR_HOME, "config.json");
117
+ var USER_STATE_PATH = join3(AVATAR_HOME, "state.json");
118
+ var AUDIT_LOG_PATH = join3(AVATAR_HOME, "audit.log");
119
+ var BACKUPS_DIR = join3(AVATAR_HOME, "backups");
88
120
  var SECRET_FILE_MODE = 384;
89
121
  async function ensureAvatarHome() {
90
122
  await ensureDir(AVATAR_HOME);
@@ -348,18 +380,18 @@ function installClaudeCodeViaNpm() {
348
380
  }
349
381
 
350
382
  // src/lib/prompt-ai-provider-choice.ts
351
- import { readFileSync } from "fs";
383
+ import { readFileSync as readFileSync2 } from "fs";
352
384
  import { homedir as homedir2 } from "os";
353
- import { join as join3 } from "path";
385
+ import { join as join4 } from "path";
354
386
  import { select } from "@inquirer/prompts";
355
387
  function getGlobalSettingsPath() {
356
- return join3(homedir2(), ".claude", "settings.json");
388
+ return join4(homedir2(), ".claude", "settings.json");
357
389
  }
358
390
  function detectGlobalClaudeSettings() {
359
391
  const path = getGlobalSettingsPath();
360
392
  let raw;
361
393
  try {
362
- raw = readFileSync(path, "utf8");
394
+ raw = readFileSync2(path, "utf8");
363
395
  } catch {
364
396
  return { exists: false, hasBaseUrl: false, hasToken: false };
365
397
  }
@@ -497,10 +529,10 @@ async function setupLLMLiteApiKeyAndModel() {
497
529
 
498
530
  // src/lib/write-claude-settings-json-per-project.ts
499
531
  import { promises as fs3 } from "fs";
500
- import { join as join4 } from "path";
532
+ import { join as join5 } from "path";
501
533
  var SECRET_FILE_MODE2 = 384;
502
534
  function getClaudeSettingsPath(workspacePath) {
503
- return join4(workspacePath, ".claude", "settings.json");
535
+ return join5(workspacePath, ".claude", "settings.json");
504
536
  }
505
537
  async function readExistingSettings(path) {
506
538
  if (!await pathExists(path)) return {};
@@ -752,15 +784,23 @@ async function testAiProviderByDetectedMode(settings) {
752
784
  // src/commands/ai.ts
753
785
  async function ensureWorkspaceCwd() {
754
786
  const cwd = process.cwd();
755
- const claudeDir = join5(cwd, ".claude");
756
- if (!await pathExists(claudeDir)) {
757
- log.error("Kh\xF4ng th\u1EA5y .claude/ trong th\u01B0 m\u1EE5c hi\u1EC7n t\u1EA1i. Ch\u1EA1y l\u1EC7nh n\xE0y trong workspace Avatar.");
787
+ const workspaceRoot = resolveAvatarWorkspaceRootFromCwd(cwd);
788
+ if (!workspaceRoot) {
789
+ log.error(
790
+ `Kh\xF4ng t\xECm th\u1EA5y Avatar workspace t\u1EEB th\u01B0 m\u1EE5c hi\u1EC7n t\u1EA1i.
791
+ Avatar workspace c\u1EA7n c\xF3: .claude/ + CLAUDE.md + src/ (ho\u1EB7c .gitmodules).
792
+ B\u1EA1n \u0111ang \u1EDF: ${cwd}
793
+ Cd v\xE0o workspace dir (vd /path/to/<project>-workspace) r\u1ED3i ch\u1EA1y l\u1EA1i.`
794
+ );
758
795
  process.exit(1);
759
796
  }
760
- return cwd;
797
+ if (workspaceRoot !== cwd) {
798
+ log.dim(`Detected workspace root: ${workspaceRoot}`);
799
+ }
800
+ return workspaceRoot;
761
801
  }
762
802
  async function readWorkspaceSettings(workspacePath) {
763
- const settingsPath = join5(workspacePath, ".claude", "settings.json");
803
+ const settingsPath = join6(workspacePath, ".claude", "settings.json");
764
804
  if (!await pathExists(settingsPath)) return {};
765
805
  try {
766
806
  return await readJson(settingsPath);
@@ -798,7 +838,7 @@ async function runAiTest() {
798
838
  }
799
839
  async function runAiReset(opts) {
800
840
  const workspacePath = await ensureWorkspaceCwd();
801
- const settingsPath = join5(workspacePath, ".claude", "settings.json");
841
+ const settingsPath = join6(workspacePath, ".claude", "settings.json");
802
842
  const settings = await readWorkspaceSettings(workspacePath);
803
843
  if (!opts.yes) {
804
844
  const ok = await confirm({
@@ -848,25 +888,25 @@ import { input as input2 } from "@inquirer/prompts";
848
888
 
849
889
  // src/lib/execute-commit-with-target-selection.ts
850
890
  import { spawnSync as spawnSync5 } from "child_process";
851
- import { existsSync } from "fs";
852
- import { join as join6 } from "path";
891
+ import { existsSync as existsSync2 } from "fs";
892
+ import { join as join7 } from "path";
853
893
  function assertAvatarWorkspaceRoot(cwd) {
854
- const srcGit = join6(cwd, "src", ".git");
855
- const workspaceGit = join6(cwd, ".git");
856
- const claudeDir = join6(cwd, ".claude");
857
- if (!existsSync(workspaceGit)) {
894
+ const srcGit = join7(cwd, "src", ".git");
895
+ const workspaceGit = join7(cwd, ".git");
896
+ const claudeDir = join7(cwd, ".claude");
897
+ if (!existsSync2(workspaceGit)) {
858
898
  throw new Error(
859
899
  `Kh\xF4ng ph\u1EA3i workspace root: ${cwd}
860
900
  Ch\u1EA1y 'avatar commit' trong workspace dir (c\xF3 .git v\xE0 .claude/).`
861
901
  );
862
902
  }
863
- if (!existsSync(claudeDir)) {
903
+ if (!existsSync2(claudeDir)) {
864
904
  throw new Error(
865
905
  `Kh\xF4ng th\u1EA5y .claude/ trong ${cwd}.
866
906
  Ch\u1EA1y 'avatar commit' trong Avatar workspace, kh\xF4ng ph\u1EA3i project b\xECnh th\u01B0\u1EDDng.`
867
907
  );
868
908
  }
869
- if (!existsSync(srcGit)) {
909
+ if (!existsSync2(srcGit)) {
870
910
  throw new Error(
871
911
  `Kh\xF4ng th\u1EA5y src/.git trong ${cwd}.
872
912
  Workspace thi\u1EBFu submodule src/. Ch\u1EA1y 'avatar init' l\u1EA1i?`
@@ -890,7 +930,7 @@ function isDirty(cwd) {
890
930
  return status.length > 0;
891
931
  }
892
932
  async function commitSrc(workspaceRoot, opts) {
893
- const srcPath = join6(workspaceRoot, "src");
933
+ const srcPath = join7(workspaceRoot, "src");
894
934
  if (!isDirty(srcPath)) {
895
935
  log.dim("src/: nothing to commit (clean)");
896
936
  return {};
@@ -928,24 +968,8 @@ async function commitWorkspace(workspaceRoot, opts) {
928
968
  }
929
969
  return { sha, pushed };
930
970
  }
931
- function warnIfOtherTargetDirty(workspaceRoot, requestedTarget) {
932
- if (requestedTarget === "all") return;
933
- const srcDirty = isDirty(join6(workspaceRoot, "src"));
934
- const workspaceDirty = isDirty(workspaceRoot);
935
- if (requestedTarget === "src" && workspaceDirty) {
936
- log.warn(
937
- "Workspace c\u0169ng c\xF3 changes ch\u01B0a commit. Ch\u1EA1y 'avatar commit workspace' (ho\u1EB7c 'avatar commit all') \u0111\u1EC3 commit lu\xF4n."
938
- );
939
- }
940
- if (requestedTarget === "workspace" && srcDirty) {
941
- log.warn(
942
- "src/ c\u0169ng c\xF3 changes ch\u01B0a commit. Ch\u1EA1y 'avatar commit src' (ho\u1EB7c 'avatar commit all') \u0111\u1EC3 commit lu\xF4n."
943
- );
944
- }
945
- }
946
971
  async function executeCommitWithTargetSelection(args) {
947
972
  assertAvatarWorkspaceRoot(args.workspaceRoot);
948
- warnIfOtherTargetDirty(args.workspaceRoot, args.target);
949
973
  const result = { target: args.target, skipped: [] };
950
974
  if (args.target === "src" || args.target === "all") {
951
975
  const srcOutcome = await commitSrc(args.workspaceRoot, args.options);
@@ -964,18 +988,12 @@ async function executeCommitWithTargetSelection(args) {
964
988
 
965
989
  // src/commands/commit.ts
966
990
  function registerCommitCommand(program2) {
967
- const commit = program2.command("commit").description("Commit code (src/) ho\u1EB7c Avatar state (workspace) \u2014 split-aware (M07)");
991
+ const commit = program2.command("commit").description("Commit code kh\xE1ch trong src/ (Avatar state do admin sync) (M07)");
968
992
  commit.command("src").description("Commit src/ \u2192 push l\xEAn client repo remote").option("-m, --message <msg>", "Commit message").option("--push", "Auto push sau commit (default: ch\u1EC9 commit)").action(async (opts) => {
969
- await runCommit("src", opts);
970
- });
971
- commit.command("workspace").description("Commit workspace root \u2192 push l\xEAn team remote (Avatar state)").option("-m, --message <msg>", "Commit message").option("--push", "Auto push sau commit (default: ch\u1EC9 commit)").action(async (opts) => {
972
- await runCommit("workspace", opts);
973
- });
974
- commit.command("all").description("Commit c\u1EA3 src/ v\xE0 workspace (src tr\u01B0\u1EDBc, gitlink update, workspace sau)").option("-m, --message <msg>", "Commit message").option("--push", "Auto push sau commit (default: ch\u1EC9 commit)").action(async (opts) => {
975
- await runCommit("all", opts);
993
+ await runCommitSrc(opts);
976
994
  });
977
995
  }
978
- async function runCommit(target, opts) {
996
+ async function runCommitSrc(opts) {
979
997
  try {
980
998
  const message = opts.message ?? await input2({
981
999
  message: "Commit message:",
@@ -983,17 +1001,12 @@ async function runCommit(target, opts) {
983
1001
  });
984
1002
  const result = await executeCommitWithTargetSelection({
985
1003
  workspaceRoot: process.cwd(),
986
- target,
1004
+ target: "src",
987
1005
  options: { message, push: opts.push }
988
1006
  });
989
1007
  if (result.srcCommitSha) {
990
1008
  log.success(`src/: ${result.srcCommitSha.slice(0, 7)}${result.srcPushed ? " (pushed)" : ""}`);
991
1009
  }
992
- if (result.workspaceCommitSha) {
993
- log.success(
994
- `workspace: ${result.workspaceCommitSha.slice(0, 7)}${result.workspacePushed ? " (pushed)" : ""}`
995
- );
996
- }
997
1010
  if (result.skipped && result.skipped.length > 0) {
998
1011
  log.dim(`Skipped (nothing to commit): ${result.skipped.join(", ")}`);
999
1012
  }
@@ -1006,23 +1019,23 @@ async function runCommit(target, opts) {
1006
1019
  // src/commands/doctor.ts
1007
1020
  import { spawnSync as spawnSync6 } from "child_process";
1008
1021
  import { promises as fs6 } from "fs";
1009
- import { join as join10 } from "path";
1022
+ import { join as join11 } from "path";
1010
1023
  import boxen from "boxen";
1011
1024
 
1012
1025
  // src/lib/git-operations.ts
1013
- import { join as join7 } from "path";
1026
+ import { join as join8 } from "path";
1014
1027
  import { simpleGit } from "simple-git";
1015
1028
  function git(cwd = process.cwd()) {
1016
1029
  return simpleGit({ baseDir: cwd, binary: "git" });
1017
1030
  }
1018
1031
  async function isGitRepo(cwd = process.cwd()) {
1019
- return await pathExists(join7(cwd, ".git"));
1032
+ return await pathExists(join8(cwd, ".git"));
1020
1033
  }
1021
1034
  async function addSubmodule(repoUrl, destPath, cwd = process.cwd()) {
1022
1035
  await git(cwd).subModule(["add", repoUrl, destPath]);
1023
1036
  }
1024
1037
  async function checkoutTagInSubmodule(submodulePath, tag, cwd = process.cwd()) {
1025
- const submoduleCwd = join7(cwd, submodulePath);
1038
+ const submoduleCwd = join8(cwd, submodulePath);
1026
1039
  await git(submoduleCwd).fetch(["--tags"]);
1027
1040
  await git(submoduleCwd).checkout(tag);
1028
1041
  }
@@ -1041,11 +1054,11 @@ async function currentCommitSha(cwd = process.cwd()) {
1041
1054
 
1042
1055
  // src/lib/project-tree-scaffolder.ts
1043
1056
  import { promises as fs5 } from "fs";
1044
- import { join as join9 } from "path";
1057
+ import { join as join10 } from "path";
1045
1058
 
1046
1059
  // src/lib/template-bundle-loader.ts
1047
- import { existsSync as existsSync2 } from "fs";
1048
- import { dirname as dirname2, join as join8 } from "path";
1060
+ import { existsSync as existsSync3 } from "fs";
1061
+ import { dirname as dirname3, join as join9 } from "path";
1049
1062
  import { fileURLToPath } from "url";
1050
1063
 
1051
1064
  // src/lib/mustache-template-engine.ts
@@ -1059,15 +1072,15 @@ function renderTemplate(source, variables) {
1059
1072
  }
1060
1073
 
1061
1074
  // src/lib/template-bundle-loader.ts
1062
- var HERE = dirname2(fileURLToPath(import.meta.url));
1075
+ var HERE = dirname3(fileURLToPath(import.meta.url));
1063
1076
  var PACKAGE_ROOT = findPackageRoot(HERE);
1064
- var TEMPLATES_ROOT = join8(PACKAGE_ROOT, "src", "templates");
1065
- var HOOKS_ROOT = join8(PACKAGE_ROOT, "src", "hooks");
1077
+ var TEMPLATES_ROOT = join9(PACKAGE_ROOT, "src", "templates");
1078
+ var HOOKS_ROOT = join9(PACKAGE_ROOT, "src", "hooks");
1066
1079
  function findPackageRoot(startDir) {
1067
1080
  let dir = startDir;
1068
1081
  while (true) {
1069
- if (existsSync2(join8(dir, "package.json"))) return dir;
1070
- const parent = dirname2(dir);
1082
+ if (existsSync3(join9(dir, "package.json"))) return dir;
1083
+ const parent = dirname3(dir);
1071
1084
  if (parent === dir) {
1072
1085
  throw new Error(`Cannot locate package root from ${startDir}`);
1073
1086
  }
@@ -1075,14 +1088,14 @@ function findPackageRoot(startDir) {
1075
1088
  }
1076
1089
  }
1077
1090
  async function loadTemplate(name) {
1078
- return await readText(join8(TEMPLATES_ROOT, `${name}.tpl`));
1091
+ return await readText(join9(TEMPLATES_ROOT, `${name}.tpl`));
1079
1092
  }
1080
1093
  async function renderTemplateByName(name, variables) {
1081
1094
  const source = await loadTemplate(name);
1082
1095
  return renderTemplate(source, variables);
1083
1096
  }
1084
1097
  async function loadHook(name) {
1085
- return await readText(join8(HOOKS_ROOT, `${name}.sh.tpl`));
1098
+ return await readText(join9(HOOKS_ROOT, `${name}.sh.tpl`));
1086
1099
  }
1087
1100
 
1088
1101
  // src/lib/project-tree-scaffolder.ts
@@ -1116,12 +1129,12 @@ var PROJECT_KNOWLEDGE_TEMPLATES = [
1116
1129
  "project/gotchas.md"
1117
1130
  ];
1118
1131
  async function createClaudeDirTree(projectRoot) {
1119
- const claudeRoot = join9(projectRoot, ".claude");
1132
+ const claudeRoot = join10(projectRoot, ".claude");
1120
1133
  await ensureDir(claudeRoot);
1121
1134
  for (const sub of CLAUDE_SUBDIRS) {
1122
- const dir = join9(claudeRoot, sub);
1135
+ const dir = join10(claudeRoot, sub);
1123
1136
  await ensureDir(dir);
1124
- await writeTextAtomic(join9(dir, ".gitkeep"), "");
1137
+ await writeTextAtomic(join10(dir, ".gitkeep"), "");
1125
1138
  }
1126
1139
  }
1127
1140
  async function writeProjectKnowledgeFiles(projectRoot, vars) {
@@ -1152,7 +1165,7 @@ async function writeProjectKnowledgeFiles(projectRoot, vars) {
1152
1165
  for (const tpl of PROJECT_KNOWLEDGE_TEMPLATES) {
1153
1166
  const content = await renderTemplateByName(tpl, baseVars);
1154
1167
  const relative4 = tpl.replace(/^project\//, "");
1155
- const outPath = join9(projectRoot, ".claude", "project", relative4);
1168
+ const outPath = join10(projectRoot, ".claude", "project", relative4);
1156
1169
  const backup = await writeWithBackup(outPath, content);
1157
1170
  if (backup) backups.push(backup);
1158
1171
  }
@@ -1160,14 +1173,14 @@ async function writeProjectKnowledgeFiles(projectRoot, vars) {
1160
1173
  }
1161
1174
  async function writeRootClaudeMd(projectRoot, vars) {
1162
1175
  const content = await renderTemplateByName("CLAUDE.md", vars);
1163
- return await writeWithBackup(join9(projectRoot, "CLAUDE.md"), content);
1176
+ return await writeWithBackup(join10(projectRoot, "CLAUDE.md"), content);
1164
1177
  }
1165
1178
  async function writeProjectSettings(projectRoot, vars) {
1166
1179
  const content = await renderTemplateByName("settings.json", vars);
1167
- return await writeWithBackup(join9(projectRoot, ".claude", "settings.json"), content);
1180
+ return await writeWithBackup(join10(projectRoot, ".claude", "settings.json"), content);
1168
1181
  }
1169
1182
  async function appendGitignoreEntries(projectRoot) {
1170
- const path = join9(projectRoot, ".gitignore");
1183
+ const path = join10(projectRoot, ".gitignore");
1171
1184
  const tpl = await renderTemplateByName("gitignore", {});
1172
1185
  const marker = "# Avatar \u2014 git-ignored entries injected on `avatar init`";
1173
1186
  let existing = "";
@@ -1181,9 +1194,9 @@ ${tpl}`);
1181
1194
  }
1182
1195
  async function installGitHook(gitDir, hookName) {
1183
1196
  const content = await loadHook(hookName);
1184
- const hooksDir = join9(gitDir, "hooks");
1197
+ const hooksDir = join10(gitDir, "hooks");
1185
1198
  await ensureDir(hooksDir);
1186
- const dest = join9(hooksDir, hookName);
1199
+ const dest = join10(hooksDir, hookName);
1187
1200
  await writeTextAtomic(dest, content, 493);
1188
1201
  }
1189
1202
 
@@ -1241,7 +1254,7 @@ async function runChecks(cwd) {
1241
1254
  detail: gitRepo ? cwd : "Kh\xF4ng ph\u1EA3i git repo (c\u1EA7n cho 'avatar init')",
1242
1255
  fixable: false
1243
1256
  });
1244
- const packPath = join10(cwd, ".claude", "pack");
1257
+ const packPath = join11(cwd, ".claude", "pack");
1245
1258
  const hasPack = await pathExists(packPath);
1246
1259
  checks.push({
1247
1260
  name: "team-ai-pack submodule",
@@ -1249,7 +1262,7 @@ async function runChecks(cwd) {
1249
1262
  detail: hasPack ? packPath : "Avatar ch\u01B0a init \u2014 ch\u1EA1y 'avatar init'",
1250
1263
  fixable: false
1251
1264
  });
1252
- const claudeMdPath = join10(cwd, "CLAUDE.md");
1265
+ const claudeMdPath = join11(cwd, "CLAUDE.md");
1253
1266
  const hasClaudeMd = await pathExists(claudeMdPath);
1254
1267
  checks.push({
1255
1268
  name: "CLAUDE.md",
@@ -1257,7 +1270,7 @@ async function runChecks(cwd) {
1257
1270
  detail: hasClaudeMd ? "t\u1ED3n t\u1EA1i \u1EDF project root" : "thi\u1EBFu \u2014 ch\u1EA1y 'avatar init'",
1258
1271
  fixable: false
1259
1272
  });
1260
- const hookPath = join10(cwd, ".git", "hooks", "post-merge");
1273
+ const hookPath = join11(cwd, ".git", "hooks", "post-merge");
1261
1274
  const hasHook = await pathExists(hookPath);
1262
1275
  if (gitRepo && hasPack) {
1263
1276
  checks.push({
@@ -1266,11 +1279,11 @@ async function runChecks(cwd) {
1266
1279
  detail: hasHook ? "installed" : "missing \u2014 fixable",
1267
1280
  fixable: !hasHook,
1268
1281
  fix: hasHook ? void 0 : async () => {
1269
- await installGitHook(join10(cwd, ".git"), "post-merge");
1282
+ await installGitHook(join11(cwd, ".git"), "post-merge");
1270
1283
  }
1271
1284
  });
1272
1285
  }
1273
- const gitignorePath = join10(cwd, ".gitignore");
1286
+ const gitignorePath = join11(cwd, ".gitignore");
1274
1287
  if (gitRepo) {
1275
1288
  let gitignoreOk = false;
1276
1289
  if (await pathExists(gitignorePath)) {
@@ -1332,7 +1345,7 @@ async function applyFixes(checks) {
1332
1345
  }
1333
1346
 
1334
1347
  // src/commands/init.ts
1335
- import { basename, join as join17, relative as relative2, resolve } from "path";
1348
+ import { basename, join as join18, relative as relative2, resolve } from "path";
1336
1349
  import { confirm as confirm3, input as input5, select as select8 } from "@inquirer/prompts";
1337
1350
  import boxen4 from "boxen";
1338
1351
 
@@ -1365,7 +1378,7 @@ async function promptRetryOrSkip(args) {
1365
1378
  }
1366
1379
 
1367
1380
  // src/lib/team-pack-submodule-manager.ts
1368
- import { join as join11 } from "path";
1381
+ import { join as join12 } from "path";
1369
1382
 
1370
1383
  // src/lib/check-team-pack-access-with-retry-loop.ts
1371
1384
  import { spawnSync as spawnSync7 } from "child_process";
@@ -1530,7 +1543,7 @@ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
1530
1543
  }
1531
1544
  let target = tag ?? null;
1532
1545
  if (!target) {
1533
- target = await latestTag(join11(projectRoot, TEAM_PACK_RELATIVE_PATH));
1546
+ target = await latestTag(join12(projectRoot, TEAM_PACK_RELATIVE_PATH));
1534
1547
  }
1535
1548
  if (target) {
1536
1549
  await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
@@ -1538,7 +1551,7 @@ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
1538
1551
  return { pinnedTag: target };
1539
1552
  }
1540
1553
  async function readPinnedPackVersion(projectRoot) {
1541
- const submoduleRoot = join11(projectRoot, TEAM_PACK_RELATIVE_PATH);
1554
+ const submoduleRoot = join12(projectRoot, TEAM_PACK_RELATIVE_PATH);
1542
1555
  const tag = await latestTag(submoduleRoot);
1543
1556
  if (tag) return tag;
1544
1557
  const sha = await currentCommitSha(submoduleRoot);
@@ -2225,11 +2238,11 @@ import { select as select7 } from "@inquirer/prompts";
2225
2238
  import { simpleGit as simpleGit3 } from "simple-git";
2226
2239
 
2227
2240
  // src/lib/check-folder-has-git.ts
2228
- import { existsSync as existsSync3, statSync } from "fs";
2229
- import { join as join12 } from "path";
2241
+ import { existsSync as existsSync4, statSync } from "fs";
2242
+ import { join as join13 } from "path";
2230
2243
  function checkFolderHasGit(folderPath) {
2231
- const gitPath = join12(folderPath, ".git");
2232
- if (!existsSync3(gitPath)) return false;
2244
+ const gitPath = join13(folderPath, ".git");
2245
+ if (!existsSync4(gitPath)) return false;
2233
2246
  const stat = statSync(gitPath);
2234
2247
  return stat.isDirectory() || stat.isFile();
2235
2248
  }
@@ -2259,8 +2272,8 @@ async function createInitialGitCommit(folderPath) {
2259
2272
  }
2260
2273
 
2261
2274
  // src/lib/detect-folder-tech-stack.ts
2262
- import { existsSync as existsSync4 } from "fs";
2263
- import { join as join13 } from "path";
2275
+ import { existsSync as existsSync5 } from "fs";
2276
+ import { join as join14 } from "path";
2264
2277
  var SIGNATURES = {
2265
2278
  node: ["package.json"],
2266
2279
  python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
@@ -2272,7 +2285,7 @@ var SIGNATURES = {
2272
2285
  function detectFolderTechStack(folderPath) {
2273
2286
  const matched = [];
2274
2287
  for (const [stack, files] of Object.entries(SIGNATURES)) {
2275
- if (files.some((f) => existsSync4(join13(folderPath, f)))) {
2288
+ if (files.some((f) => existsSync5(join14(folderPath, f)))) {
2276
2289
  matched.push(stack);
2277
2290
  }
2278
2291
  }
@@ -2280,26 +2293,26 @@ function detectFolderTechStack(folderPath) {
2280
2293
  }
2281
2294
 
2282
2295
  // src/lib/gitignore-template-loader.ts
2283
- import { readFileSync as readFileSync2 } from "fs";
2284
- import { dirname as dirname3, join as join14 } from "path";
2296
+ import { readFileSync as readFileSync3 } from "fs";
2297
+ import { dirname as dirname4, join as join15 } from "path";
2285
2298
  import { fileURLToPath as fileURLToPath2 } from "url";
2286
- var __dirname = dirname3(fileURLToPath2(import.meta.url));
2299
+ var __dirname = dirname4(fileURLToPath2(import.meta.url));
2287
2300
  var CANDIDATE_DIRS = [
2288
2301
  // Bundled production: dist/index.js → __dirname = .../dist/, sibling dist/templates
2289
- join14(__dirname, "templates", "gitignore"),
2302
+ join15(__dirname, "templates", "gitignore"),
2290
2303
  // Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
2291
- join14(__dirname, "..", "templates", "gitignore"),
2304
+ join15(__dirname, "..", "templates", "gitignore"),
2292
2305
  // Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
2293
- join14(__dirname, "..", "..", "src", "templates", "gitignore"),
2306
+ join15(__dirname, "..", "..", "src", "templates", "gitignore"),
2294
2307
  // npm-installed alt: __dirname = .../dist/ → package_root/src/templates
2295
- join14(__dirname, "..", "src", "templates", "gitignore")
2308
+ join15(__dirname, "..", "src", "templates", "gitignore")
2296
2309
  ];
2297
2310
  var AVATAR_MARKER_START = "# === avatar ===";
2298
2311
  var AVATAR_MARKER_END = "# === /avatar ===";
2299
2312
  function readTemplate(stack) {
2300
2313
  for (const dir of CANDIDATE_DIRS) {
2301
2314
  try {
2302
- return readFileSync2(join14(dir, `${stack}.txt`), "utf8");
2315
+ return readFileSync3(join15(dir, `${stack}.txt`), "utf8");
2303
2316
  } catch {
2304
2317
  }
2305
2318
  }
@@ -2313,15 +2326,15 @@ ${readTemplate(s).trim()}`);
2313
2326
  }
2314
2327
 
2315
2328
  // src/lib/write-or-merge-gitignore.ts
2316
- import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync } from "fs";
2317
- import { join as join15 } from "path";
2329
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync } from "fs";
2330
+ import { join as join16 } from "path";
2318
2331
  function writeOrMergeGitignore(folderPath, avatarBlock) {
2319
- const path = join15(folderPath, ".gitignore");
2320
- if (!existsSync5(path)) {
2332
+ const path = join16(folderPath, ".gitignore");
2333
+ if (!existsSync6(path)) {
2321
2334
  writeFileSync(path, avatarBlock, "utf8");
2322
2335
  return;
2323
2336
  }
2324
- const existing = readFileSync3(path, "utf8");
2337
+ const existing = readFileSync4(path, "utf8");
2325
2338
  const startIdx = existing.indexOf(AVATAR_MARKER_START);
2326
2339
  const endIdx = existing.indexOf(AVATAR_MARKER_END);
2327
2340
  if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
@@ -2493,7 +2506,7 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
2493
2506
 
2494
2507
  // src/commands/init-conflict-detection-helpers.ts
2495
2508
  import { readdir } from "fs/promises";
2496
- import { join as join16 } from "path";
2509
+ import { join as join17 } from "path";
2497
2510
  async function isEmptyOrMissing(path) {
2498
2511
  if (!await pathExists(path)) return true;
2499
2512
  try {
@@ -2506,7 +2519,7 @@ async function isEmptyOrMissing(path) {
2506
2519
  }
2507
2520
  async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
2508
2521
  for (let i = 2; i < maxAttempts; i++) {
2509
- const candidate = join16(parent, `${desiredName}-${i}`);
2522
+ const candidate = join17(parent, `${desiredName}-${i}`);
2510
2523
  if (await isEmptyOrMissing(candidate)) return candidate;
2511
2524
  }
2512
2525
  return null;
@@ -2898,7 +2911,7 @@ async function runInitFromScratch(opts, ownerEmail) {
2898
2911
  const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
2899
2912
  const workspaceParent = resolve(opts.workspaceParent ?? ".");
2900
2913
  const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
2901
- const srcPath = join17(workspacePath, "src");
2914
+ const srcPath = join18(workspacePath, "src");
2902
2915
  await ensureDir(workspacePath);
2903
2916
  await ensureDir(srcPath);
2904
2917
  await safeBootstrapGitInFolder(srcPath, { autoYes: true });
@@ -3034,10 +3047,10 @@ async function finalizeWorkspaceScaffold(args) {
3034
3047
  await writeRootClaudeMd(args.workspacePath, vars);
3035
3048
  await writeProjectSettings(args.workspacePath, vars);
3036
3049
  await appendGitignoreEntries(args.workspacePath);
3037
- await ensureDir(join17(args.workspacePath, "notes"));
3038
- await ensureDir(join17(args.workspacePath, "scripts"));
3039
- await installGitHook(join17(args.workspacePath, ".git"), "post-merge");
3040
- await installGitHook(join17(args.workspacePath, ".git", "modules", "src"), "pre-push");
3050
+ await ensureDir(join18(args.workspacePath, "notes"));
3051
+ await ensureDir(join18(args.workspacePath, "scripts"));
3052
+ await installGitHook(join18(args.workspacePath, ".git"), "post-merge");
3053
+ await installGitHook(join18(args.workspacePath, ".git", "modules", "src"), "pre-push");
3041
3054
  log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
3042
3055
  await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
3043
3056
  await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
@@ -3137,7 +3150,7 @@ async function maybeCreateWorkspaceRemote(args) {
3137
3150
  }
3138
3151
  }
3139
3152
  async function resolveWorkspacePath(parent, desiredName, force) {
3140
- const desired = join17(parent, desiredName);
3153
+ const desired = join18(parent, desiredName);
3141
3154
  if (await isEmptyOrMissing(desired)) return desired;
3142
3155
  log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
3143
3156
  while (true) {
@@ -3168,7 +3181,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
3168
3181
  message: "T\xEAn workspace m\u1EDBi:",
3169
3182
  validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
3170
3183
  });
3171
- const newPath = join17(parent, newName.trim());
3184
+ const newPath = join18(parent, newName.trim());
3172
3185
  if (await isEmptyOrMissing(newPath)) return newPath;
3173
3186
  log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
3174
3187
  }
@@ -3262,15 +3275,15 @@ function registerSecretsCommand(program2) {
3262
3275
 
3263
3276
  // src/commands/status.ts
3264
3277
  import { promises as fs8 } from "fs";
3265
- import { join as join19 } from "path";
3278
+ import { join as join20 } from "path";
3266
3279
  import boxen5 from "boxen";
3267
3280
 
3268
3281
  // src/lib/pack-backup-manager.ts
3269
3282
  import { promises as fs7 } from "fs";
3270
- import { join as join18 } from "path";
3283
+ import { join as join19 } from "path";
3271
3284
  var BACKUP_DIR_NAME = "_backup";
3272
3285
  async function listBackups(projectRoot) {
3273
- const dir = join18(projectRoot, ".claude", BACKUP_DIR_NAME);
3286
+ const dir = join19(projectRoot, ".claude", BACKUP_DIR_NAME);
3274
3287
  if (!await pathExists(dir)) return [];
3275
3288
  const entries = await fs7.readdir(dir, { withFileTypes: true });
3276
3289
  return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
@@ -3296,7 +3309,7 @@ function registerStatusCommand(program2) {
3296
3309
  }
3297
3310
  async function gatherStatus(cwd) {
3298
3311
  const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
3299
- const claudeRoot = join19(cwd, ".claude");
3312
+ const claudeRoot = join20(cwd, ".claude");
3300
3313
  const hasAvatar = await pathExists(claudeRoot);
3301
3314
  if (!hasAvatar) {
3302
3315
  return {
@@ -3309,8 +3322,8 @@ async function gatherStatus(cwd) {
3309
3322
  hasAvatar: false
3310
3323
  };
3311
3324
  }
3312
- const packVersion = await isGitRepo(join19(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
3313
- const pendingDir = join19(claudeRoot, "_pending");
3325
+ const packVersion = await isGitRepo(join20(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
3326
+ const pendingDir = join20(claudeRoot, "_pending");
3314
3327
  const pendingCount = await pathExists(pendingDir) ? (await fs8.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
3315
3328
  const backupCount = (await listBackups(cwd)).length;
3316
3329
  const techStackSummary = await readTechStackFirstLine(claudeRoot);
@@ -3325,7 +3338,7 @@ async function gatherStatus(cwd) {
3325
3338
  };
3326
3339
  }
3327
3340
  async function readTechStackFirstLine(claudeRoot) {
3328
- const techStackPath = join19(claudeRoot, "project", "tech-stack.md");
3341
+ const techStackPath = join20(claudeRoot, "project", "tech-stack.md");
3329
3342
  if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
3330
3343
  const content = await readText(techStackPath);
3331
3344
  const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
@@ -3366,27 +3379,27 @@ import boxen6 from "boxen";
3366
3379
  // src/lib/create-uninstall-backup-snapshot.ts
3367
3380
  import { cp, mkdir, writeFile } from "fs/promises";
3368
3381
  import { homedir as homedir3 } from "os";
3369
- import { basename as basename2, join as join20 } from "path";
3370
- var UNINSTALL_BACKUPS_DIR = join20(homedir3(), ".avatar", "uninstall-backups");
3382
+ import { basename as basename2, join as join21 } from "path";
3383
+ var UNINSTALL_BACKUPS_DIR = join21(homedir3(), ".avatar", "uninstall-backups");
3371
3384
  async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersion) {
3372
3385
  const projectName = basename2(projectRoot);
3373
3386
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3374
- const backupDir = join20(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
3387
+ const backupDir = join21(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
3375
3388
  await mkdir(backupDir, { recursive: true, mode: 448 });
3376
3389
  if (artifacts.claudeDir) {
3377
- await cp(artifacts.claudeDir, join20(backupDir, ".claude"), { recursive: true });
3390
+ await cp(artifacts.claudeDir, join21(backupDir, ".claude"), { recursive: true });
3378
3391
  }
3379
3392
  if (artifacts.claudeMd) {
3380
- await cp(artifacts.claudeMd, join20(backupDir, "CLAUDE.md"));
3393
+ await cp(artifacts.claudeMd, join21(backupDir, "CLAUDE.md"));
3381
3394
  }
3382
3395
  if (artifacts.postMergeHook || artifacts.prePushHook) {
3383
- const hooksBackupDir = join20(backupDir, "hooks");
3396
+ const hooksBackupDir = join21(backupDir, "hooks");
3384
3397
  await mkdir(hooksBackupDir, { recursive: true });
3385
3398
  if (artifacts.postMergeHook) {
3386
- await cp(artifacts.postMergeHook, join20(hooksBackupDir, "post-merge"));
3399
+ await cp(artifacts.postMergeHook, join21(hooksBackupDir, "post-merge"));
3387
3400
  }
3388
3401
  if (artifacts.prePushHook) {
3389
- await cp(artifacts.prePushHook, join20(hooksBackupDir, "pre-push"));
3402
+ await cp(artifacts.prePushHook, join21(hooksBackupDir, "pre-push"));
3390
3403
  }
3391
3404
  }
3392
3405
  const manifest = {
@@ -3401,27 +3414,27 @@ async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersi
3401
3414
  prePushHook: !!artifacts.prePushHook
3402
3415
  }
3403
3416
  };
3404
- await writeFile(join20(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
3417
+ await writeFile(join21(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
3405
3418
  return backupDir;
3406
3419
  }
3407
3420
 
3408
3421
  // src/lib/detect-avatar-project-artifacts.ts
3409
- import { existsSync as existsSync6 } from "fs";
3410
- import { join as join21 } from "path";
3422
+ import { existsSync as existsSync7 } from "fs";
3423
+ import { join as join22 } from "path";
3411
3424
  function existsOrNull(path) {
3412
- return existsSync6(path) ? path : null;
3425
+ return existsSync7(path) ? path : null;
3413
3426
  }
3414
3427
  function detectAvatarProjectArtifacts(projectRoot) {
3415
- const claudeDir = existsOrNull(join21(projectRoot, ".claude"));
3416
- const claudeMd = existsOrNull(join21(projectRoot, "CLAUDE.md"));
3417
- const postMergeHook = existsOrNull(join21(projectRoot, ".git", "hooks", "post-merge"));
3428
+ const claudeDir = existsOrNull(join22(projectRoot, ".claude"));
3429
+ const claudeMd = existsOrNull(join22(projectRoot, "CLAUDE.md"));
3430
+ const postMergeHook = existsOrNull(join22(projectRoot, ".git", "hooks", "post-merge"));
3418
3431
  const prePushHook = existsOrNull(
3419
- join21(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
3432
+ join22(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
3420
3433
  );
3421
- const gitignorePath = existsOrNull(join21(projectRoot, ".gitignore"));
3422
- const gitmodulesPath = existsOrNull(join21(projectRoot, ".gitmodules"));
3423
- const notesDir = existsOrNull(join21(projectRoot, "notes"));
3424
- const scriptsDir = existsOrNull(join21(projectRoot, "scripts"));
3434
+ const gitignorePath = existsOrNull(join22(projectRoot, ".gitignore"));
3435
+ const gitmodulesPath = existsOrNull(join22(projectRoot, ".gitmodules"));
3436
+ const notesDir = existsOrNull(join22(projectRoot, "notes"));
3437
+ const scriptsDir = existsOrNull(join22(projectRoot, "scripts"));
3425
3438
  const hasAnyArtifact = !!(claudeDir || claudeMd || postMergeHook || prePushHook);
3426
3439
  return {
3427
3440
  hasAnyArtifact,
@@ -3442,11 +3455,11 @@ async function executeUninstallDeletion(artifacts, flags) {
3442
3455
  if (artifacts.claudeDir) {
3443
3456
  if (flags.keepSubmodule) {
3444
3457
  const { readdir: readdir2 } = await import("fs/promises");
3445
- const { join: join22 } = await import("path");
3458
+ const { join: join23 } = await import("path");
3446
3459
  const entries = await readdir2(artifacts.claudeDir);
3447
3460
  for (const entry of entries) {
3448
3461
  if (entry === "pack") continue;
3449
- await rm(join22(artifacts.claudeDir, entry), { recursive: true, force: true });
3462
+ await rm(join23(artifacts.claudeDir, entry), { recursive: true, force: true });
3450
3463
  }
3451
3464
  } else {
3452
3465
  await rm(artifacts.claudeDir, { recursive: true, force: true });
@@ -3515,7 +3528,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
3515
3528
  }
3516
3529
 
3517
3530
  // src/commands/uninstall.ts
3518
- var CLI_VERSION = "1.3.0";
3531
+ var CLI_VERSION = "1.3.2";
3519
3532
  function registerUninstallCommand(program2) {
3520
3533
  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) => {
3521
3534
  try {
@@ -3597,7 +3610,7 @@ function printUninstallSuccessBox(backupPath) {
3597
3610
  }
3598
3611
 
3599
3612
  // src/index.ts
3600
- var CLI_VERSION2 = "1.3.0";
3613
+ var CLI_VERSION2 = "1.3.2";
3601
3614
  var program = new Command();
3602
3615
  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(
3603
3616
  "beforeAll",