@nalvietnam/avatar-cli 1.4.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -103,11 +103,22 @@ var userStateSchema = z.object({
103
103
  tool_inputs: z.record(z.string(), z.unknown()).default({})
104
104
  });
105
105
  var projectSettingsSchema = z.object({
106
- allowedTools: z.array(z.string()),
107
- hooks: z.object({
108
- PostToolUse: z.array(z.unknown()).optional()
106
+ $schema: z.string().optional(),
107
+ includeCoAuthoredBy: z.boolean().optional(),
108
+ env: z.record(z.string(), z.string()).default({}),
109
+ permissions: z.object({
110
+ allow: z.array(z.string()).default([]),
111
+ deny: z.array(z.string()).default([])
109
112
  }).partial().optional(),
110
- env: z.record(z.string(), z.string()).default({})
113
+ // Hooks shape per Claude Code spec: each event key maps to array of matcher entries.
114
+ // We accept unknown body since pack/template control concrete schema; Zod just guards
115
+ // top-level structure to avoid corrupting user file on merge.
116
+ hooks: z.record(z.string(), z.array(z.unknown())).optional(),
117
+ statusLine: z.object({
118
+ type: z.string(),
119
+ command: z.string(),
120
+ padding: z.number().optional()
121
+ }).optional()
111
122
  });
112
123
  var initModeSchema = z.enum(["internal", "client", "library"]);
113
124
 
@@ -134,8 +145,8 @@ async function writeUserConfig(config) {
134
145
  }
135
146
  async function clearUserConfig() {
136
147
  if (await pathExists(USER_CONFIG_PATH)) {
137
- const { promises: fs11 } = await import("fs");
138
- await fs11.unlink(USER_CONFIG_PATH);
148
+ const { promises: fs14 } = await import("fs");
149
+ await fs14.unlink(USER_CONFIG_PATH);
139
150
  }
140
151
  }
141
152
  function isTokenExpired(config) {
@@ -462,27 +473,126 @@ async function promptAiProviderChoice(globalInfo = detectGlobalClaudeSettings())
462
473
  message: "Ch\u1ECDn provider cho AI features:",
463
474
  choices: [
464
475
  {
465
- name: "1. Claude Code Subscription (d\xF9ng quota c\xE1 nh\xE2n Anthropic)",
476
+ name: "1. Claude Code Subscription (d\xF9ng quota c\xE1 nh\xE2n Anthropic, OAuth login)",
466
477
  value: "subscription"
467
478
  },
468
479
  {
469
- name: "2. LLMLite API key (llm.nal.vn \u2014 NAL c\u1EA5p)",
480
+ name: "2. LLMLite API key (llm.nal.vn \u2014 NAL gateway, key sk-...)",
470
481
  value: "llmlite"
482
+ },
483
+ {
484
+ name: "3. Anthropic API key tr\u1EF1c ti\u1EBFp (console.anthropic.com, key sk-ant-...)",
485
+ value: "anthropic"
471
486
  }
472
487
  ]
473
488
  });
474
489
  }
475
490
 
491
+ // src/lib/setup-anthropic-api-key-and-model.ts
492
+ import { password, select as select2 } from "@inquirer/prompts";
493
+ var ANTHROPIC_BASE_URL = "https://api.anthropic.com";
494
+ var ANTHROPIC_API_VERSION = "2023-06-01";
495
+ var FETCH_TIMEOUT_MS = 1e4;
496
+ function maskAnthropicKey(key) {
497
+ if (key.length <= 12) return "sk-ant-***";
498
+ return `${key.slice(0, 7)}...${key.slice(-4)}`;
499
+ }
500
+ function validateAnthropicKeyFormat(key) {
501
+ const trimmed = key.trim();
502
+ if (trimmed.length === 0) return "API key b\u1EAFt bu\u1ED9c";
503
+ if (!trimmed.startsWith("sk-ant-")) {
504
+ return "Anthropic API key th\u01B0\u1EDDng b\u1EAFt \u0111\u1EA7u b\u1EB1ng 'sk-ant-' (l\u1EA5y t\u1EEB console.anthropic.com).";
505
+ }
506
+ return true;
507
+ }
508
+ async function promptAnthropicKeyHidden() {
509
+ return await password({
510
+ message: "Anthropic API key (sk-ant-..., \u1EA9n input):",
511
+ mask: "*",
512
+ validate: validateAnthropicKeyFormat
513
+ });
514
+ }
515
+ async function fetchAnthropicModels(apiKey) {
516
+ const controller = new AbortController();
517
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
518
+ try {
519
+ const res = await fetch(`${ANTHROPIC_BASE_URL}/v1/models`, {
520
+ method: "GET",
521
+ headers: {
522
+ "x-api-key": apiKey,
523
+ "anthropic-version": ANTHROPIC_API_VERSION,
524
+ Accept: "application/json"
525
+ },
526
+ signal: controller.signal
527
+ });
528
+ if (res.status === 401) {
529
+ throw new Error("API key invalid (HTTP 401). Check key tr\xEAn console.anthropic.com.");
530
+ }
531
+ if (res.status === 403) {
532
+ throw new Error(
533
+ "API key b\u1ECB reject (HTTP 403). Key c\xF3 th\u1EC3 \u0111\xE3 b\u1ECB revoke ho\u1EB7c thi\u1EBFu permission."
534
+ );
535
+ }
536
+ if (res.status === 429) {
537
+ throw new Error("Rate limit (HTTP 429). Ch\u1EDD v\xE0i gi\xE2y r\u1ED3i th\u1EED l\u1EA1i.");
538
+ }
539
+ if (!res.ok) {
540
+ throw new Error(`Fetch models th\u1EA5t b\u1EA1i (HTTP ${res.status}).`);
541
+ }
542
+ const json = await res.json();
543
+ const models = (json.data || []).map((m) => typeof m.id === "string" ? m.id : null).filter((id) => id !== null);
544
+ if (models.length === 0) {
545
+ throw new Error("Anthropic tr\u1EA3 v\u1EC1 list r\u1ED7ng. Li\xEAn h\u1EC7 support ho\u1EB7c check account.");
546
+ }
547
+ return models;
548
+ } catch (err) {
549
+ if (err.name === "AbortError") {
550
+ throw new Error(`Connect ${ANTHROPIC_BASE_URL} timeout sau ${FETCH_TIMEOUT_MS / 1e3}s.`);
551
+ }
552
+ throw err;
553
+ } finally {
554
+ clearTimeout(timer);
555
+ }
556
+ }
557
+ async function promptAnthropicModelChoice(models) {
558
+ if (models.length === 1) {
559
+ log.info(`Auto-pick model: ${models[0]} (ch\u1EC9 1 model available)`);
560
+ return models[0];
561
+ }
562
+ const sorted = [...models].sort((a, b) => {
563
+ const score = (m) => {
564
+ const lower = m.toLowerCase();
565
+ if (lower.includes("sonnet")) return 0;
566
+ if (lower.includes("opus")) return 1;
567
+ if (lower.includes("haiku")) return 2;
568
+ return 3;
569
+ };
570
+ return score(a) - score(b);
571
+ });
572
+ return await select2({
573
+ message: "Ch\u1ECDn model m\u1EB7c \u0111\u1ECBnh cho project:",
574
+ choices: sorted.map((m) => ({ name: m, value: m }))
575
+ });
576
+ }
577
+ async function setupAnthropicApiKeyAndModel() {
578
+ const apiKey = await promptAnthropicKeyHidden();
579
+ log.info(`Verify key (${maskAnthropicKey(apiKey)}) qua ${ANTHROPIC_BASE_URL}/v1/models...`);
580
+ const models = await fetchAnthropicModels(apiKey);
581
+ log.success(`Endpoint OK \u2014 ${models.length} models available`);
582
+ const model = await promptAnthropicModelChoice(models);
583
+ return { apiKey, baseUrl: ANTHROPIC_BASE_URL, model };
584
+ }
585
+
476
586
  // src/lib/setup-llmlite-api-key-and-model.ts
