@nalvietnam/avatar-cli 1.6.0 → 1.6.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
@@ -1852,17 +1852,41 @@ import { join as join14 } from "path";
1852
1852
  import { confirm as confirm2 } from "@inquirer/prompts";
1853
1853
  var WIKI_TIMEOUT_MS = 15 * 60 * 1e3;
1854
1854
  var DEFAULT_LLMLITE_MODEL = "nal-claude";
1855
+ var DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-5";
1856
+ function normalizeAnthropicBaseUrl(rawBaseUrl) {
1857
+ const cleaned = rawBaseUrl.replace(/\/+$/, "");
1858
+ if (cleaned.endsWith("/v1")) return `${cleaned}/`;
1859
+ if (cleaned.endsWith("/v1/")) return cleaned;
1860
+ return `${cleaned}/v1/`;
1861
+ }
1855
1862
  async function readSettingsForWikiCredentials(workspacePath) {
1856
1863
  const settingsPath = join14(workspacePath, ".claude", "settings.json");
1857
1864
  if (!await pathExists(settingsPath)) return null;
1858
1865
  try {
1859
1866
  const settings = await readJson(settingsPath);
1860
1867
  const env = settings.env || {};
1861
- const apiKey = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : null;
1862
1868
  const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : null;
1863
- if (!apiKey || !baseUrl) return null;
1864
- const model = typeof env.ANTHROPIC_MODEL === "string" && env.ANTHROPIC_MODEL.length > 0 ? env.ANTHROPIC_MODEL : DEFAULT_LLMLITE_MODEL;
1865
- return { apiKey, baseUrl, model };
1869
+ if (!baseUrl) return null;
1870
+ const userModel = typeof env.ANTHROPIC_MODEL === "string" ? env.ANTHROPIC_MODEL : "";
1871
+ const anthropicKey = typeof env.ANTHROPIC_API_KEY === "string" ? env.ANTHROPIC_API_KEY : null;
1872
+ if (anthropicKey) {
1873
+ return {
1874
+ provider: "anthropic",
1875
+ apiKey: anthropicKey,
1876
+ baseUrl: normalizeAnthropicBaseUrl(baseUrl),
1877
+ model: userModel.length > 0 ? userModel : DEFAULT_ANTHROPIC_MODEL
1878
+ };
1879
+ }
1880
+ const llmliteToken = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : null;
1881
+ if (llmliteToken) {
1882
+ return {
1883
+ provider: "llmlite",
1884
+ apiKey: llmliteToken,
1885
+ baseUrl,
1886
+ model: userModel.length > 0 ? userModel : DEFAULT_LLMLITE_MODEL
1887
+ };
1888
+ }
1889
+ return null;
1866
1890
  } catch {
1867
1891
  return null;
1868
1892
  }
@@ -1880,8 +1904,10 @@ function tailLines2(text, n) {
1880
1904
  async function runGitnexusWikiConditional(workspacePath) {
1881
1905
  const creds = await readSettingsForWikiCredentials(workspacePath);
1882
1906
  if (!creds) {
1883
- log.warn("Subscription mode (ho\u1EB7c settings.json kh\xF4ng c\xF3 LLMLite key) \u2192 skip wiki gen.");
1884
- log.dim("\u0110\u1EC3 gen wiki sau, ch\u1EA1y manual: gitnexus wiki . --api-key <openai-key>");
1907
+ log.warn("Subscription mode (OAuth, kh\xF4ng c\xF3 API key trong settings.json) \u2192 skip wiki gen.");
1908
+ log.dim(
1909
+ "\u0110\u1EC3 gen wiki sau, ch\u1EA1y manual:\n gitnexus wiki . --api-key <key> --base-url <url> --model <model>"
1910
+ );
1885
1911
  return { ran: false, skipped: true, reason: "subscription-mode" };
1886
1912
  }
1887
1913
  const proceed = await confirmWikiGeneration(creds.baseUrl, creds.model);
@@ -1891,7 +1917,9 @@ async function runGitnexusWikiConditional(workspacePath) {
1891
1917
  );
1892
1918
  return { ran: false, skipped: true, reason: "user-declined" };
1893
1919
  }
1894
- const sp = spinnerWithElapsed(`Generating wiki via ${creds.baseUrl} model=${creds.model}`);
1920
+ const sp = spinnerWithElapsed(
1921
+ `Generating wiki via ${creds.baseUrl} (${creds.provider}) model=${creds.model}`
1922
+ );
1895
1923
  const result = spawnSync10(
1896
1924
  "gitnexus",
1897
1925
  ["wiki", ".", "--api-key", creds.apiKey, "--base-url", creds.baseUrl, "--model", creds.model],
@@ -2169,7 +2197,7 @@ function registerGitnexusCommand(program2) {
2169
2197
  }
2170
2198
 
2171
2199
  // src/commands/init.ts
2172
- import { basename, join as join22, relative as relative2, resolve } from "path";
2200
+ import { basename, join as join24, relative as relative3, resolve } from "path";
2173
2201
  import { confirm as confirm5, input as input5, select as select9 } from "@inquirer/prompts";
2174
2202
  import boxen5 from "boxen";
2175
2203
 
@@ -3032,6 +3060,131 @@ function linkExistingRemoteToWorkspace(args) {
3032
3060
  return { sshUrl, httpsUrl };
3033
3061
  }
3034
3062
 
3063
+ // src/lib/merge-pack-settings-into-project-settings.ts
3064
+ import { promises as fs9 } from "fs";
3065
+ import { join as join17 } from "path";
3066
+ function backupFilename(originalPath) {
3067
+ const d = /* @__PURE__ */ new Date();
3068
+ const stamp = `${d.getFullYear().toString().slice(-2) + String(d.getMonth() + 1).padStart(2, "0") + String(d.getDate()).padStart(2, "0")}-${String(d.getHours()).padStart(2, "0")}${String(d.getMinutes()).padStart(2, "0")}`;
3069
+ return `${originalPath}.backup-${stamp}`;
3070
+ }
3071
+ function unionDedupe(a, b) {
3072
+ const seen = /* @__PURE__ */ new Set();
3073
+ const out = [];
3074
+ for (const item of [...a, ...b]) {
3075
+ const key = typeof item === "string" ? item : JSON.stringify(item);
3076
+ if (!seen.has(key)) {
3077
+ seen.add(key);
3078
+ out.push(item);
3079
+ }
3080
+ }
3081
+ return out;
3082
+ }
3083
+ function mergeHooksPerEvent(packHooks, userHooks) {
3084
+ const touched = [];
3085
+ const merged = { ...userHooks };
3086
+ for (const [event, packEntries] of Object.entries(packHooks)) {
3087
+ const userEntries = userHooks[event] || [];
3088
+ const union = unionDedupe(userEntries, packEntries);
3089
+ if (union.length !== userEntries.length) {
3090
+ touched.push(event);
3091
+ }
3092
+ merged[event] = union;
3093
+ }
3094
+ return { merged, touchedEvents: touched };
3095
+ }
3096
+ async function mergePackSettingsIntoProjectSettings(workspacePath) {
3097
+ const packTemplatePath = join17(workspacePath, ".claude", "pack", "templates", "settings.json.tpl");
3098
+ const projectSettingsPath = join17(workspacePath, ".claude", "settings.json");
3099
+ if (!await pathExists(packTemplatePath)) {
3100
+ return { action: "no-pack-template", changes: [] };
3101
+ }
3102
+ let packTemplate;
3103
+ try {
3104
+ const raw = await readText(packTemplatePath);
3105
+ packTemplate = JSON.parse(raw);
3106
+ } catch (err) {
3107
+ throw new Error(
3108
+ `Pack settings template kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: ${err.message}. Path: ${packTemplatePath}`
3109
+ );
3110
+ }
3111
+ let userSettings = {};
3112
+ let projectHasSettings = false;
3113
+ if (await pathExists(projectSettingsPath)) {
3114
+ projectHasSettings = true;
3115
+ try {
3116
+ userSettings = await readJson(projectSettingsPath);
3117
+ } catch (err) {
3118
+ throw new Error(
3119
+ `Project settings.json kh\xF4ng parse \u0111\u01B0\u1EE3c: ${err.message}. Manual fix tr\u01B0\u1EDBc khi sync.`
3120
+ );
3121
+ }
3122
+ }
3123
+ const changes = [];
3124
+ const merged = { ...userSettings };
3125
+ if (packTemplate.statusLine && !userSettings.statusLine) {
3126
+ merged.statusLine = packTemplate.statusLine;
3127
+ changes.push("statusLine added");
3128
+ }
3129
+ if (typeof packTemplate.includeCoAuthoredBy === "boolean" && typeof userSettings.includeCoAuthoredBy !== "boolean") {
3130
+ merged.includeCoAuthoredBy = packTemplate.includeCoAuthoredBy;
3131
+ changes.push("includeCoAuthoredBy added");
3132
+ }
3133
+ if (packTemplate.model && !userSettings.model) {
3134
+ merged.model = packTemplate.model;
3135
+ changes.push("model added");
3136
+ }
3137
+ if (packTemplate.env) {
3138
+ const mergedEnv = { ...userSettings.env || {} };
3139
+ let envChanged = false;
3140
+ for (const [k, v] of Object.entries(packTemplate.env)) {
3141
+ if (!(k in mergedEnv)) {
3142
+ mergedEnv[k] = v;
3143
+ envChanged = true;
3144
+ }
3145
+ }
3146
+ if (envChanged) {
3147
+ merged.env = mergedEnv;
3148
+ changes.push("env vars added from pack");
3149
+ }
3150
+ }
3151
+ if (packTemplate.permissions) {
3152
+ const userAllow = userSettings.permissions?.allow || [];
3153
+ const userDeny = userSettings.permissions?.deny || [];
3154
+ const packAllow = packTemplate.permissions.allow || [];
3155
+ const packDeny = packTemplate.permissions.deny || [];
3156
+ const mergedAllow = unionDedupe(userAllow, packAllow);
3157
+ const mergedDeny = unionDedupe(userDeny, packDeny);
3158
+ if (mergedAllow.length !== userAllow.length || mergedDeny.length !== userDeny.length) {
3159
+ merged.permissions = { allow: mergedAllow, deny: mergedDeny };
3160
+ changes.push(
3161
+ `permissions union (+${mergedAllow.length - userAllow.length} allow, +${mergedDeny.length - userDeny.length} deny)`
3162
+ );
3163
+ }
3164
+ }
3165
+ if (packTemplate.hooks) {
3166
+ const userHooks = userSettings.hooks || {};
3167
+ const { merged: mergedHooks, touchedEvents } = mergeHooksPerEvent(
3168
+ packTemplate.hooks,
3169
+ userHooks
3170
+ );
3171
+ if (touchedEvents.length > 0) {
3172
+ merged.hooks = mergedHooks;
3173
+ changes.push(`hooks added for events: ${touchedEvents.join(", ")}`);
3174
+ }
3175
+ }
3176
+ if (changes.length === 0) {
3177
+ return { action: "no-change", changes: [] };
3178
+ }
3179
+ let backupPath;
3180
+ if (projectHasSettings) {
3181
+ backupPath = backupFilename(projectSettingsPath);
3182
+ await fs9.copyFile(projectSettingsPath, backupPath);
3183
+ }
3184
+ await writeJsonAtomic(projectSettingsPath, merged);
3185
+ return { action: "merged", backupPath, changes };
3186
+ }
3187
+
3035
3188
  // src/lib/safe-bootstrap-for-dirty-folder.ts
3036
3189
  import { readdirSync } from "fs";
3037
3190
  import { select as select8 } from "@inquirer/prompts";
@@ -3039,9 +3192,9 @@ import { simpleGit as simpleGit3 } from "simple-git";
3039
3192
 
3040
3193
  // src/lib/check-folder-has-git.ts
3041
3194
  import { existsSync as existsSync6, statSync } from "fs";
3042
- import { join as join17 } from "path";
3195
+ import { join as join18 } from "path";
3043
3196
  function checkFolderHasGit(folderPath) {
3044
- const gitPath = join17(folderPath, ".git");
3197
+ const gitPath = join18(folderPath, ".git");
3045
3198
  if (!existsSync6(gitPath)) return false;
3046
3199
  const stat = statSync(gitPath);
3047
3200
  return stat.isDirectory() || stat.isFile();
@@ -3073,7 +3226,7 @@ async function createInitialGitCommit(folderPath) {
3073
3226
 
3074
3227
  // src/lib/detect-folder-tech-stack.ts
3075
3228
  import { existsSync as existsSync7 } from "fs";
3076
- import { join as join18 } from "path";
3229
+ import { join as join19 } from "path";
3077
3230
  var SIGNATURES = {
3078
3231
  node: ["package.json"],
3079
3232
  python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
@@ -3085,7 +3238,7 @@ var SIGNATURES = {
3085
3238
  function detectFolderTechStack(folderPath) {
3086
3239
  const matched = [];
3087
3240
  for (const [stack, files] of Object.entries(SIGNATURES)) {
3088
- if (files.some((f) => existsSync7(join18(folderPath, f)))) {
3241
+ if (files.some((f) => existsSync7(join19(folderPath, f)))) {
3089
3242
  matched.push(stack);
3090
3243
  }
3091
3244
  }
@@ -3094,25 +3247,25 @@ function detectFolderTechStack(folderPath) {
3094
3247
 
3095
3248
  // src/lib/gitignore-template-loader.ts
3096
3249
  import { readFileSync as readFileSync3 } from "fs";
3097
- import { dirname as dirname4, join as join19 } from "path";
3250
+ import { dirname as dirname4, join as join20 } from "path";
3098
3251
  import { fileURLToPath as fileURLToPath2 } from "url";
3099
3252
  var __dirname = dirname4(fileURLToPath2(import.meta.url));
3100
3253
  var CANDIDATE_DIRS = [
3101
3254
  // Bundled production: dist/index.js → __dirname = .../dist/, sibling dist/templates
3102
- join19(__dirname, "templates", "gitignore"),
3255
+ join20(__dirname, "templates", "gitignore"),
3103
3256
  // Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
3104
- join19(__dirname, "..", "templates", "gitignore"),
3257
+ join20(__dirname, "..", "templates", "gitignore"),
3105
3258
  // Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
3106
- join19(__dirname, "..", "..", "src", "templates", "gitignore"),
3259
+ join20(__dirname, "..", "..", "src", "templates", "gitignore"),
3107
3260
  // npm-installed alt: __dirname = .../dist/ → package_root/src/templates
3108
- join19(__dirname, "..", "src", "templates", "gitignore")
3261
+ join20(__dirname, "..", "src", "templates", "gitignore")
3109
3262
  ];
3110
3263
  var AVATAR_MARKER_START = "# === avatar ===";
3111
3264
  var AVATAR_MARKER_END = "# === /avatar ===";
3112
3265
  function readTemplate(stack) {
3113
3266
  for (const dir of CANDIDATE_DIRS) {
3114
3267
  try {
3115
- return readFileSync3(join19(dir, `${stack}.txt`), "utf8");
3268
+ return readFileSync3(join20(dir, `${stack}.txt`), "utf8");
3116
3269
  } catch {
3117
3270
  }
3118
3271
  }
@@ -3127,9 +3280,9 @@ ${readTemplate(s).trim()}`);
3127
3280
 
3128
3281
  // src/lib/write-or-merge-gitignore.ts
3129
3282
  import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
3130
- import { join as join20 } from "path";
3283
+ import { join as join21 } from "path";
3131
3284
  function writeOrMergeGitignore(folderPath, avatarBlock) {
3132
- const path = join20(folderPath, ".gitignore");
3285
+ const path = join21(folderPath, ".gitignore");
3133
3286
  if (!existsSync8(path)) {
3134
3287
  writeFileSync(path, avatarBlock, "utf8");
3135
3288
  return;
@@ -3304,9 +3457,75 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
3304
3457
  await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
3305
3458
  }
3306
3459
 
3460
+ // src/lib/symlink-farm-for-team-pack-mount-dirs.ts
3461
+ import { promises as fs11 } from "fs";
3462
+ import { dirname as dirname5, join as join22, relative as relative2 } from "path";
3463
+
3464
+ // src/lib/backup-existing-dir-before-symlink-override.ts
3465
+ import { promises as fs10 } from "fs";
3466
+ function timestamp() {
3467
+ const d = /* @__PURE__ */ new Date();
3468
+ const pad = (n) => n.toString().padStart(2, "0");
3469
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
3470
+ }
3471
+ async function backupDirBeforeReplace(targetPath) {
3472
+ const backupPath = `${targetPath}.backup-${timestamp()}`;
3473
+ await fs10.rename(targetPath, backupPath);
3474
+ return backupPath;
3475
+ }
3476
+
3477
+ // src/lib/symlink-farm-for-team-pack-mount-dirs.ts
3478
+ var TEAM_PACK_MOUNT_DIRS = [
3479
+ "skills",
3480
+ "agents",
3481
+ "commands",
3482
+ "hooks",
3483
+ "workflows",
3484
+ "scripts",
3485
+ "knowledge"
3486
+ ];
3487
+ async function isSymbolicLink(path) {
3488
+ try {
3489
+ const st = await fs11.lstat(path);
3490
+ return st.isSymbolicLink();
3491
+ } catch {
3492
+ return false;
3493
+ }
3494
+ }
3495
+ async function syncMountedDir(source, dest, force) {
3496
+ const dir = relative2(dirname5(dest), dest) || dest;
3497
+ if (!await pathExists(source)) {
3498
+ return { dir, action: "source-missing" };
3499
+ }
3500
+ if (await pathExists(dest)) {
3501
+ if (await isSymbolicLink(dest)) {
3502
+ await fs11.unlink(dest);
3503
+ } else if (force) {
3504
+ const backupPath = await backupDirBeforeReplace(dest);
3505
+ const relativeSource2 = relative2(dirname5(dest), source);
3506
+ await fs11.symlink(relativeSource2, dest);
3507
+ return { dir, action: "backed-up-and-linked", backupPath };
3508
+ } else {
3509
+ return { dir, action: "skipped-conflict" };
3510
+ }
3511
+ }
3512
+ const relativeSource = relative2(dirname5(dest), source);
3513
+ await fs11.symlink(relativeSource, dest);
3514
+ return { dir, action: "created" };
3515
+ }
3516
+ async function syncAllMountDirs(packDir, claudeDir, force) {
3517
+ const results = [];
3518
+ for (const dir of TEAM_PACK_MOUNT_DIRS) {
3519
+ const source = join22(packDir, dir);
3520
+ const dest = join22(claudeDir, dir);
3521
+ results.push(await syncMountedDir(source, dest, force));
3522
+ }
3523
+ return results;
3524
+ }
3525
+
3307
3526
  // src/commands/init-conflict-detection-helpers.ts
3308
3527
  import { readdir } from "fs/promises";
3309
- import { join as join21 } from "path";
3528
+ import { join as join23 } from "path";
3310
3529
  async function isEmptyOrMissing(path) {
3311
3530
  if (!await pathExists(path)) return true;
3312
3531
  try {
@@ -3319,7 +3538,7 @@ async function isEmptyOrMissing(path) {
3319
3538
  }
3320
3539
  async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
3321
3540
  for (let i = 2; i < maxAttempts; i++) {
3322
- const candidate = join21(parent, `${desiredName}-${i}`);
3541
+ const candidate = join23(parent, `${desiredName}-${i}`);
3323
3542
  if (await isEmptyOrMissing(candidate)) return candidate;
3324
3543
  }
3325
3544
  return null;
@@ -3749,7 +3968,7 @@ async function runInitFromScratch(opts, ownerEmail) {
3749
3968
  const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
3750
3969
  const workspaceParent = resolve(opts.workspaceParent ?? ".");
3751
3970
  const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
3752
- const srcPath = join22(workspacePath, "src");
3971
+ const srcPath = join24(workspacePath, "src");
3753
3972
  await ensureDir(workspacePath);
3754
3973
  await ensureDir(srcPath);
3755
3974
  await safeBootstrapGitInFolder(srcPath, { autoYes: true });
@@ -3887,11 +4106,12 @@ async function finalizeWorkspaceScaffold(args) {
3887
4106
  await writeRootClaudeMd(args.workspacePath, vars);
3888
4107
  await writeProjectSettings(args.workspacePath, vars);
3889
4108
  await appendGitignoreEntries(args.workspacePath);
3890
- await ensureDir(join22(args.workspacePath, "notes"));
3891
- await ensureDir(join22(args.workspacePath, "scripts"));
3892
- await installGitHook(join22(args.workspacePath, ".git"), "post-merge");
3893
- await installGitHook(join22(args.workspacePath, ".git", "modules", "src"), "pre-push");
4109
+ await ensureDir(join24(args.workspacePath, "notes"));
4110
+ await ensureDir(join24(args.workspacePath, "scripts"));
4111
+ await installGitHook(join24(args.workspacePath, ".git"), "post-merge");
4112
+ await installGitHook(join24(args.workspacePath, ".git", "modules", "src"), "pre-push");
3894
4113
  log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
4114
+ await autoSyncPackOnInit(args.workspacePath);
3895
4115
  await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
3896
4116
  await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
3897
4117
  await maybeCreateWorkspaceRemote(args);
@@ -3926,6 +4146,39 @@ async function finalizeWorkspaceScaffold(args) {
3926
4146
  }
3927
4147
  printInitSuccessBox(args.workspacePath, args.flow, aiResult, gitnexusResult);
3928
4148
  }
4149
+ async function autoSyncPackOnInit(workspacePath) {
4150
+ const packDir = join24(workspacePath, TEAM_PACK_RELATIVE_PATH);
4151
+ if (!await pathExists(packDir)) {
4152
+ log.dim("Pack submodule kh\xF4ng t\u1ED3n t\u1EA1i (skip auto-sync). C\xF3 th\u1EC3 ch\u1EA1y `avatar sync` sau.");
4153
+ return;
4154
+ }
4155
+ const claudeDir = join24(workspacePath, ".claude");
4156
+ log.info("Auto-sync pack content v\xE0o .claude/ (symlinks + settings merge)...");
4157
+ try {
4158
+ const results = await syncAllMountDirs(packDir, claudeDir, false);
4159
+ const created = results.filter((r) => r.action === "created" || r.action === "updated").length;
4160
+ const missing = results.filter((r) => r.action === "source-missing").length;
4161
+ log.success(
4162
+ ` \u2713 Symlinks: ${created} created${missing > 0 ? `, ${missing} source-missing (pack thi\u1EBFu dir)` : ""}`
4163
+ );
4164
+ const mergeResult = await mergePackSettingsIntoProjectSettings(workspacePath);
4165
+ switch (mergeResult.action) {
4166
+ case "merged":
4167
+ log.success(` \u2713 settings.json merged (${mergeResult.changes.join("; ")})`);
4168
+ break;
4169
+ case "no-change":
4170
+ log.dim(" - settings.json \u0111\xE3 sync, kh\xF4ng c\u1EA7n thay \u0111\u1ED5i.");
4171
+ break;
4172
+ case "no-pack-template":
4173
+ log.dim(" - Pack kh\xF4ng c\xF3 templates/settings.json.tpl, skip merge.");
4174
+ break;
4175
+ }
4176
+ } catch (err) {
4177
+ log.warn(
4178
+ `Auto-sync pack fail: ${err instanceof Error ? err.message : err}. Ch\u1EA1y \`avatar sync\` th\u1EE7 c\xF4ng \u0111\u1EC3 retry.`
4179
+ );
4180
+ }
4181
+ }
3929
4182
  async function maybeCreateWorkspaceRemote(args) {
3930
4183
  if (args.skipCommit) {
3931
4184
  log.dim("Skip workspace remote (ch\u01B0a commit). Setup sau qua: gh repo create ...");
@@ -4013,7 +4266,7 @@ async function maybeCreateWorkspaceRemote(args) {
4013
4266
  }
4014
4267
  }
4015
4268
  async function resolveWorkspacePath(parent, desiredName, force) {
4016
- const desired = join22(parent, desiredName);
4269
+ const desired = join24(parent, desiredName);
4017
4270
  if (await isEmptyOrMissing(desired)) return desired;
4018
4271
  log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
4019
4272
  while (true) {
@@ -4044,7 +4297,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
4044
4297
  message: "T\xEAn workspace m\u1EDBi:",
4045
4298
  validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
4046
4299
  });
4047
- const newPath = join22(parent, newName.trim());
4300
+ const newPath = join24(parent, newName.trim());
4048
4301
  if (await isEmptyOrMissing(newPath)) return newPath;
4049
4302
  log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
4050
4303
  }
@@ -4087,7 +4340,7 @@ function formatGitnexusStatusLine(result) {
4087
4340
  }
4088
4341
  function printInitSuccessBox(rootPath, flow, aiResult = null, gitnexusResult = null) {
4089
4342
  const lines = [
4090
- `${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative2(process.cwd(), rootPath) || rootPath}`,
4343
+ `${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative3(process.cwd(), rootPath) || rootPath}`,
4091
4344
  ` ${chalk.dim(`(flow: ${flow})`)}`,
4092
4345
  formatAiStatusLine(aiResult),
4093
4346
  formatGitnexusStatusLine(gitnexusResult),
@@ -4150,18 +4403,18 @@ function registerSecretsCommand(program2) {
4150
4403
  }
4151
4404
 
4152
4405
  // src/commands/status.ts
4153
- import { promises as fs10 } from "fs";
4154
- import { join as join24 } from "path";
4406
+ import { promises as fs13 } from "fs";
4407
+ import { join as join26 } from "path";
4155
4408
  import boxen6 from "boxen";
4156
4409
 
4157
4410
  // src/lib/pack-backup-manager.ts
4158
- import { promises as fs9 } from "fs";
4159
- import { join as join23 } from "path";
4411
+ import { promises as fs12 } from "fs";
4412
+ import { join as join25 } from "path";
4160
4413
  var BACKUP_DIR_NAME = "_backup";
4161
4414
  async function listBackups(projectRoot) {
4162
- const dir = join23(projectRoot, ".claude", BACKUP_DIR_NAME);
4415
+ const dir = join25(projectRoot, ".claude", BACKUP_DIR_NAME);
4163
4416
  if (!await pathExists(dir)) return [];
4164
- const entries = await fs9.readdir(dir, { withFileTypes: true });
4417
+ const entries = await fs12.readdir(dir, { withFileTypes: true });
4165
4418
  return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
4166
4419
  }
4167
4420
 
@@ -4185,7 +4438,7 @@ function registerStatusCommand(program2) {
4185
4438
  }
4186
4439
  async function gatherStatus(cwd) {
4187
4440
  const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
4188
- const claudeRoot = join24(cwd, ".claude");
4441
+ const claudeRoot = join26(cwd, ".claude");
4189
4442
  const hasAvatar = await pathExists(claudeRoot);
4190
4443
  if (!hasAvatar) {
4191
4444
  return {
@@ -4198,9 +4451,9 @@ async function gatherStatus(cwd) {
4198
4451
  hasAvatar: false
4199
4452
  };
4200
4453
  }
4201
- const packVersion = await isGitRepo(join24(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
4202
- const pendingDir = join24(claudeRoot, "_pending");
4203
- const pendingCount = await pathExists(pendingDir) ? (await fs10.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
4454
+ const packVersion = await isGitRepo(join26(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
4455
+ const pendingDir = join26(claudeRoot, "_pending");
4456
+ const pendingCount = await pathExists(pendingDir) ? (await fs13.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
4204
4457
  const backupCount = (await listBackups(cwd)).length;
4205
4458
  const techStackSummary = await readTechStackFirstLine(claudeRoot);
4206
4459
  return {
@@ -4214,7 +4467,7 @@ async function gatherStatus(cwd) {
4214
4467
  };
4215
4468
  }
4216
4469
  async function readTechStackFirstLine(claudeRoot) {
4217
- const techStackPath = join24(claudeRoot, "project", "tech-stack.md");
4470
+ const techStackPath = join26(claudeRoot, "project", "tech-stack.md");
4218
4471
  if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
4219
4472
  const content = await readText(techStackPath);
4220
4473
  const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
@@ -4237,201 +4490,8 @@ function renderStatusBox(s) {
4237
4490
  // src/commands/sync.ts
4238
4491
  import { join as join28 } from "path";
4239
4492
 
4240
- // src/lib/merge-pack-settings-into-project-settings.ts
4241
- import { promises as fs11 } from "fs";
4242
- import { join as join25 } from "path";
4243
- function backupFilename(originalPath) {
4244
- const d = /* @__PURE__ */ new Date();
4245
- const stamp = `${d.getFullYear().toString().slice(-2) + String(d.getMonth() + 1).padStart(2, "0") + String(d.getDate()).padStart(2, "0")}-${String(d.getHours()).padStart(2, "0")}${String(d.getMinutes()).padStart(2, "0")}`;
4246
- return `${originalPath}.backup-${stamp}`;
4247
- }
4248
- function unionDedupe(a, b) {
4249
- const seen = /* @__PURE__ */ new Set();
4250
- const out = [];
4251
- for (const item of [...a, ...b]) {
4252
- const key = typeof item === "string" ? item : JSON.stringify(item);
4253
- if (!seen.has(key)) {
4254
- seen.add(key);
4255
- out.push(item);
4256
- }
4257
- }
4258
- return out;
4259
- }
4260
- function mergeHooksPerEvent(packHooks, userHooks) {
4261
- const touched = [];
4262
- const merged = { ...userHooks };
4263
- for (const [event, packEntries] of Object.entries(packHooks)) {
4264
- const userEntries = userHooks[event] || [];
4265
- const union = unionDedupe(userEntries, packEntries);
4266
- if (union.length !== userEntries.length) {
4267
- touched.push(event);
4268
- }
4269
- merged[event] = union;
4270
- }
4271
- return { merged, touchedEvents: touched };
4272
- }
4273
- async function mergePackSettingsIntoProjectSettings(workspacePath) {
4274
- const packTemplatePath = join25(workspacePath, ".claude", "pack", "templates", "settings.json.tpl");
4275
- const projectSettingsPath = join25(workspacePath, ".claude", "settings.json");
4276
- if (!await pathExists(packTemplatePath)) {
4277
- return { action: "no-pack-template", changes: [] };
4278
- }
4279
- let packTemplate;
4280
- try {
4281
- const raw = await readText(packTemplatePath);
4282
- packTemplate = JSON.parse(raw);
4283
- } catch (err) {
4284
- throw new Error(
4285
- `Pack settings template kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: ${err.message}. Path: ${packTemplatePath}`
4286
- );
4287
- }
4288
- let userSettings = {};
4289
- let projectHasSettings = false;
4290
- if (await pathExists(projectSettingsPath)) {
4291
- projectHasSettings = true;
4292
- try {
4293
- userSettings = await readJson(projectSettingsPath);
4294
- } catch (err) {
4295
- throw new Error(
4296
- `Project settings.json kh\xF4ng parse \u0111\u01B0\u1EE3c: ${err.message}. Manual fix tr\u01B0\u1EDBc khi sync.`
4297
- );
4298
- }
4299
- }
4300
- const changes = [];
4301
- const merged = { ...userSettings };
4302
- if (packTemplate.statusLine && !userSettings.statusLine) {
4303
- merged.statusLine = packTemplate.statusLine;
4304
- changes.push("statusLine added");
4305
- }
4306
- if (typeof packTemplate.includeCoAuthoredBy === "boolean" && typeof userSettings.includeCoAuthoredBy !== "boolean") {
4307
- merged.includeCoAuthoredBy = packTemplate.includeCoAuthoredBy;
4308
- changes.push("includeCoAuthoredBy added");
4309
- }
4310
- if (packTemplate.model && !userSettings.model) {
4311
- merged.model = packTemplate.model;
4312
- changes.push("model added");
4313
- }
4314
- if (packTemplate.env) {
4315
- const mergedEnv = { ...userSettings.env || {} };
4316
- let envChanged = false;
4317
- for (const [k, v] of Object.entries(packTemplate.env)) {
4318
- if (!(k in mergedEnv)) {
4319
- mergedEnv[k] = v;
4320
- envChanged = true;
4321
- }
4322
- }
4323
- if (envChanged) {
4324
- merged.env = mergedEnv;
4325
- changes.push("env vars added from pack");
4326
- }
4327
- }
4328
- if (packTemplate.permissions) {
4329
- const userAllow = userSettings.permissions?.allow || [];
4330
- const userDeny = userSettings.permissions?.deny || [];
4331
- const packAllow = packTemplate.permissions.allow || [];
4332
- const packDeny = packTemplate.permissions.deny || [];
4333
- const mergedAllow = unionDedupe(userAllow, packAllow);
4334
- const mergedDeny = unionDedupe(userDeny, packDeny);
4335
- if (mergedAllow.length !== userAllow.length || mergedDeny.length !== userDeny.length) {
4336
- merged.permissions = { allow: mergedAllow, deny: mergedDeny };
4337
- changes.push(
4338
- `permissions union (+${mergedAllow.length - userAllow.length} allow, +${mergedDeny.length - userDeny.length} deny)`
4339
- );
4340
- }
4341
- }
4342
- if (packTemplate.hooks) {
4343
- const userHooks = userSettings.hooks || {};
4344
- const { merged: mergedHooks, touchedEvents } = mergeHooksPerEvent(
4345
- packTemplate.hooks,
4346
- userHooks
4347
- );
4348
- if (touchedEvents.length > 0) {
4349
- merged.hooks = mergedHooks;
4350
- changes.push(`hooks added for events: ${touchedEvents.join(", ")}`);
4351
- }
4352
- }
4353
- if (changes.length === 0) {
4354
- return { action: "no-change", changes: [] };
4355
- }
4356
- let backupPath;
4357
- if (projectHasSettings) {
4358
- backupPath = backupFilename(projectSettingsPath);
4359
- await fs11.copyFile(projectSettingsPath, backupPath);
4360
- }
4361
- await writeJsonAtomic(projectSettingsPath, merged);
4362
- return { action: "merged", backupPath, changes };
4363
- }
4364
-
4365
4493
  // src/lib/preview-team-pack-sync-changes-for-dry-run.ts
4366
4494
  import { join as join27 } from "path";
4367
-
4368
- // src/lib/symlink-farm-for-team-pack-mount-dirs.ts
4369
- import { promises as fs13 } from "fs";
4370
- import { dirname as dirname5, join as join26, relative as relative3 } from "path";
4371
-
4372
- // src/lib/backup-existing-dir-before-symlink-override.ts
4373
- import { promises as fs12 } from "fs";
4374
- function timestamp() {
4375
- const d = /* @__PURE__ */ new Date();
4376
- const pad = (n) => n.toString().padStart(2, "0");
4377
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
4378
- }
4379
- async function backupDirBeforeReplace(targetPath) {
4380
- const backupPath = `${targetPath}.backup-${timestamp()}`;
4381
- await fs12.rename(targetPath, backupPath);
4382
- return backupPath;
4383
- }
4384
-
4385
- // src/lib/symlink-farm-for-team-pack-mount-dirs.ts
4386
- var TEAM_PACK_MOUNT_DIRS = [
4387
- "skills",
4388
- "agents",
4389
- "commands",
4390
- "hooks",
4391
- "workflows",
4392
- "scripts",
4393
- "knowledge"
4394
- ];
4395
- async function isSymbolicLink(path) {
4396
- try {
4397
- const st = await fs13.lstat(path);
4398
- return st.isSymbolicLink();
4399
- } catch {
4400
- return false;
4401
- }
4402
- }
4403
- async function syncMountedDir(source, dest, force) {
4404
- const dir = relative3(dirname5(dest), dest) || dest;
4405
- if (!await pathExists(source)) {
4406
- return { dir, action: "source-missing" };
4407
- }
4408
- if (await pathExists(dest)) {
4409
- if (await isSymbolicLink(dest)) {
4410
- await fs13.unlink(dest);
4411
- } else if (force) {
4412
- const backupPath = await backupDirBeforeReplace(dest);
4413
- const relativeSource2 = relative3(dirname5(dest), source);
4414
- await fs13.symlink(relativeSource2, dest);
4415
- return { dir, action: "backed-up-and-linked", backupPath };
4416
- } else {
4417
- return { dir, action: "skipped-conflict" };
4418
- }
4419
- }
4420
- const relativeSource = relative3(dirname5(dest), source);
4421
- await fs13.symlink(relativeSource, dest);
4422
- return { dir, action: "created" };
4423
- }
4424
- async function syncAllMountDirs(packDir, claudeDir, force) {
4425
- const results = [];
4426
- for (const dir of TEAM_PACK_MOUNT_DIRS) {
4427
- const source = join26(packDir, dir);
4428
- const dest = join26(claudeDir, dir);
4429
- results.push(await syncMountedDir(source, dest, force));
4430
- }
4431
- return results;
4432
- }
4433
-
4434
- // src/lib/preview-team-pack-sync-changes-for-dry-run.ts
4435
4495
  async function inspectMountDir(packDir, claudeDir, dir) {
4436
4496
  const source = join27(packDir, dir);
4437
4497
  const dest = join27(claudeDir, dir);
@@ -4746,7 +4806,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
4746
4806
  }
4747
4807
 
4748
4808
  // src/commands/uninstall.ts
4749
- var CLI_VERSION = "1.6.0";
4809
+ var CLI_VERSION = "1.6.2";
4750
4810
  function registerUninstallCommand(program2) {
4751
4811
  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) => {
4752
4812
  try {
@@ -4828,7 +4888,7 @@ function printUninstallSuccessBox(backupPath) {
4828
4888
  }
4829
4889
 
4830
4890
  // src/index.ts
4831
- var CLI_VERSION2 = "1.6.0";
4891
+ var CLI_VERSION2 = "1.6.2";
4832
4892
  var program = new Command();
4833
4893
  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(
4834
4894
  "beforeAll",