@nalvietnam/avatar-cli 1.6.1 → 1.6.3

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
@@ -1851,8 +1851,8 @@ import { existsSync as existsSync5 } from "fs";
1851
1851
  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
- var DEFAULT_LLMLITE_MODEL = "nal-claude";
1855
- var DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-5";
1854
+ var FALLBACK_LLMLITE_MODEL = "nal-claude";
1855
+ var FALLBACK_ANTHROPIC_MODEL = "claude-sonnet-4-5";
1856
1856
  function normalizeAnthropicBaseUrl(rawBaseUrl) {
1857
1857
  const cleaned = rawBaseUrl.replace(/\/+$/, "");
1858
1858
  if (cleaned.endsWith("/v1")) return `${cleaned}/`;
@@ -1867,14 +1867,16 @@ async function readSettingsForWikiCredentials(workspacePath) {
1867
1867
  const env = settings.env || {};
1868
1868
  const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : null;
1869
1869
  if (!baseUrl) return null;
1870
- const userModel = typeof env.ANTHROPIC_MODEL === "string" ? env.ANTHROPIC_MODEL : "";
1870
+ const topLevelModel = typeof settings.model === "string" ? settings.model : "";
1871
+ const envModel = typeof env.ANTHROPIC_MODEL === "string" ? env.ANTHROPIC_MODEL : "";
1872
+ const userModel = topLevelModel.length > 0 ? topLevelModel : envModel;
1871
1873
  const anthropicKey = typeof env.ANTHROPIC_API_KEY === "string" ? env.ANTHROPIC_API_KEY : null;
1872
1874
  if (anthropicKey) {
1873
1875
  return {
1874
1876
  provider: "anthropic",
1875
1877
  apiKey: anthropicKey,
1876
1878
  baseUrl: normalizeAnthropicBaseUrl(baseUrl),
1877
- model: userModel.length > 0 ? userModel : DEFAULT_ANTHROPIC_MODEL
1879
+ model: userModel.length > 0 ? userModel : FALLBACK_ANTHROPIC_MODEL
1878
1880
  };
1879
1881
  }
1880
1882
  const llmliteToken = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : null;
@@ -1883,7 +1885,7 @@ async function readSettingsForWikiCredentials(workspacePath) {
1883
1885
  provider: "llmlite",
1884
1886
  apiKey: llmliteToken,
1885
1887
  baseUrl,
1886
- model: userModel.length > 0 ? userModel : DEFAULT_LLMLITE_MODEL
1888
+ model: userModel.length > 0 ? userModel : FALLBACK_LLMLITE_MODEL
1887
1889
  };
1888
1890
  }
1889
1891
  return null;
@@ -2197,7 +2199,7 @@ function registerGitnexusCommand(program2) {
2197
2199
  }
2198
2200
 
2199
2201
  // src/commands/init.ts
2200
- import { basename, join as join22, relative as relative2, resolve } from "path";
2202
+ import { basename, join as join24, relative as relative3, resolve } from "path";
2201
2203
  import { confirm as confirm5, input as input5, select as select9 } from "@inquirer/prompts";
2202
2204
  import boxen5 from "boxen";
2203
2205
 
@@ -3060,6 +3062,131 @@ function linkExistingRemoteToWorkspace(args) {
3060
3062
  return { sshUrl, httpsUrl };
3061
3063
  }
3062
3064
 
3065
+ // src/lib/merge-pack-settings-into-project-settings.ts
3066
+ import { promises as fs9 } from "fs";
3067
+ import { join as join17 } from "path";
3068
+ function backupFilename(originalPath) {
3069
+ const d = /* @__PURE__ */ new Date();
3070
+ 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")}`;
3071
+ return `${originalPath}.backup-${stamp}`;
3072
+ }
3073
+ function unionDedupe(a, b) {
3074
+ const seen = /* @__PURE__ */ new Set();
3075
+ const out = [];
3076
+ for (const item of [...a, ...b]) {
3077
+ const key = typeof item === "string" ? item : JSON.stringify(item);
3078
+ if (!seen.has(key)) {
3079
+ seen.add(key);
3080
+ out.push(item);
3081
+ }
3082
+ }
3083
+ return out;
3084
+ }
3085
+ function mergeHooksPerEvent(packHooks, userHooks) {
3086
+ const touched = [];
3087
+ const merged = { ...userHooks };
3088
+ for (const [event, packEntries] of Object.entries(packHooks)) {
3089
+ const userEntries = userHooks[event] || [];
3090
+ const union = unionDedupe(userEntries, packEntries);
3091
+ if (union.length !== userEntries.length) {
3092
+ touched.push(event);
3093
+ }
3094
+ merged[event] = union;
3095
+ }
3096
+ return { merged, touchedEvents: touched };
3097
+ }
3098
+ async function mergePackSettingsIntoProjectSettings(workspacePath) {
3099
+ const packTemplatePath = join17(workspacePath, ".claude", "pack", "templates", "settings.json.tpl");
3100
+ const projectSettingsPath = join17(workspacePath, ".claude", "settings.json");
3101
+ if (!await pathExists(packTemplatePath)) {
3102
+ return { action: "no-pack-template", changes: [] };
3103
+ }
3104
+ let packTemplate;
3105
+ try {
3106
+ const raw = await readText(packTemplatePath);
3107
+ packTemplate = JSON.parse(raw);
3108
+ } catch (err) {
3109
+ throw new Error(
3110
+ `Pack settings template kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: ${err.message}. Path: ${packTemplatePath}`
3111
+ );
3112
+ }
3113
+ let userSettings = {};
3114
+ let projectHasSettings = false;
3115
+ if (await pathExists(projectSettingsPath)) {
3116
+ projectHasSettings = true;
3117
+ try {
3118
+ userSettings = await readJson(projectSettingsPath);
3119
+ } catch (err) {
3120
+ throw new Error(
3121
+ `Project settings.json kh\xF4ng parse \u0111\u01B0\u1EE3c: ${err.message}. Manual fix tr\u01B0\u1EDBc khi sync.`
3122
+ );
3123
+ }
3124
+ }
3125
+ const changes = [];
3126
+ const merged = { ...userSettings };
3127
+ if (packTemplate.statusLine && !userSettings.statusLine) {
3128
+ merged.statusLine = packTemplate.statusLine;
3129
+ changes.push("statusLine added");
3130
+ }
3131
+ if (typeof packTemplate.includeCoAuthoredBy === "boolean" && typeof userSettings.includeCoAuthoredBy !== "boolean") {
3132
+ merged.includeCoAuthoredBy = packTemplate.includeCoAuthoredBy;
3133
+ changes.push("includeCoAuthoredBy added");
3134
+ }
3135
+ if (packTemplate.model && !userSettings.model) {
3136
+ merged.model = packTemplate.model;
3137
+ changes.push("model added");
3138
+ }
3139
+ if (packTemplate.env) {
3140
+ const mergedEnv = { ...userSettings.env || {} };
3141
+ let envChanged = false;
3142
+ for (const [k, v] of Object.entries(packTemplate.env)) {
3143
+ if (!(k in mergedEnv)) {
3144
+ mergedEnv[k] = v;
3145
+ envChanged = true;
3146
+ }
3147
+ }
3148
+ if (envChanged) {
3149
+ merged.env = mergedEnv;
3150
+ changes.push("env vars added from pack");
3151
+ }
3152
+ }
3153
+ if (packTemplate.permissions) {
3154
+ const userAllow = userSettings.permissions?.allow || [];
3155
+ const userDeny = userSettings.permissions?.deny || [];
3156
+ const packAllow = packTemplate.permissions.allow || [];
3157
+ const packDeny = packTemplate.permissions.deny || [];
3158
+ const mergedAllow = unionDedupe(userAllow, packAllow);
3159
+ const mergedDeny = unionDedupe(userDeny, packDeny);
3160
+ if (mergedAllow.length !== userAllow.length || mergedDeny.length !== userDeny.length) {
3161
+ merged.permissions = { allow: mergedAllow, deny: mergedDeny };
3162
+ changes.push(
3163
+ `permissions union (+${mergedAllow.length - userAllow.length} allow, +${mergedDeny.length - userDeny.length} deny)`
3164
+ );
3165
+ }
3166
+ }
3167
+ if (packTemplate.hooks) {
3168
+ const userHooks = userSettings.hooks || {};
3169
+ const { merged: mergedHooks, touchedEvents } = mergeHooksPerEvent(
3170
+ packTemplate.hooks,
3171
+ userHooks
3172
+ );
3173
+ if (touchedEvents.length > 0) {
3174
+ merged.hooks = mergedHooks;
3175
+ changes.push(`hooks added for events: ${touchedEvents.join(", ")}`);
3176
+ }
3177
+ }
3178
+ if (changes.length === 0) {
3179
+ return { action: "no-change", changes: [] };
3180
+ }
3181
+ let backupPath;
3182
+ if (projectHasSettings) {
3183
+ backupPath = backupFilename(projectSettingsPath);
3184
+ await fs9.copyFile(projectSettingsPath, backupPath);
3185
+ }
3186
+ await writeJsonAtomic(projectSettingsPath, merged);
3187
+ return { action: "merged", backupPath, changes };
3188
+ }
3189
+
3063
3190
  // src/lib/safe-bootstrap-for-dirty-folder.ts
3064
3191
  import { readdirSync } from "fs";
3065
3192
  import { select as select8 } from "@inquirer/prompts";
@@ -3067,9 +3194,9 @@ import { simpleGit as simpleGit3 } from "simple-git";
3067
3194
 
3068
3195
  // src/lib/check-folder-has-git.ts
3069
3196
  import { existsSync as existsSync6, statSync } from "fs";
3070
- import { join as join17 } from "path";
3197
+ import { join as join18 } from "path";
3071
3198
  function checkFolderHasGit(folderPath) {
3072
- const gitPath = join17(folderPath, ".git");
3199
+ const gitPath = join18(folderPath, ".git");
3073
3200
  if (!existsSync6(gitPath)) return false;
3074
3201
  const stat = statSync(gitPath);
3075
3202
  return stat.isDirectory() || stat.isFile();
@@ -3101,7 +3228,7 @@ async function createInitialGitCommit(folderPath) {
3101
3228
 
3102
3229
  // src/lib/detect-folder-tech-stack.ts
3103
3230
  import { existsSync as existsSync7 } from "fs";
3104
- import { join as join18 } from "path";
3231
+ import { join as join19 } from "path";
3105
3232
  var SIGNATURES = {
3106
3233
  node: ["package.json"],
3107
3234
  python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
@@ -3113,7 +3240,7 @@ var SIGNATURES = {
3113
3240
  function detectFolderTechStack(folderPath) {
3114
3241
  const matched = [];
3115
3242
  for (const [stack, files] of Object.entries(SIGNATURES)) {
3116
- if (files.some((f) => existsSync7(join18(folderPath, f)))) {
3243
+ if (files.some((f) => existsSync7(join19(folderPath, f)))) {
3117
3244
  matched.push(stack);
3118
3245
  }
3119
3246
  }
@@ -3122,25 +3249,25 @@ function detectFolderTechStack(folderPath) {
3122
3249
 
3123
3250
  // src/lib/gitignore-template-loader.ts
3124
3251
  import { readFileSync as readFileSync3 } from "fs";
3125
- import { dirname as dirname4, join as join19 } from "path";
3252
+ import { dirname as dirname4, join as join20 } from "path";
3126
3253
  import { fileURLToPath as fileURLToPath2 } from "url";
3127
3254
  var __dirname = dirname4(fileURLToPath2(import.meta.url));
3128
3255
  var CANDIDATE_DIRS = [
3129
3256
  // Bundled production: dist/index.js → __dirname = .../dist/, sibling dist/templates
3130
- join19(__dirname, "templates", "gitignore"),
3257
+ join20(__dirname, "templates", "gitignore"),
3131
3258
  // Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
3132
- join19(__dirname, "..", "templates", "gitignore"),
3259
+ join20(__dirname, "..", "templates", "gitignore"),
3133
3260
  // Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
3134
- join19(__dirname, "..", "..", "src", "templates", "gitignore"),
3261
+ join20(__dirname, "..", "..", "src", "templates", "gitignore"),
3135
3262
  // npm-installed alt: __dirname = .../dist/ → package_root/src/templates
3136
- join19(__dirname, "..", "src", "templates", "gitignore")
3263
+ join20(__dirname, "..", "src", "templates", "gitignore")
3137
3264
  ];
3138
3265
  var AVATAR_MARKER_START = "# === avatar ===";
3139
3266
  var AVATAR_MARKER_END = "# === /avatar ===";
3140
3267
  function readTemplate(stack) {
3141
3268
  for (const dir of CANDIDATE_DIRS) {
3142
3269
  try {
3143
- return readFileSync3(join19(dir, `${stack}.txt`), "utf8");
3270
+ return readFileSync3(join20(dir, `${stack}.txt`), "utf8");
3144
3271
  } catch {
3145
3272
  }
3146
3273
  }
@@ -3155,9 +3282,9 @@ ${readTemplate(s).trim()}`);
3155
3282
 
3156
3283
  // src/lib/write-or-merge-gitignore.ts
3157
3284
  import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
3158
- import { join as join20 } from "path";
3285
+ import { join as join21 } from "path";
3159
3286
  function writeOrMergeGitignore(folderPath, avatarBlock) {
3160
- const path = join20(folderPath, ".gitignore");
3287
+ const path = join21(folderPath, ".gitignore");
3161
3288
  if (!existsSync8(path)) {
3162
3289
  writeFileSync(path, avatarBlock, "utf8");
3163
3290
  return;
@@ -3332,9 +3459,75 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
3332
3459
  await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
3333
3460
  }
3334
3461
 
3462
+ // src/lib/symlink-farm-for-team-pack-mount-dirs.ts
3463
+ import { promises as fs11 } from "fs";
3464
+ import { dirname as dirname5, join as join22, relative as relative2 } from "path";
3465
+
3466
+ // src/lib/backup-existing-dir-before-symlink-override.ts
3467
+ import { promises as fs10 } from "fs";
3468
+ function timestamp() {
3469
+ const d = /* @__PURE__ */ new Date();
3470
+ const pad = (n) => n.toString().padStart(2, "0");
3471
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
3472
+ }
3473
+ async function backupDirBeforeReplace(targetPath) {
3474
+ const backupPath = `${targetPath}.backup-${timestamp()}`;
3475
+ await fs10.rename(targetPath, backupPath);
3476
+ return backupPath;
3477
+ }
3478
+
3479
+ // src/lib/symlink-farm-for-team-pack-mount-dirs.ts
3480
+ var TEAM_PACK_MOUNT_DIRS = [
3481
+ "skills",
3482
+ "agents",
3483
+ "commands",
3484
+ "hooks",
3485
+ "workflows",
3486
+ "scripts",
3487
+ "knowledge"
3488
+ ];
3489
+ async function isSymbolicLink(path) {
3490
+ try {
3491
+ const st = await fs11.lstat(path);
3492
+ return st.isSymbolicLink();
3493
+ } catch {
3494
+ return false;
3495
+ }
3496
+ }
3497
+ async function syncMountedDir(source, dest, force) {
3498
+ const dir = relative2(dirname5(dest), dest) || dest;
3499
+ if (!await pathExists(source)) {
3500
+ return { dir, action: "source-missing" };
3501
+ }
3502
+ if (await pathExists(dest)) {
3503
+ if (await isSymbolicLink(dest)) {
3504
+ await fs11.unlink(dest);
3505
+ } else if (force) {
3506
+ const backupPath = await backupDirBeforeReplace(dest);
3507
+ const relativeSource2 = relative2(dirname5(dest), source);
3508
+ await fs11.symlink(relativeSource2, dest);
3509
+ return { dir, action: "backed-up-and-linked", backupPath };
3510
+ } else {
3511
+ return { dir, action: "skipped-conflict" };
3512
+ }
3513
+ }
3514
+ const relativeSource = relative2(dirname5(dest), source);
3515
+ await fs11.symlink(relativeSource, dest);
3516
+ return { dir, action: "created" };
3517
+ }
3518
+ async function syncAllMountDirs(packDir, claudeDir, force) {
3519
+ const results = [];
3520
+ for (const dir of TEAM_PACK_MOUNT_DIRS) {
3521
+ const source = join22(packDir, dir);
3522
+ const dest = join22(claudeDir, dir);
3523
+ results.push(await syncMountedDir(source, dest, force));
3524
+ }
3525
+ return results;
3526
+ }
3527
+
3335
3528
  // src/commands/init-conflict-detection-helpers.ts
3336
3529
  import { readdir } from "fs/promises";
3337
- import { join as join21 } from "path";
3530
+ import { join as join23 } from "path";
3338
3531
  async function isEmptyOrMissing(path) {
3339
3532
  if (!await pathExists(path)) return true;
3340
3533
  try {
@@ -3347,7 +3540,7 @@ async function isEmptyOrMissing(path) {
3347
3540
  }
3348
3541
  async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
3349
3542
  for (let i = 2; i < maxAttempts; i++) {
3350
- const candidate = join21(parent, `${desiredName}-${i}`);
3543
+ const candidate = join23(parent, `${desiredName}-${i}`);
3351
3544
  if (await isEmptyOrMissing(candidate)) return candidate;
3352
3545
  }
3353
3546
  return null;
@@ -3777,7 +3970,7 @@ async function runInitFromScratch(opts, ownerEmail) {
3777
3970
  const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
3778
3971
  const workspaceParent = resolve(opts.workspaceParent ?? ".");
3779
3972
  const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
3780
- const srcPath = join22(workspacePath, "src");
3973
+ const srcPath = join24(workspacePath, "src");
3781
3974
  await ensureDir(workspacePath);
3782
3975
  await ensureDir(srcPath);
3783
3976
  await safeBootstrapGitInFolder(srcPath, { autoYes: true });
@@ -3915,11 +4108,12 @@ async function finalizeWorkspaceScaffold(args) {
3915
4108
  await writeRootClaudeMd(args.workspacePath, vars);
3916
4109
  await writeProjectSettings(args.workspacePath, vars);
3917
4110
  await appendGitignoreEntries(args.workspacePath);
3918
- await ensureDir(join22(args.workspacePath, "notes"));
3919
- await ensureDir(join22(args.workspacePath, "scripts"));
3920
- await installGitHook(join22(args.workspacePath, ".git"), "post-merge");
3921
- await installGitHook(join22(args.workspacePath, ".git", "modules", "src"), "pre-push");
4111
+ await ensureDir(join24(args.workspacePath, "notes"));
4112
+ await ensureDir(join24(args.workspacePath, "scripts"));
4113
+ await installGitHook(join24(args.workspacePath, ".git"), "post-merge");
4114
+ await installGitHook(join24(args.workspacePath, ".git", "modules", "src"), "pre-push");
3922
4115
  log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
4116
+ await autoSyncPackOnInit(args.workspacePath);
3923
4117
  await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
3924
4118
  await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
3925
4119
  await maybeCreateWorkspaceRemote(args);
@@ -3954,6 +4148,39 @@ async function finalizeWorkspaceScaffold(args) {
3954
4148
  }
3955
4149
  printInitSuccessBox(args.workspacePath, args.flow, aiResult, gitnexusResult);
3956
4150
  }
4151
+ async function autoSyncPackOnInit(workspacePath) {
4152
+ const packDir = join24(workspacePath, TEAM_PACK_RELATIVE_PATH);
4153
+ if (!await pathExists(packDir)) {
4154
+ log.dim("Pack submodule kh\xF4ng t\u1ED3n t\u1EA1i (skip auto-sync). C\xF3 th\u1EC3 ch\u1EA1y `avatar sync` sau.");
4155
+ return;
4156
+ }
4157
+ const claudeDir = join24(workspacePath, ".claude");
4158
+ log.info("Auto-sync pack content v\xE0o .claude/ (symlinks + settings merge)...");
4159
+ try {
4160
+ const results = await syncAllMountDirs(packDir, claudeDir, false);
4161
+ const created = results.filter((r) => r.action === "created" || r.action === "updated").length;
4162
+ const missing = results.filter((r) => r.action === "source-missing").length;
4163
+ log.success(
4164
+ ` \u2713 Symlinks: ${created} created${missing > 0 ? `, ${missing} source-missing (pack thi\u1EBFu dir)` : ""}`
4165
+ );
4166
+ const mergeResult = await mergePackSettingsIntoProjectSettings(workspacePath);
4167
+ switch (mergeResult.action) {
4168
+ case "merged":
4169
+ log.success(` \u2713 settings.json merged (${mergeResult.changes.join("; ")})`);
4170
+ break;
4171
+ case "no-change":
4172
+ log.dim(" - settings.json \u0111\xE3 sync, kh\xF4ng c\u1EA7n thay \u0111\u1ED5i.");
4173
+ break;
4174
+ case "no-pack-template":
4175
+ log.dim(" - Pack kh\xF4ng c\xF3 templates/settings.json.tpl, skip merge.");
4176
+ break;
4177
+ }
4178
+ } catch (err) {
4179
+ log.warn(
4180
+ `Auto-sync pack fail: ${err instanceof Error ? err.message : err}. Ch\u1EA1y \`avatar sync\` th\u1EE7 c\xF4ng \u0111\u1EC3 retry.`
4181
+ );
4182
+ }
4183
+ }
3957
4184
  async function maybeCreateWorkspaceRemote(args) {
3958
4185
  if (args.skipCommit) {
3959
4186
  log.dim("Skip workspace remote (ch\u01B0a commit). Setup sau qua: gh repo create ...");
@@ -4041,7 +4268,7 @@ async function maybeCreateWorkspaceRemote(args) {
4041
4268
  }
4042
4269
  }
4043
4270
  async function resolveWorkspacePath(parent, desiredName, force) {
4044
- const desired = join22(parent, desiredName);
4271
+ const desired = join24(parent, desiredName);
4045
4272
  if (await isEmptyOrMissing(desired)) return desired;
4046
4273
  log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
4047
4274
  while (true) {
@@ -4072,7 +4299,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
4072
4299
  message: "T\xEAn workspace m\u1EDBi:",
4073
4300
  validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
4074
4301
  });
4075
- const newPath = join22(parent, newName.trim());
4302
+ const newPath = join24(parent, newName.trim());
4076
4303
  if (await isEmptyOrMissing(newPath)) return newPath;
4077
4304
  log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
4078
4305
  }
@@ -4115,7 +4342,7 @@ function formatGitnexusStatusLine(result) {
4115
4342
  }
4116
4343
  function printInitSuccessBox(rootPath, flow, aiResult = null, gitnexusResult = null) {
4117
4344
  const lines = [
4118
- `${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative2(process.cwd(), rootPath) || rootPath}`,
4345
+ `${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative3(process.cwd(), rootPath) || rootPath}`,
4119
4346
  ` ${chalk.dim(`(flow: ${flow})`)}`,
4120
4347
  formatAiStatusLine(aiResult),
4121
4348
  formatGitnexusStatusLine(gitnexusResult),
@@ -4178,18 +4405,18 @@ function registerSecretsCommand(program2) {
4178
4405
  }
4179
4406
 
4180
4407
  // src/commands/status.ts
4181
- import { promises as fs10 } from "fs";
4182
- import { join as join24 } from "path";
4408
+ import { promises as fs13 } from "fs";
4409
+ import { join as join26 } from "path";
4183
4410
  import boxen6 from "boxen";
4184
4411
 
4185
4412
  // src/lib/pack-backup-manager.ts
4186
- import { promises as fs9 } from "fs";
4187
- import { join as join23 } from "path";
4413
+ import { promises as fs12 } from "fs";
4414
+ import { join as join25 } from "path";
4188
4415
  var BACKUP_DIR_NAME = "_backup";
4189
4416
  async function listBackups(projectRoot) {
4190
- const dir = join23(projectRoot, ".claude", BACKUP_DIR_NAME);
4417
+ const dir = join25(projectRoot, ".claude", BACKUP_DIR_NAME);
4191
4418
  if (!await pathExists(dir)) return [];
4192
- const entries = await fs9.readdir(dir, { withFileTypes: true });
4419
+ const entries = await fs12.readdir(dir, { withFileTypes: true });
4193
4420
  return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
4194
4421
  }
4195
4422
 
@@ -4213,7 +4440,7 @@ function registerStatusCommand(program2) {
4213
4440
  }
4214
4441
  async function gatherStatus(cwd) {
4215
4442
  const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
4216
- const claudeRoot = join24(cwd, ".claude");
4443
+ const claudeRoot = join26(cwd, ".claude");
4217
4444
  const hasAvatar = await pathExists(claudeRoot);
4218
4445
  if (!hasAvatar) {
4219
4446
  return {
@@ -4226,9 +4453,9 @@ async function gatherStatus(cwd) {
4226
4453
  hasAvatar: false
4227
4454
  };
4228
4455
  }
4229
- const packVersion = await isGitRepo(join24(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
4230
- const pendingDir = join24(claudeRoot, "_pending");
4231
- const pendingCount = await pathExists(pendingDir) ? (await fs10.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
4456
+ const packVersion = await isGitRepo(join26(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
4457
+ const pendingDir = join26(claudeRoot, "_pending");
4458
+ const pendingCount = await pathExists(pendingDir) ? (await fs13.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
4232
4459
  const backupCount = (await listBackups(cwd)).length;
4233
4460
  const techStackSummary = await readTechStackFirstLine(claudeRoot);
4234
4461
  return {
@@ -4242,7 +4469,7 @@ async function gatherStatus(cwd) {
4242
4469
  };
4243
4470
  }
4244
4471
  async function readTechStackFirstLine(claudeRoot) {
4245
- const techStackPath = join24(claudeRoot, "project", "tech-stack.md");
4472
+ const techStackPath = join26(claudeRoot, "project", "tech-stack.md");
4246
4473
  if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
4247
4474
  const content = await readText(techStackPath);
4248
4475
  const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
@@ -4265,201 +4492,8 @@ function renderStatusBox(s) {
4265
4492
  // src/commands/sync.ts
4266
4493
  import { join as join28 } from "path";
4267
4494
 
4268
- // src/lib/merge-pack-settings-into-project-settings.ts
4269
- import { promises as fs11 } from "fs";
4270
- import { join as join25 } from "path";
4271
- function backupFilename(originalPath) {
4272
- const d = /* @__PURE__ */ new Date();
4273
- 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")}`;
4274
- return `${originalPath}.backup-${stamp}`;
4275
- }
4276
- function unionDedupe(a, b) {
4277
- const seen = /* @__PURE__ */ new Set();
4278
- const out = [];
4279
- for (const item of [...a, ...b]) {
4280
- const key = typeof item === "string" ? item : JSON.stringify(item);
4281
- if (!seen.has(key)) {
4282
- seen.add(key);
4283
- out.push(item);
4284
- }
4285
- }
4286
- return out;
4287
- }
4288
- function mergeHooksPerEvent(packHooks, userHooks) {
4289
- const touched = [];
4290
- const merged = { ...userHooks };
4291
- for (const [event, packEntries] of Object.entries(packHooks)) {
4292
- const userEntries = userHooks[event] || [];
4293
- const union = unionDedupe(userEntries, packEntries);
4294
- if (union.length !== userEntries.length) {
4295
- touched.push(event);
4296
- }
4297
- merged[event] = union;
4298
- }
4299
- return { merged, touchedEvents: touched };
4300
- }
4301
- async function mergePackSettingsIntoProjectSettings(workspacePath) {
4302
- const packTemplatePath = join25(workspacePath, ".claude", "pack", "templates", "settings.json.tpl");
4303
- const projectSettingsPath = join25(workspacePath, ".claude", "settings.json");
4304
- if (!await pathExists(packTemplatePath)) {
4305
- return { action: "no-pack-template", changes: [] };
4306
- }
4307
- let packTemplate;
4308
- try {
4309
- const raw = await readText(packTemplatePath);
4310
- packTemplate = JSON.parse(raw);
4311
- } catch (err) {
4312
- throw new Error(
4313
- `Pack settings template kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: ${err.message}. Path: ${packTemplatePath}`
4314
- );
4315
- }
4316
- let userSettings = {};
4317
- let projectHasSettings = false;
4318
- if (await pathExists(projectSettingsPath)) {
4319
- projectHasSettings = true;
4320
- try {
4321
- userSettings = await readJson(projectSettingsPath);
4322
- } catch (err) {
4323
- throw new Error(
4324
- `Project settings.json kh\xF4ng parse \u0111\u01B0\u1EE3c: ${err.message}. Manual fix tr\u01B0\u1EDBc khi sync.`
4325
- );
4326
- }
4327
- }
4328
- const changes = [];
4329
- const merged = { ...userSettings };
4330
- if (packTemplate.statusLine && !userSettings.statusLine) {
4331
- merged.statusLine = packTemplate.statusLine;
4332
- changes.push("statusLine added");
4333
- }
4334
- if (typeof packTemplate.includeCoAuthoredBy === "boolean" && typeof userSettings.includeCoAuthoredBy !== "boolean") {
4335
- merged.includeCoAuthoredBy = packTemplate.includeCoAuthoredBy;
4336
- changes.push("includeCoAuthoredBy added");
4337
- }
4338
- if (packTemplate.model && !userSettings.model) {
4339
- merged.model = packTemplate.model;
4340
- changes.push("model added");
4341
- }
4342
- if (packTemplate.env) {
4343
- const mergedEnv = { ...userSettings.env || {} };
4344
- let envChanged = false;
4345
- for (const [k, v] of Object.entries(packTemplate.env)) {
4346
- if (!(k in mergedEnv)) {
4347
- mergedEnv[k] = v;
4348
- envChanged = true;
4349
- }
4350
- }
4351
- if (envChanged) {
4352
- merged.env = mergedEnv;
4353
- changes.push("env vars added from pack");
4354
- }
4355
- }
4356
- if (packTemplate.permissions) {
4357
- const userAllow = userSettings.permissions?.allow || [];
4358
- const userDeny = userSettings.permissions?.deny || [];
4359
- const packAllow = packTemplate.permissions.allow || [];
4360
- const packDeny = packTemplate.permissions.deny || [];
4361
- const mergedAllow = unionDedupe(userAllow, packAllow);
4362
- const mergedDeny = unionDedupe(userDeny, packDeny);
4363
- if (mergedAllow.length !== userAllow.length || mergedDeny.length !== userDeny.length) {
4364
- merged.permissions = { allow: mergedAllow, deny: mergedDeny };
4365
- changes.push(
4366
- `permissions union (+${mergedAllow.length - userAllow.length} allow, +${mergedDeny.length - userDeny.length} deny)`
4367
- );
4368
- }
4369
- }
4370
- if (packTemplate.hooks) {
4371
- const userHooks = userSettings.hooks || {};
4372
- const { merged: mergedHooks, touchedEvents } = mergeHooksPerEvent(
4373
- packTemplate.hooks,
4374
- userHooks
4375
- );
4376
- if (touchedEvents.length > 0) {
4377
- merged.hooks = mergedHooks;
4378
- changes.push(`hooks added for events: ${touchedEvents.join(", ")}`);
4379
- }
4380
- }
4381
- if (changes.length === 0) {
4382
- return { action: "no-change", changes: [] };
4383
- }
4384
- let backupPath;
4385
- if (projectHasSettings) {
4386
- backupPath = backupFilename(projectSettingsPath);
4387
- await fs11.copyFile(projectSettingsPath, backupPath);
4388
- }
4389
- await writeJsonAtomic(projectSettingsPath, merged);
4390
- return { action: "merged", backupPath, changes };
4391
- }
4392
-
4393
4495
  // src/lib/preview-team-pack-sync-changes-for-dry-run.ts
4394
4496
  import { join as join27 } from "path";
4395
-
4396
- // src/lib/symlink-farm-for-team-pack-mount-dirs.ts
4397
- import { promises as fs13 } from "fs";
4398
- import { dirname as dirname5, join as join26, relative as relative3 } from "path";
4399
-
4400
- // src/lib/backup-existing-dir-before-symlink-override.ts
4401
- import { promises as fs12 } from "fs";
4402
- function timestamp() {
4403
- const d = /* @__PURE__ */ new Date();
4404
- const pad = (n) => n.toString().padStart(2, "0");
4405
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
4406
- }
4407
- async function backupDirBeforeReplace(targetPath) {
4408
- const backupPath = `${targetPath}.backup-${timestamp()}`;
4409
- await fs12.rename(targetPath, backupPath);
4410
- return backupPath;
4411
- }
4412
-
4413
- // src/lib/symlink-farm-for-team-pack-mount-dirs.ts
4414
- var TEAM_PACK_MOUNT_DIRS = [
4415
- "skills",
4416
- "agents",
4417
- "commands",
4418
- "hooks",
4419
- "workflows",
4420
- "scripts",
4421
- "knowledge"
4422
- ];
4423
- async function isSymbolicLink(path) {
4424
- try {
4425
- const st = await fs13.lstat(path);
4426
- return st.isSymbolicLink();
4427
- } catch {
4428
- return false;
4429
- }
4430
- }
4431
- async function syncMountedDir(source, dest, force) {
4432
- const dir = relative3(dirname5(dest), dest) || dest;
4433
- if (!await pathExists(source)) {
4434
- return { dir, action: "source-missing" };
4435
- }
4436
- if (await pathExists(dest)) {
4437
- if (await isSymbolicLink(dest)) {
4438
- await fs13.unlink(dest);
4439
- } else if (force) {
4440
- const backupPath = await backupDirBeforeReplace(dest);
4441
- const relativeSource2 = relative3(dirname5(dest), source);
4442
- await fs13.symlink(relativeSource2, dest);
4443
- return { dir, action: "backed-up-and-linked", backupPath };
4444
- } else {
4445
- return { dir, action: "skipped-conflict" };
4446
- }
4447
- }
4448
- const relativeSource = relative3(dirname5(dest), source);
4449
- await fs13.symlink(relativeSource, dest);
4450
- return { dir, action: "created" };
4451
- }
4452
- async function syncAllMountDirs(packDir, claudeDir, force) {
4453
- const results = [];
4454
- for (const dir of TEAM_PACK_MOUNT_DIRS) {
4455
- const source = join26(packDir, dir);
4456
- const dest = join26(claudeDir, dir);
4457
- results.push(await syncMountedDir(source, dest, force));
4458
- }
4459
- return results;
4460
- }
4461
-
4462
- // src/lib/preview-team-pack-sync-changes-for-dry-run.ts
4463
4497
  async function inspectMountDir(packDir, claudeDir, dir) {
4464
4498
  const source = join27(packDir, dir);
4465
4499
  const dest = join27(claudeDir, dir);
@@ -4774,7 +4808,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
4774
4808
  }
4775
4809
 
4776
4810
  // src/commands/uninstall.ts
4777
- var CLI_VERSION = "1.6.1";
4811
+ var CLI_VERSION = "1.6.3";
4778
4812
  function registerUninstallCommand(program2) {
4779
4813
  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) => {
4780
4814
  try {
@@ -4856,7 +4890,7 @@ function printUninstallSuccessBox(backupPath) {
4856
4890
  }
4857
4891
 
4858
4892
  // src/index.ts
4859
- var CLI_VERSION2 = "1.6.1";
4893
+ var CLI_VERSION2 = "1.6.3";
4860
4894
  var program = new Command();
4861
4895
  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(
4862
4896
  "beforeAll",