477
- import { input, password, select as select2 } from "@inquirer/prompts";
587
+ import { input, password as password2, select as select3 } from "@inquirer/prompts";
478
588
  var DEFAULT_BASE_URL = "https://llm.nal.vn";
479
- var FETCH_TIMEOUT_MS = 1e4;
589
+ var FETCH_TIMEOUT_MS2 = 1e4;
480
590
  function maskApiKey(key) {
481
591
  if (key.length <= 8) return "sk-***";
482
592
  return `${key.slice(0, 3)}...${key.slice(-4)}`;
483
593
  }
484
594
  async function promptApiKeyHidden() {
485
- return await password({
595
+ return await password2({
486
596
  message: "LLMLite API key (\u1EA9n input):",
487
597
  mask: "*",
488
598
  validate: (v) => v.trim().length > 0 ? true : "API key b\u1EAFt bu\u1ED9c"
@@ -498,7 +608,7 @@ async function promptBaseUrl(defaultUrl = DEFAULT_BASE_URL) {
498
608
  }
499
609
  async function fetchAvailableModels(baseUrl, apiKey) {
500
610
  const controller = new AbortController();
501
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
611
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
502
612
  try {
503
613
  const res = await fetch(`${baseUrl}/v1/models`, {
504
614
  method: "GET",
@@ -525,7 +635,7 @@ async function fetchAvailableModels(baseUrl, apiKey) {
525
635
  return models;
526
636
  } catch (err) {
527
637
  if (err.name === "AbortError") {
528
- throw new Error(`Connect ${baseUrl} timeout sau ${FETCH_TIMEOUT_MS / 1e3}s.`);
638
+ throw new Error(`Connect ${baseUrl} timeout sau ${FETCH_TIMEOUT_MS2 / 1e3}s.`);
529
639
  }
530
640
  throw err;
531
641
  } finally {
@@ -539,7 +649,7 @@ async function promptModelChoice(models) {
539
649
  return claudeAliases[0];
540
650
  }
541
651
  const choiceList = claudeAliases.length > 0 ? claudeAliases : models;
542
- return await select2({
652
+ return await select3({
543
653
  message: "Ch\u1ECDn model m\u1EB7c \u0111\u1ECBnh cho project:",
544
654
  choices: choiceList.map((m) => ({ name: m, value: m }))
545
655
  });
@@ -575,7 +685,12 @@ function applySubscription(existing, model) {
575
685
  const { env: existingEnv, ...rest } = existing;
576
686
  const merged = { ...rest, model };
577
687
  if (existingEnv) {
578
- const { ANTHROPIC_BASE_URL: _b, ANTHROPIC_AUTH_TOKEN: _t, ...envRest } = existingEnv;
688
+ const {
689
+ ANTHROPIC_BASE_URL: _b,
690
+ ANTHROPIC_AUTH_TOKEN: _t,
691
+ ANTHROPIC_API_KEY: _k,
692
+ ...envRest
693
+ } = existingEnv;
579
694
  if (Object.keys(envRest).length > 0) {
580
695
  merged.env = envRest;
581
696
  }
@@ -583,16 +698,29 @@ function applySubscription(existing, model) {
583
698
  return merged;
584
699
  }
585
700
  function applyLLMLite(existing, apiKey, baseUrl, model) {
701
+ const { ANTHROPIC_API_KEY: _k, ...envRest } = existing.env || {};
586
702
  return {
587
703
  ...existing,
588
704
  env: {
589
- ...existing.env || {},
705
+ ...envRest,
590
706
  ANTHROPIC_BASE_URL: baseUrl,
591
707
  ANTHROPIC_AUTH_TOKEN: apiKey
592
708
  },
593
709
  model
594
710
  };
595
711
  }
712
+ function applyAnthropic(existing, apiKey, baseUrl, model) {
713
+ const { ANTHROPIC_AUTH_TOKEN: _t, ...envRest } = existing.env || {};
714
+ return {
715
+ ...existing,
716
+ env: {
717
+ ...envRest,
718
+ ANTHROPIC_BASE_URL: baseUrl,
719
+ ANTHROPIC_API_KEY: apiKey
720
+ },
721
+ model
722
+ };
723
+ }
596
724
  function applyUseGlobal(existing, source) {
597
725
  const sourceEnv = source.env || {};
598
726
  const sourceModel = typeof source.model === "string" ? source.model : void 0;
@@ -616,6 +744,9 @@ async function writeClaudeSettings(workspacePath, input6) {
616
744
  case "llmlite":
617
745
  merged = applyLLMLite(existing, input6.apiKey, input6.baseUrl, input6.model);
618
746
  break;
747
+ case "anthropic":
748
+ merged = applyAnthropic(existing, input6.apiKey, input6.baseUrl, input6.model);
749
+ break;
619
750
  case "use-global":
620
751
  merged = applyUseGlobal(existing, input6.sourceSettings);
621
752
  break;
@@ -690,6 +821,23 @@ async function runAiSetupPhase(args) {
690
821
  log.success(`AI ready \xB7 LLMLite \xB7 model=${llmConfig.model} \xB7 ${llmConfig.baseUrl}`);
691
822
  return { ok: true, provider: "llmlite", model: llmConfig.model };
692
823
  }
824
+ case "anthropic": {
825
+ const anthropicConfig = await setupAnthropicApiKeyAndModel();
826
+ await writeClaudeSettings(args.workspacePath, {
827
+ provider: "anthropic",
828
+ apiKey: anthropicConfig.apiKey,
829
+ baseUrl: anthropicConfig.baseUrl,
830
+ model: anthropicConfig.model
831
+ });
832
+ await appendAuditEntry(
833
+ "ai_setup",
834
+ `provider=anthropic,result=ok,model=${anthropicConfig.model}`
835
+ );
836
+ log.success(
837
+ `AI ready \xB7 Anthropic Direct \xB7 model=${anthropicConfig.model} \xB7 ${anthropicConfig.baseUrl}`
838
+ );
839
+ return { ok: true, provider: "anthropic", model: anthropicConfig.model };
840
+ }
693
841
  case "use-global": {
694
842
  if (!globalInfo.rawSettings) {
695
843
  throw new Error("use-global ch\u1ECDn nh\u01B0ng kh\xF4ng \u0111\u1ECDc \u0111\u01B0\u1EE3c global settings.");
@@ -714,14 +862,15 @@ async function runAiSetupPhase(args) {
714
862
 
715
863
  // src/lib/test-ai-provider-by-detected-mode.ts
716
864
  import { spawnSync as spawnSync4 } from "child_process";
717
- var FETCH_TIMEOUT_MS2 = 1e4;
865
+ var FETCH_TIMEOUT_MS3 = 1e4;
718
866
  var CLAUDE_PRINT_TIMEOUT_MS = 3e4;
719
867
  var TEST_CHAT_MAX_TOKENS = 5;
720
868
  var TEST_CHAT_PROMPT = "say ok";
869
+ var ANTHROPIC_API_VERSION2 = "2023-06-01";
721
870
  async function testLLMLiteProvider(baseUrl, token, model) {
722
871
  log.info(`Testing LLMLite provider: ${baseUrl} (key: ${maskApiKey(token)})`);
723
872
  const controller = new AbortController();
724
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
873
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS3);
725
874
  try {
726
875
  const modelsRes = await fetch(`${baseUrl}/v1/models`, {
727
876
  headers: { Authorization: `Bearer ${token}` },
@@ -766,7 +915,7 @@ async function testLLMLiteProvider(baseUrl, token, model) {
766
915
  log.dim(` Tokens used: ${tokens}`);
767
916
  } catch (err) {
768
917
  if (err.name === "AbortError") {
769
- throw new Error(`Timeout ${FETCH_TIMEOUT_MS2 / 1e3}s. Check m\u1EA1ng / endpoint ${baseUrl}.`);
918
+ throw new Error(`Timeout ${FETCH_TIMEOUT_MS3 / 1e3}s. Check m\u1EA1ng / endpoint ${baseUrl}.`);
770
919
  }
771
920
  throw err;
772
921
  } finally {
@@ -795,11 +944,63 @@ function testSubscriptionProvider() {
795
944
  }
796
945
  log.success(`Response: "${(result.stdout || "").trim().slice(0, 100)}"`);
797
946
  }
947
+ async function testAnthropicProvider(baseUrl, apiKey, model) {
948
+ log.info(`Testing Anthropic Direct provider: ${baseUrl} (key: ${maskApiKey(apiKey)})`);
949
+ const controller = new AbortController();
950
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS3);
951
+ try {
952
+ const modelsRes = await fetch(`${baseUrl}/v1/models`, {
953
+ headers: {
954
+ "x-api-key": apiKey,
955
+ "anthropic-version": ANTHROPIC_API_VERSION2
956
+ },
957
+ signal: controller.signal
958
+ });
959
+ if (modelsRes.status === 401 || modelsRes.status === 403) {
960
+ throw new Error(`API key invalid (HTTP ${modelsRes.status}). Re-run: avatar ai setup`);
961
+ }
962
+ if (!modelsRes.ok) {
963
+ throw new Error(`Endpoint /v1/models l\u1ED7i (HTTP ${modelsRes.status}).`);
964
+ }
965
+ const modelsJson = await modelsRes.json();
966
+ const models = (modelsJson.data || []).map((m) => typeof m.id === "string" ? m.id : null).filter((id) => id !== null);
967
+ log.success(`Connectivity OK \xB7 ${models.length} models available`);
968
+ log.info(`Testing message v\u1EDBi model "${model}"...`);
969
+ const msgRes = await fetch(`${baseUrl}/v1/messages`, {
970
+ method: "POST",
971
+ headers: {
972
+ "x-api-key": apiKey,
973
+ "anthropic-version": ANTHROPIC_API_VERSION2,
974
+ "Content-Type": "application/json"
975
+ },
976
+ body: JSON.stringify({
977
+ model,
978
+ max_tokens: TEST_CHAT_MAX_TOKENS,
979
+ messages: [{ role: "user", content: TEST_CHAT_PROMPT }]
980
+ }),
981
+ signal: controller.signal
982
+ });
983
+ if (!msgRes.ok) {
984
+ const errText = (await msgRes.text()).slice(0, 200);
985
+ throw new Error(`Message endpoint fail (HTTP ${msgRes.status}): ${errText}`);
986
+ }
987
+ const msgJson = await msgRes.json();
988
+ const text = (msgJson.content || []).map((c) => typeof c.text === "string" ? c.text : "").join("").trim().slice(0, 100);
989
+ log.success(`Response: "${text}"`);
990
+ } finally {
991
+ clearTimeout(timer);
992
+ }
993
+ }
798
994
  async function testAiProviderByDetectedMode(settings) {
799
995
  const env = settings.env || {};
800
996
  const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
801
997
  const token = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : void 0;
998
+ const apiKey = typeof env.ANTHROPIC_API_KEY === "string" ? env.ANTHROPIC_API_KEY : void 0;
802
999
  const model = typeof settings.model === "string" ? settings.model : "default";
1000
+ if (apiKey && baseUrl) {
1001
+ await testAnthropicProvider(baseUrl, apiKey, model);
1002
+ return { ok: true, provider: "anthropic", message: "Anthropic Direct provider working" };
1003
+ }
803
1004
  if (baseUrl && token) {
804
1005
  await testLLMLiteProvider(baseUrl, token, model);
805
1006
  return { ok: true, provider: "llmlite", message: "LLMLite provider working" };
@@ -845,12 +1046,27 @@ async function runAiStatus() {
845
1046
  const env = settings.env || {};
846
1047
  const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
847
1048
  const token = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : void 0;
1049
+ const apiKey = typeof env.ANTHROPIC_API_KEY === "string" ? env.ANTHROPIC_API_KEY : void 0;
848
1050
  const model = typeof settings.model === "string" ? settings.model : void 0;
849
- const provider = baseUrl ? "LLMLite" : token ? "Custom" : "Subscription (default)";
1051
+ let provider;
1052
+ let credentialDisplay;
1053
+ if (apiKey) {
1054
+ provider = baseUrl?.includes("api.anthropic.com") || apiKey.startsWith("sk-ant-") ? "Anthropic Direct" : "Custom (API key)";
1055
+ credentialDisplay = maskApiKey(apiKey);
1056
+ } else if (baseUrl && token) {
1057
+ provider = "LLMLite";
1058
+ credentialDisplay = maskApiKey(token);
1059
+ } else if (token) {
1060
+ provider = "Custom";
1061
+ credentialDisplay = maskApiKey(token);
1062
+ } else {
1063
+ provider = "Subscription (default)";
1064
+ credentialDisplay = "(kh\xF4ng set \u2014 d\xF9ng subscription auth)";
1065
+ }
850
1066
  log.info(`Project: ${workspacePath}`);
851
1067
  log.info(`Provider: ${provider}${baseUrl ? ` (${baseUrl})` : ""}`);
852
1068
  log.info(`Model: ${model ?? "(default \u2014 Claude Code ch\u1ECDn)"}`);
853
- log.info(`Token: ${token ? maskApiKey(token) : "(kh\xF4ng set \u2014 d\xF9ng subscription auth)"}`);
1069
+ log.info(`Token: ${credentialDisplay}`);
854
1070
  }
855
1071
  async function runAiTest() {
856
1072
  const workspacePath = await ensureWorkspaceCwd();
@@ -880,7 +1096,12 @@ async function runAiReset(opts) {
880
1096
  const { env: existingEnv, ...rest } = settings;
881
1097
  const reset = { ...rest };
882
1098
  if (existingEnv) {
883
- const { ANTHROPIC_BASE_URL: _b, ANTHROPIC_AUTH_TOKEN: _t, ...envRest } = existingEnv;
1099
+ const {
1100
+ ANTHROPIC_BASE_URL: _b,
1101
+ ANTHROPIC_AUTH_TOKEN: _t,
1102
+ ANTHROPIC_API_KEY: _k,
1103
+ ...envRest
1104
+ } = existingEnv;
884
1105
  if (Object.keys(envRest).length > 0) {
885
1106
  reset.env = envRest;
886
1107
  }
@@ -1538,7 +1759,7 @@ function installGitnexusViaNpm() {
1538
1759
  }
1539
1760
 
1540
1761
  // src/lib/prompt-recovery-action-on-failure.ts
1541
- import { input as input3, select as select3 } from "@inquirer/prompts";
1762
+ import { input as input3, select as select4 } from "@inquirer/prompts";
1542
1763
  var UserAbortedRecoveryError = class extends Error {
1543
1764
  constructor(message) {
1544
1765
  super(message);
@@ -1555,7 +1776,7 @@ async function promptRetryOrSkip(args) {
1555
1776
  choices.push({ name: "B\u1ECF qua b\u01B0\u1EDBc n\xE0y v\xE0 ti\u1EBFp t\u1EE5c (Skip)", value: "skip" });
1556
1777
  }
1557
1778
  choices.push({ name: "T\u1EA1m ng\u01B0ng init \u2014 ch\u1EA1y l\u1EA1i sau (Abort)", value: "abort" });
1558
- return await select3({
1779
+ return await select4({
1559
1780
  message: "C\xE1ch x\u1EED l\xFD?",
1560
1781
  choices
1561
1782
  });
@@ -1949,19 +2170,19 @@ function registerGitnexusCommand(program2) {
1949
2170
 
1950
2171
  // src/commands/init.ts
1951
2172
  import { basename, join as join22, relative as relative2, resolve } from "path";
1952
- import { confirm as confirm5, input as input5, select as select8 } from "@inquirer/prompts";
2173
+ import { confirm as confirm5, input as input5, select as select9 } from "@inquirer/prompts";
1953
2174
  import boxen5 from "boxen";
1954
2175
 
1955
2176
  // src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
1956
2177
  import { spawnSync as spawnSync13 } from "child_process";
1957
- import { select as select5 } from "@inquirer/prompts";
2178
+ import { select as select6 } from "@inquirer/prompts";
1958
2179
 
1959
2180
  // src/lib/team-pack-submodule-manager.ts
1960
2181
  import { join as join16 } from "path";
1961
2182
 
1962
2183
  // src/lib/check-team-pack-access-with-retry-loop.ts
1963
2184
  import { spawnSync as spawnSync12 } from "child_process";
1964
- import { confirm as confirm4, select as select4 } from "@inquirer/prompts";
2185
+ import { confirm as confirm4, select as select5 } from "@inquirer/prompts";
1965
2186
  import boxen3 from "boxen";
1966
2187
  function parseRepoSlugFromGitUrl(url) {
1967
2188
  const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
@@ -2040,7 +2261,7 @@ async function ensureTeamPackAccessWithRetry(args) {
2040
2261
  while (true) {
2041
2262
  const ghUser = getCurrentGhUser();
2042
2263
  const ghUserDisplay = ghUser ?? "(ch\u01B0a gh auth)";
2043
- const action = await select4({
2264
+ const action = await select5({
2044
2265
  message: "C\xE1ch x\u1EED l\xFD?",
2045
2266
  choices: [
2046
2267
  {
@@ -2157,7 +2378,7 @@ function openGithubSshKeysPage() {
2157
2378
  }
2158
2379
  }
2159
2380
  async function handleSshPermissionError() {
2160
- return await select5({
2381
+ return await select6({
2161
2382
  message: "SSH permission denied. C\xE1ch x\u1EED l\xFD?",
2162
2383
  choices: [
2163
2384
  {
@@ -2420,7 +2641,7 @@ function detectPackageManager() {
2420
2641
 
2421
2642
  // src/lib/handle-remote-access-failure-with-account-switch.ts
2422
2643
  import { spawnSync as spawnSync19 } from "child_process";
2423
- import { input as input4, select as select6 } from "@inquirer/prompts";
2644
+ import { input as input4, select as select7 } from "@inquirer/prompts";
2424
2645
 
2425
2646
  // src/lib/verify-git-remote-accessible.ts
2426
2647
  import { spawnSync as spawnSync18 } from "child_process";
@@ -2504,7 +2725,7 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
2504
2725
  log.dim(` L\xFD do: ${reason}${detail ? ` \u2014 ${detail.slice(0, 150)}` : ""}`);
2505
2726
  log.info(getReasonHint(reason, currentUrl, ghUser));
2506
2727
  if (ghUser) log.dim(` gh CLI hi\u1EC7n \u0111ang login: ${ghUser}`);
2507
- const action = await select6({
2728
+ const action = await select7({
2508
2729
  message: "C\xE1ch x\u1EED l\xFD?",
2509
2730
  choices: [
2510
2731
  {
@@ -2813,7 +3034,7 @@ function linkExistingRemoteToWorkspace(args) {
2813
3034
 
2814
3035
  // src/lib/safe-bootstrap-for-dirty-folder.ts
2815
3036
  import { readdirSync } from "fs";
2816
- import { select as select7 } from "@inquirer/prompts";
3037
+ import { select as select8 } from "@inquirer/prompts";
2817
3038
  import { simpleGit as simpleGit3 } from "simple-git";
2818
3039
 
2819
3040
  // src/lib/check-folder-has-git.ts
@@ -2950,7 +3171,7 @@ async function promptBootstrapStrategy(state, opts) {
2950
3171
  if (opts.presetStrategy) return opts.presetStrategy;
2951
3172
  if (opts.autoYes) return "stash";
2952
3173
  if (state === "empty" || state === "clean") return "commit-all";
2953
- return await select7({
3174
+ return await select8({
2954
3175
  message: state === "dirty" ? "Folder c\xF3 changes ch\u01B0a commit. C\xE1ch x\u1EED l\xFD:" : "Folder c\xF3 file ch\u01B0a version. C\xE1ch x\u1EED l\xFD:",
2955
3176
  choices: [
2956
3177
  {
@@ -3435,7 +3656,7 @@ async function runInit(opts) {
3435
3656
  }
3436
3657
  }
3437
3658
  async function promptProjectStatus() {
3438
- return await select8({
3659
+ return await select9({
3439
3660
  message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
3440
3661
  choices: [
3441
3662
  { name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
@@ -3518,7 +3739,7 @@ async function runInitFromScratch(opts, ownerEmail) {
3518
3739
  message: "T\xEAn d\u1EF1 \xE1n:",
3519
3740
  validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
3520
3741
  });
3521
- const visibility = opts.repoVisibility ?? await select8({
3742
+ const visibility = opts.repoVisibility ?? await select9({
3522
3743
  message: "Visibility?",
3523
3744
  choices: [
3524
3745
  { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
@@ -3593,7 +3814,7 @@ async function getOrCreateOriginRemote(folderPath, opts) {
3593
3814
  return void 0;
3594
3815
  }
3595
3816
  await ensureGitHubReady();
3596
- const visibility = opts.repoVisibility ?? await select8({
3817
+ const visibility = opts.repoVisibility ?? await select9({
3597
3818
  message: "Visibility?",
3598
3819
  choices: [
3599
3820
  { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
@@ -3719,7 +3940,7 @@ async function maybeCreateWorkspaceRemote(args) {
3719
3940
  });
3720
3941
  }
3721
3942
  if (!shouldCreate) return;
3722
- const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select8({
3943
+ const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select9({
3723
3944
  message: "Workspace visibility?",
3724
3945
  choices: [
3725
3946
  { name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
@@ -3738,7 +3959,7 @@ async function maybeCreateWorkspaceRemote(args) {
3738
3959
  } catch (err) {
3739
3960
  if (err instanceof CreateWorkspaceRemoteError && err.reason === "repo-exists") {
3740
3961
  const fullName = err.fullName;
3741
- const reuseAction = await select8({
3962
+ const reuseAction = await select9({
3742
3963
  message: `Repo '${fullName}' \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. C\xE1ch x\u1EED l\xFD?`,
3743
3964
  choices: [
3744
3965
  {
@@ -3807,7 +4028,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
3807
4028
  }
3808
4029
  choices.push({ name: "Nh\u1EADp t\xEAn workspace kh\xE1c (manual)", value: "manual" });
3809
4030
  choices.push({ name: "T\u1EA1m ng\u01B0ng init", value: "abort" });
3810
- const action = await select8({
4031
+ const action = await select9({
3811
4032
  message: "C\xE1ch x\u1EED l\xFD workspace path conflict?",
3812
4033
  choices
3813
4034
  });
@@ -4014,8 +4235,350 @@ function renderStatusBox(s) {
4014
4235
  }
4015
4236
 
4016
4237
  // src/commands/sync.ts
4238
+ import { join as join28 } from "path";
4239
+
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
+ // src/lib/preview-team-pack-sync-changes-for-dry-run.ts
4366
+ 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
+ async function inspectMountDir(packDir, claudeDir, dir) {
4436
+ const source = join27(packDir, dir);
4437
+ const dest = join27(claudeDir, dir);
4438
+ if (!await pathExists(source)) return "source-missing";
4439
+ if (!await pathExists(dest)) return "needs-creation";
4440
+ const { promises: fs14 } = await import("fs");
4441
+ const st = await fs14.lstat(dest);
4442
+ if (st.isSymbolicLink()) return "already-linked";
4443
+ return "conflict-real-dir";
4444
+ }
4445
+ async function listCommitsBetween(packDir, fromSha, toRef) {
4446
+ try {
4447
+ const result = await git(packDir).raw(["log", "--oneline", `${fromSha}..${toRef}`]);
4448
+ return result.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
4449
+ } catch {
4450
+ return [];
4451
+ }
4452
+ }
4453
+ async function buildSyncPreview(packDir, claudeDir, targetVersion) {
4454
+ const currentSha = await currentCommitSha(packDir);
4455
+ const currentVersion = currentSha.slice(0, 7);
4456
+ const target = targetVersion ?? await latestTag(packDir) ?? "HEAD";
4457
+ const commits = await listCommitsBetween(packDir, currentSha, target);
4458
+ const mountStatuses = [];
4459
+ for (const dir of TEAM_PACK_MOUNT_DIRS) {
4460
+ mountStatuses.push({
4461
+ dir,
4462
+ status: await inspectMountDir(packDir, claudeDir, dir)
4463
+ });
4464
+ }
4465
+ return {
4466
+ currentVersion,
4467
+ targetVersion: target,
4468
+ commitsBehind: commits,
4469
+ mountDirStatuses: mountStatuses
4470
+ };
4471
+ }
4472
+
4473
+ // src/commands/sync.ts
4474
+ async function syncAction(opts) {
4475
+ const projectRoot = process.cwd();
4476
+ const claudeDir = join28(projectRoot, ".claude");
4477
+ const packDir = join28(projectRoot, TEAM_PACK_RELATIVE_PATH);
4478
+ if (!await pathExists(packDir)) {
4479
+ log.error(
4480
+ `team-ai-pack submodule ch\u01B0a \u0111\u01B0\u1EE3c kh\u1EDFi t\u1EA1o \u1EDF ${TEAM_PACK_RELATIVE_PATH}/.
4481
+ Ch\u1EA1y 'avatar init' \u0111\u1EC3 add submodule, ho\u1EB7c 'git submodule update --init' n\u1EBFu \u0111\xE3 clone repo.`
4482
+ );
4483
+ process.exit(1);
4484
+ return;
4485
+ }
4486
+ try {
4487
+ await git(packDir).fetch(["--tags", "origin"]);
4488
+ } catch (err) {
4489
+ log.warn(
4490
+ `Kh\xF4ng fetch \u0111\u01B0\u1EE3c tags t\u1EEB origin (${err instanceof Error ? err.message : err}). S\u1EBD d\xF9ng tag local hi\u1EC7n c\xF3.`
4491
+ );
4492
+ }
4493
+ const targetVersion = opts.version ?? await latestTag(packDir);
4494
+ if (!targetVersion) {
4495
+ log.error(
4496
+ "Kh\xF4ng t\xECm th\u1EA5y tag n\xE0o trong team-ai-pack submodule. Pass --version <tag> r\xF5 r\xE0ng, ho\u1EB7c ki\u1EC3m tra repo c\xF3 tag \u0111\u01B0\u1EE3c kh\xF4ng."
4497
+ );
4498
+ process.exit(1);
4499
+ return;
4500
+ }
4501
+ if (opts.dryRun) {
4502
+ const preview = await buildSyncPreview(packDir, claudeDir, targetVersion);
4503
+ log.info(`Pack version hi\u1EC7n t\u1EA1i: ${preview.currentVersion}`);
4504
+ log.info(`Target version: ${preview.targetVersion}`);
4505
+ if (preview.commitsBehind.length === 0) {
4506
+ log.info("\u0110\xE3 \u1EDF target version, kh\xF4ng c\xF3 commit m\u1EDBi.");
4507
+ } else {
4508
+ log.info(`Commits c\u1EA7n pull (${preview.commitsBehind.length}):`);
4509
+ for (const c of preview.commitsBehind.slice(0, 20)) {
4510
+ console.log(` ${c}`);
4511
+ }
4512
+ if (preview.commitsBehind.length > 20) {
4513
+ console.log(` ... v\xE0 ${preview.commitsBehind.length - 20} commits kh\xE1c`);
4514
+ }
4515
+ }
4516
+ log.info("\nMount dir statuses:");
4517
+ for (const m of preview.mountDirStatuses) {
4518
+ console.log(` ${m.dir.padEnd(12)} ${m.status}`);
4519
+ }
4520
+ log.info("\nDry-run done. Kh\xF4ng apply thay \u0111\u1ED5i. B\u1ECF --dry-run \u0111\u1EC3 th\u1EF1c thi.");
4521
+ return;
4522
+ }
4523
+ log.info(`Checking out ${targetVersion} trong submodule...`);
4524
+ await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, targetVersion, projectRoot);
4525
+ log.info("Creating symlink farm...");
4526
+ const results = await syncAllMountDirs(packDir, claudeDir, opts.force === true);
4527
+ reportResults(results, opts.force === true);
4528
+ log.info("Merging pack settings.json template into project settings.json...");
4529
+ try {
4530
+ const mergeResult = await mergePackSettingsIntoProjectSettings(projectRoot);
4531
+ switch (mergeResult.action) {
4532
+ case "merged":
4533
+ log.success(
4534
+ ` \u2713 settings.json merged (${mergeResult.changes.join("; ")}). Backup: ${mergeResult.backupPath ?? "n/a"}`
4535
+ );
4536
+ break;
4537
+ case "no-change":
4538
+ log.info(" - settings.json \u0111\xE3 sync v\u1EDBi pack, kh\xF4ng c\xF3 thay \u0111\u1ED5i.");
4539
+ break;
4540
+ case "no-pack-template":
4541
+ log.dim(" - Pack kh\xF4ng c\xF3 templates/settings.json.tpl, skip merge.");
4542
+ break;
4543
+ }
4544
+ } catch (err) {
4545
+ log.warn(
4546
+ ` ! Merge settings.json fail: ${err instanceof Error ? err.message : err}. Symlinks \u0111\xE3 t\u1EA1o OK, hooks c\xF3 th\u1EC3 ch\u01B0a active. Manual merge n\u1EBFu c\u1EA7n.`
4547
+ );
4548
+ }
4549
+ log.success(`Synced team-ai-pack to ${targetVersion}.`);
4550
+ }
4551
+ function reportResults(results, force) {
4552
+ for (const r of results) {
4553
+ switch (r.action) {
4554
+ case "created":
4555
+ log.info(` \u2713 ${r.dir} \u2192 symlinked (new)`);
4556
+ break;
4557
+ case "updated":
4558
+ log.info(` \u2713 ${r.dir} \u2192 symlink refreshed`);
4559
+ break;
4560
+ case "backed-up-and-linked":
4561
+ log.info(` \u2713 ${r.dir} \u2192 symlinked (backup: ${r.backupPath})`);
4562
+ break;
4563
+ case "source-missing":
4564
+ log.warn(` - ${r.dir} \u2192 pack kh\xF4ng c\xF3 dir n\xE0y, skip`);
4565
+ break;
4566
+ case "skipped-conflict":
4567
+ log.warn(
4568
+ ` ! ${r.dir} \u2192 CONFLICT: existing real dir. D\xF9ng --force \u0111\u1EC3 backup + override (s\u1EBD gi\u1EEF data \u1EDF ${r.dir}.backup-<timestamp>/).`
4569
+ );
4570
+ break;
4571
+ }
4572
+ }
4573
+ const conflicts = results.filter((r) => r.action === "skipped-conflict").length;
4574
+ if (conflicts > 0 && !force) {
4575
+ log.warn(
4576
+ `${conflicts} mount dir(s) b\u1ECB skip do conflict. Ch\u1EA1y l\u1EA1i v\u1EDBi --force n\u1EBFu mu\u1ED1n override (backup t\u1EF1 \u0111\u1ED9ng).`
4577
+ );
4578
+ }
4579
+ }
4017
4580
  function registerSyncCommand(program2) {
4018
- program2.command("sync").description("Pull team-ai-pack m\u1EDBi nh\u1EA5t (M08)").option("--force", "Override .claude/pack/, backup tr\u01B0\u1EDBc").option("--version <tag>", "Pin v\xE0o version c\u1EE5 th\u1EC3").option("--dry-run", "Hi\u1EC3n th\u1ECB changes, kh\xF4ng apply").action(notImplementedYet("sync", "Milestone 08"));
4581
+ program2.command("sync").description("Pull team-ai-pack m\u1EDBi nh\u1EA5t + t\u1EA1o symlink farm v\xE0o .claude/").option("--force", "Override .claude/<dir>/ n\u1EBFu l\xE0 real dir (backup tr\u01B0\u1EDBc)").option("--version <tag>", "Pin v\xE0o version c\u1EE5 th\u1EC3 (vd: v0.2.0)").option("--dry-run", "Hi\u1EC3n th\u1ECB preview, kh\xF4ng apply thay \u0111\u1ED5i").action(syncAction);
4019
4582
  }
4020
4583
 
4021
4584
  // src/commands/tools.ts
@@ -4027,40 +4590,40 @@ function registerToolsCommand(program2) {
4027
4590
  }
4028
4591
 
4029
4592
  // src/commands/uninstall.ts
4030
- import { relative as relative3 } from "path";
4593
+ import { relative as relative4 } from "path";
4031
4594
  import { confirm as confirm6 } from "@inquirer/prompts";
4032
4595
  import boxen7 from "boxen";
4033
4596
 
4034
4597
  // src/lib/create-uninstall-backup-snapshot.ts
4035
4598
  import { cp, mkdir, writeFile } from "fs/promises";
4036
4599
  import { homedir as homedir4 } from "os";
4037
- import { basename as basename2, join as join25 } from "path";
4038
- var UNINSTALL_BACKUPS_DIR = join25(homedir4(), ".avatar", "uninstall-backups");
4600
+ import { basename as basename2, join as join29 } from "path";
4601
+ var UNINSTALL_BACKUPS_DIR = join29(homedir4(), ".avatar", "uninstall-backups");
4039
4602
  async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersion) {
4040
4603
  const projectName = basename2(projectRoot);
4041
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4042
- const backupDir = join25(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
4604
+ const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4605
+ const backupDir = join29(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp2}`);
4043
4606
  await mkdir(backupDir, { recursive: true, mode: 448 });
4044
4607
  if (artifacts.claudeDir) {
4045
- await cp(artifacts.claudeDir, join25(backupDir, ".claude"), { recursive: true });
4608
+ await cp(artifacts.claudeDir, join29(backupDir, ".claude"), { recursive: true });
4046
4609
  }
4047
4610
  if (artifacts.claudeMd) {
4048
- await cp(artifacts.claudeMd, join25(backupDir, "CLAUDE.md"));
4611
+ await cp(artifacts.claudeMd, join29(backupDir, "CLAUDE.md"));
4049
4612
  }
4050
4613
  if (artifacts.postMergeHook || artifacts.prePushHook) {
4051
- const hooksBackupDir = join25(backupDir, "hooks");
4614
+ const hooksBackupDir = join29(backupDir, "hooks");
4052
4615
  await mkdir(hooksBackupDir, { recursive: true });
4053
4616
  if (artifacts.postMergeHook) {
4054
- await cp(artifacts.postMergeHook, join25(hooksBackupDir, "post-merge"));
4617
+ await cp(artifacts.postMergeHook, join29(hooksBackupDir, "post-merge"));
4055
4618
  }
4056
4619
  if (artifacts.prePushHook) {
4057
- await cp(artifacts.prePushHook, join25(hooksBackupDir, "pre-push"));
4620
+ await cp(artifacts.prePushHook, join29(hooksBackupDir, "pre-push"));
4058
4621
  }
4059
4622
  }
4060
4623
  const manifest = {
4061
4624
  projectName,
4062
4625
  projectPath: projectRoot,
4063
- timestamp,
4626
+ timestamp: timestamp2,
4064
4627
  avatarVersion,
4065
4628
  artifacts: {
4066
4629
  claudeDir: !!artifacts.claudeDir,
@@ -4069,27 +4632,27 @@ async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersi
4069
4632
  prePushHook: !!artifacts.prePushHook
4070
4633
  }
4071
4634
  };
4072
- await writeFile(join25(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
4635
+ await writeFile(join29(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
4073
4636
  return backupDir;
4074
4637
  }
4075
4638
 
4076
4639
  // src/lib/detect-avatar-project-artifacts.ts
4077
4640
  import { existsSync as existsSync9 } from "fs";
4078
- import { join as join26 } from "path";
4641
+ import { join as join30 } from "path";
4079
4642
  function existsOrNull(path) {
4080
4643
  return existsSync9(path) ? path : null;
4081
4644
  }
4082
4645
  function detectAvatarProjectArtifacts(projectRoot) {
4083
- const claudeDir = existsOrNull(join26(projectRoot, ".claude"));
4084
- const claudeMd = existsOrNull(join26(projectRoot, "CLAUDE.md"));
4085
- const postMergeHook = existsOrNull(join26(projectRoot, ".git", "hooks", "post-merge"));
4646
+ const claudeDir = existsOrNull(join30(projectRoot, ".claude"));
4647
+ const claudeMd = existsOrNull(join30(projectRoot, "CLAUDE.md"));
4648
+ const postMergeHook = existsOrNull(join30(projectRoot, ".git", "hooks", "post-merge"));
4086
4649
  const prePushHook = existsOrNull(
4087
- join26(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
4650
+ join30(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
4088
4651
  );
4089
- const gitignorePath = existsOrNull(join26(projectRoot, ".gitignore"));
4090
- const gitmodulesPath = existsOrNull(join26(projectRoot, ".gitmodules"));
4091
- const notesDir = existsOrNull(join26(projectRoot, "notes"));
4092
- const scriptsDir = existsOrNull(join26(projectRoot, "scripts"));
4652
+ const gitignorePath = existsOrNull(join30(projectRoot, ".gitignore"));
4653
+ const gitmodulesPath = existsOrNull(join30(projectRoot, ".gitmodules"));
4654
+ const notesDir = existsOrNull(join30(projectRoot, "notes"));
4655
+ const scriptsDir = existsOrNull(join30(projectRoot, "scripts"));
4093
4656
  const hasAnyArtifact = !!(claudeDir || claudeMd || postMergeHook || prePushHook);
4094
4657
  return {
4095
4658
  hasAnyArtifact,
@@ -4110,11 +4673,11 @@ async function executeUninstallDeletion(artifacts, flags) {
4110
4673
  if (artifacts.claudeDir) {
4111
4674
  if (flags.keepSubmodule) {
4112
4675
  const { readdir: readdir2 } = await import("fs/promises");
4113
- const { join: join27 } = await import("path");
4676
+ const { join: join31 } = await import("path");
4114
4677
  const entries = await readdir2(artifacts.claudeDir);
4115
4678
  for (const entry of entries) {
4116
4679
  if (entry === "pack") continue;
4117
- await rm(join27(artifacts.claudeDir, entry), { recursive: true, force: true });
4680
+ await rm(join31(artifacts.claudeDir, entry), { recursive: true, force: true });
4118
4681
  }
4119
4682
  } else {
4120
4683
  await rm(artifacts.claudeDir, { recursive: true, force: true });
@@ -4183,7 +4746,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
4183
4746
  }
4184
4747
 
4185
4748
  // src/commands/uninstall.ts
4186
- var CLI_VERSION = "1.4.2";
4749
+ var CLI_VERSION = "1.6.0";
4187
4750
  function registerUninstallCommand(program2) {
4188
4751
  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) => {
4189
4752
  try {
@@ -4233,7 +4796,7 @@ function printUninstallSummary(projectRoot, artifacts, opts) {
4233
4796
  log.plain("");
4234
4797
  log.plain("C\xE1c artifact s\u1EBD g\u1EE1:");
4235
4798
  if (artifacts.claudeDir)
4236
- log.plain(` ${chalk.red("\u2717")} ${relative3(projectRoot, artifacts.claudeDir) || ".claude/"}`);
4799
+ log.plain(` ${chalk.red("\u2717")} ${relative4(projectRoot, artifacts.claudeDir) || ".claude/"}`);
4237
4800
  if (artifacts.claudeMd) log.plain(` ${chalk.red("\u2717")} CLAUDE.md`);
4238
4801
  if (artifacts.postMergeHook && !opts.keepHooks) {
4239
4802
  log.plain(` ${chalk.red("\u2717")} .git/hooks/post-merge`);
@@ -4265,7 +4828,7 @@ function printUninstallSuccessBox(backupPath) {
4265
4828
  }
4266
4829
 
4267
4830
  // src/index.ts
4268
- var CLI_VERSION2 = "1.4.2";
4831
+ var CLI_VERSION2 = "1.6.0";
4269
4832
  var program = new Command();
4270
4833
  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(
4271
4834
  "beforeAll",