@nalvietnam/avatar-cli 1.6.1 → 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
@@ -2197,7 +2197,7 @@ function registerGitnexusCommand(program2) {
2197
2197
  }
2198
2198
 
2199
2199
  // src/commands/init.ts
2200
- import { basename, join as join22, relative as relative2, resolve } from "path";
2200
+ import { basename, join as join24, relative as relative3, resolve } from "path";
2201
2201
  import { confirm as confirm5, input as input5, select as select9 } from "@inquirer/prompts";
2202
2202
  import boxen5 from "boxen";
2203
2203
 
@@ -3060,6 +3060,131 @@ function linkExistingRemoteToWorkspace(args) {
3060
3060
  return { sshUrl, httpsUrl };
3061
3061
  }
3062
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
+
3063
3188
  // src/lib/safe-bootstrap-for-dirty-folder.ts
3064
3189
  import { readdirSync } from "fs";
3065
3190
  import { select as select8 } from "@inquirer/prompts";
@@ -3067,9 +3192,9 @@ import { simpleGit as simpleGit3 } from "simple-git";
3067
3192
 
3068
3193
  // src/lib/check-folder-has-git.ts
3069
3194
  import { existsSync as existsSync6, statSync } from "fs";
3070
- import { join as join17 } from "path";
3195
+ import { join as join18 } from "path";
3071
3196
  function checkFolderHasGit(folderPath) {
3072
- const gitPath = join17(folderPath, ".git");
3197
+ const gitPath = join18(folderPath, ".git");
3073
3198
  if (!existsSync6(gitPath)) return false;
3074
3199
  const stat = statSync(gitPath);
3075
3200
  return stat.isDirectory() || stat.isFile();
@@ -3101,7 +3226,7 @@ async function createInitialGitCommit(folderPath) {
3101
3226
 
3102
3227
  // src/lib/detect-folder-tech-stack.ts
3103
3228
  import { existsSync as existsSync7 } from "fs";
3104
- import { join as join18 } from "path";
3229
+ import { join as join19 } from "path";
3105
3230
  var SIGNATURES = {
3106
3231
  node: ["package.json"],
3107
3232
  python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
@@ -3113,7 +3238,7 @@ var SIGNATURES = {
3113
3238
  function detectFolderTechStack(folderPath) {
3114
3239
  const matched = [];
3115
3240
  for (const [stack, files] of Object.entries(SIGNATURES)) {
3116
- if (files.some((f) => existsSync7(join18(folderPath, f)))) {
3241
+ if (files.some((f) => existsSync7(join19(folderPath, f)))) {
3117
3242
  matched.push(stack);
3118
3243
  }
3119
3244
  }
@@ -3122,25 +3247,25 @@ function detectFolderTechStack(folderPath) {
3122
3247
 
3123
3248
  // src/lib/gitignore-template-loader.ts
3124
3249
  import { readFileSync as readFileSync3 } from "fs";
3125
- import { dirname as dirname4, join as join19 } from "path";
3250
+ import { dirname as dirname4, join as join20 } from "path";
3126
3251
  import { fileURLToPath as fileURLToPath2 } from "url";
3127
3252
  var __dirname = dirname4(fileURLToPath2(import.meta.url));
3128
3253
  var CANDIDATE_DIRS = [
3129
3254
  // Bundled production: dist/index.js → __dirname = .../dist/, sibling dist/templates
3130
- join19(__dirname, "templates", "gitignore"),
3255
+ join20(__dirname, "templates", "gitignore"),
3131
3256
  // Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
3132
- join19(__dirname, "..", "templates", "gitignore"),
3257
+ join20(__dirname, "..", "templates", "gitignore"),
3133
3258
  // Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
3134
- join19(__dirname, "..", "..", "src", "templates", "gitignore"),
3259
+ join20(__dirname, "..", "..", "src", "templates", "gitignore"),
3135
3260
  // npm-installed alt: __dirname = .../dist/ → package_root/src/templates
3136
- join19(__dirname, "..", "src", "templates", "gitignore")
3261
+ join20(__dirname, "..", "src", "templates", "gitignore")
3137
3262
  ];
3138
3263
  var AVATAR_MARKER_START = "# === avatar ===";
3139
3264
  var AVATAR_MARKER_END = "# === /avatar ===";
3140
3265
  function readTemplate(stack) {
3141
3266
  for (const dir of CANDIDATE_DIRS) {
3142
3267
  try {
3143
- return readFileSync3(join19(dir, `${stack}.txt`), "utf8");
3268
+ return readFileSync3(join20(dir, `${stack}.txt`), "utf8");
3144
3269
  } catch {
3145
3270
  }
3146
3271
  }
@@ -3155,9 +3280,9 @@ ${readTemplate(s).trim()}`);
3155
3280
 
3156
3281
  // src/lib/write-or-merge-gitignore.ts
3157
3282
  import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
3158
- import { join as join20 } from "path";
3283
+ import { join as join21 } from "path";
3159
3284
  function writeOrMergeGitignore(folderPath, avatarBlock) {
3160
- const path = join20(folderPath, ".gitignore");
3285
+ const path = join21(folderPath, ".gitignore");
3161
3286
  if (!existsSync8(path)) {
3162
3287
  writeFileSync(path, avatarBlock, "utf8");
3163
3288
  return;
@@ -3332,9 +3457,75 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
3332
3457
  await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
3333
3458
  }
3334
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
+
3335
3526
  // src/commands/init-conflict-detection-helpers.ts
3336
3527
  import { readdir } from "fs/promises";
3337
- import { join as join21 } from "path";
3528
+ import { join as join23 } from "path";
3338
3529
  async function isEmptyOrMissing(path) {
3339
3530
  if (!await pathExists(path)) return true;
3340
3531
  try {
@@ -3347,7 +3538,7 @@ async function isEmptyOrMissing(path) {
3347
3538
  }
3348
3539
  async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
3349
3540
  for (let i = 2; i < maxAttempts; i++) {
3350
- const candidate = join21(parent, `${desiredName}-${i}`);
3541
+ const candidate = join23(parent, `${desiredName}-${i}`);
3351
3542
  if (await isEmptyOrMissing(candidate)) return candidate;
3352
3543
  }
3353
3544
  return null;
@@ -3777,7 +3968,7 @@ async function runInitFromScratch(opts, ownerEmail) {
3777
3968
  const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
3778
3969
  const workspaceParent = resolve(opts.workspaceParent ?? ".");
3779
3970
  const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
3780
- const srcPath = join22(workspacePath, "src");
3971
+ const srcPath = join24(workspacePath, "src");
3781
3972
  await ensureDir(workspacePath);
3782
3973
  await ensureDir(srcPath);
3783
3974
  await safeBootstrapGitInFolder(srcPath, { autoYes: true });
@@ -3915,11 +4106,12 @@ async function finalizeWorkspaceScaffold(args) {
3915
4106
  await writeRootClaudeMd(args.workspacePath, vars);
3916
4107
  await writeProjectSettings(args.workspacePath, vars);
3917
4108
  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");
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");
3922
4113
  log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
4114
+ await autoSyncPackOnInit(args.workspacePath);
3923
4115
  await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
3924
4116
  await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
3925
4117
  await maybeCreateWorkspaceRemote(args);
@@ -3954,6 +4146,39 @@ async function finalizeWorkspaceScaffold(args) {
3954
4146
  }
3955
4147
  printInitSuccessBox(args.workspacePath, args.flow, aiResult, gitnexusResult);
3956
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
+ }
3957
4182
  async function maybeCreateWorkspaceRemote(args) {
3958
4183
  if (args.skipCommit) {
3959
4184
  log.dim("Skip workspace remote (ch\u01B0a commit). Setup sau qua: gh repo create ...");
@@ -4041,7 +4266,7 @@ async function maybeCreateWorkspaceRemote(args) {
4041
4266
  }
4042
4267
  }
4043
4268
  async function resolveWorkspacePath(parent, desiredName, force) {
4044
- const desired = join22(parent, desiredName);
4269
+ const desired = join24(parent, desiredName);
4045
4270
  if (await isEmptyOrMissing(desired)) return desired;
4046
4271
  log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
4047
4272
  while (true) {
@@ -4072,7 +4297,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
4072
4297
  message: "T\xEAn workspace m\u1EDBi:",
4073
4298
  validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
4074
4299
  });
4075
- const newPath = join22(parent, newName.trim());
4300
+ const newPath = join24(parent, newName.trim());
4076
4301
  if (await isEmptyOrMissing(newPath)) return newPath;
4077
4302
  log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
4078
4303
  }
@@ -4115,7 +4340,7 @@ function formatGitnexusStatusLine(result) {
4115
4340
  }
4116
4341
  function printInitSuccessBox(rootPath, flow, aiResult = null, gitnexusResult = null) {
4117
4342
  const lines = [
4118
- `${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}`,
4119
4344
  ` ${chalk.dim(`(flow: ${flow})`)}`,
4120
4345
  formatAiStatusLine(aiResult),
4121
4346
  formatGitnexusStatusLine(gitnexusResult),
@@ -4178,18 +4403,18 @@ function registerSecretsCommand(program2) {
4178
4403
  }
4179
4404
 
4180
4405
  // src/commands/status.ts
4181
- import { promises as fs10 } from "fs";
4182
- import { join as join24 } from "path";
4406
+ import { promises as fs13 } from "fs";
4407
+ import { join as join26 } from "path";
4183
4408
  import boxen6 from "boxen";
4184
4409
 
4185
4410
  // src/lib/pack-backup-manager.ts
4186
- import { promises as fs9 } from "fs";
4187
- import { join as join23 } from "path";
4411
+ import { promises as fs12 } from "fs";
4412
+ import { join as join25 } from "path";
4188
4413
  var BACKUP_DIR_NAME = "_backup";
4189
4414
  async function listBackups(projectRoot) {
4190
- const dir = join23(projectRoot, ".claude", BACKUP_DIR_NAME);
4415
+ const dir = join25(projectRoot, ".claude", BACKUP_DIR_NAME);
4191
4416
  if (!await pathExists(dir)) return [];
4192
- const entries = await fs9.readdir(dir, { withFileTypes: true });
4417
+ const entries = await fs12.readdir(dir, { withFileTypes: true });
4193
4418
  return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
4194
4419
  }
4195
4420
 
@@ -4213,7 +4438,7 @@ function registerStatusCommand(program2) {
4213
4438
  }
4214
4439
  async function gatherStatus(cwd) {
4215
4440
  const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
4216
- const claudeRoot = join24(cwd, ".claude");
4441
+ const claudeRoot = join26(cwd, ".claude");
4217
4442
  const hasAvatar = await pathExists(claudeRoot);
4218
4443
  if (!hasAvatar) {
4219
4444
  return {
@@ -4226,9 +4451,9 @@ async function gatherStatus(cwd) {
4226
4451
  hasAvatar: false
4227
4452
  };
4228
4453
  }
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;
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;
4232
4457
  const backupCount = (await listBackups(cwd)).length;
4233
4458
  const techStackSummary = await readTechStackFirstLine(claudeRoot);
4234
4459
  return {
@@ -4242,7 +4467,7 @@ async function gatherStatus(cwd) {
4242
4467
  };
4243
4468
  }
4244
4469
  async function readTechStackFirstLine(claudeRoot) {
4245
- const techStackPath = join24(claudeRoot, "project", "tech-stack.md");
4470
+ const techStackPath = join26(claudeRoot, "project", "tech-stack.md");
4246
4471
  if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
4247
4472
  const content = await readText(techStackPath);
4248
4473
  const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
@@ -4265,201 +4490,8 @@ function renderStatusBox(s) {
4265
4490
  // src/commands/sync.ts
4266
4491
  import { join as join28 } from "path";
4267
4492
 
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
4493
  // src/lib/preview-team-pack-sync-changes-for-dry-run.ts
4394
4494
  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
4495
  async function inspectMountDir(packDir, claudeDir, dir) {
4464
4496
  const source = join27(packDir, dir);
4465
4497
  const dest = join27(claudeDir, dir);
@@ -4774,7 +4806,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
4774
4806
  }
4775
4807
 
4776
4808
  // src/commands/uninstall.ts
4777
- var CLI_VERSION = "1.6.1";
4809
+ var CLI_VERSION = "1.6.2";
4778
4810
  function registerUninstallCommand(program2) {
4779
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) => {
4780
4812
  try {
@@ -4856,7 +4888,7 @@ function printUninstallSuccessBox(backupPath) {
4856
4888
  }
4857
4889
 
4858
4890
  // src/index.ts
4859
- var CLI_VERSION2 = "1.6.1";
4891
+ var CLI_VERSION2 = "1.6.2";
4860
4892
  var program = new Command();
4861
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(
4862
4894
  "beforeAll",