@nalvietnam/avatar-cli 1.6.4 → 1.7.1

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
@@ -761,6 +761,14 @@ async function writeClaudeSettings(workspacePath, input6) {
761
761
 
762
762
  // src/lib/run-ai-setup-phase.ts
763
763
  var SUBSCRIPTION_DEFAULT_MODEL = "sonnet";
764
+ function warnAboutPlaintextSecret(providerLabel) {
765
+ log.warn(
766
+ `\u{1F512} ${providerLabel} key \u0111\xE3 l\u01B0u PLAINTEXT v\xE0o .claude/settings.json.
767
+ File n\xE0y \u0111\u01B0\u1EE3c gitignore t\u1EEB Avatar v1.7.0, nh\u01B0ng workspace c\u0169 c\xF3 th\u1EC3 CH\u01AFA.
768
+ Ki\u1EC3m tra: grep '.claude/settings.json' .gitignore
769
+ N\u1EBFu ch\u01B0a c\xF3 \u2192 TH\xCAM NGAY, tr\xE1nh leak key khi commit/push.`
770
+ );
771
+ }
764
772
  async function runAiSetupPhase(args) {
765
773
  try {
766
774
  log.info("Setup AI provider cho workspace...");
@@ -819,6 +827,7 @@ async function runAiSetupPhase(args) {
819
827
  `provider=llmlite,result=ok,model=${llmConfig.model},base=${llmConfig.baseUrl}`
820
828
  );
821
829
  log.success(`AI ready \xB7 LLMLite \xB7 model=${llmConfig.model} \xB7 ${llmConfig.baseUrl}`);
830
+ warnAboutPlaintextSecret("LLMLite");
822
831
  return { ok: true, provider: "llmlite", model: llmConfig.model };
823
832
  }
824
833
  case "anthropic": {
@@ -836,6 +845,7 @@ async function runAiSetupPhase(args) {
836
845
  log.success(
837
846
  `AI ready \xB7 Anthropic Direct \xB7 model=${anthropicConfig.model} \xB7 ${anthropicConfig.baseUrl}`
838
847
  );
848
+ warnAboutPlaintextSecret("Anthropic Direct API");
839
849
  return { ok: true, provider: "anthropic", model: anthropicConfig.model };
840
850
  }
841
851
  case "use-global": {
@@ -1291,9 +1301,13 @@ async function listTags(cwd = process.cwd()) {
1291
1301
  const result = await git(cwd).tags();
1292
1302
  return result.all;
1293
1303
  }
1294
- async function latestTag(cwd = process.cwd()) {
1295
- const tags = await listTags(cwd);
1296
- return tags.length > 0 ? tags[tags.length - 1] ?? null : null;
1304
+ async function tagAtHead(cwd = process.cwd()) {
1305
+ try {
1306
+ const result = await git(cwd).raw(["describe", "--tags", "--exact-match", "HEAD"]);
1307
+ return result.trim() || null;
1308
+ } catch {
1309
+ return null;
1310
+ }
1297
1311
  }
1298
1312
  async function currentCommitSha(cwd = process.cwd()) {
1299
1313
  const result = await git(cwd).revparse(["HEAD"]);
@@ -1494,11 +1508,12 @@ async function runChecks(cwd) {
1494
1508
  });
1495
1509
  }
1496
1510
  const gitignorePath = join11(cwd, ".gitignore");
1511
+ let gitignoreContent = "";
1497
1512
  if (gitRepo) {
1498
1513
  let gitignoreOk = false;
1499
1514
  if (await pathExists(gitignorePath)) {
1500
- const content = await fs6.readFile(gitignorePath, "utf8");
1501
- gitignoreOk = content.includes(".claude/_pending/");
1515
+ gitignoreContent = await fs6.readFile(gitignorePath, "utf8");
1516
+ gitignoreOk = gitignoreContent.includes(".claude/_pending/");
1502
1517
  }
1503
1518
  checks.push({
1504
1519
  name: ".gitignore Avatar entries",
@@ -1506,6 +1521,66 @@ async function runChecks(cwd) {
1506
1521
  detail: gitignoreOk ? "c\xF3 .claude/_pending/, .claude/_backup/" : "thi\u1EBFu entries",
1507
1522
  fixable: false
1508
1523
  });
1524
+ const settingsGitignored = gitignoreContent.includes(".claude/settings.json") || gitignoreContent.includes("/.claude/settings.json") || gitignoreContent.includes(".claude/*.json");
1525
+ checks.push({
1526
+ name: "\u{1F512} settings.json gitignored",
1527
+ status: settingsGitignored ? "ok" : "fail",
1528
+ detail: settingsGitignored ? "an to\xE0n \u2014 settings.json kh\xF4ng commit nh\u1EA7m" : "CRITICAL: settings.json ch\u1EE9a API key \u2014 ch\u1EA1y 'avatar doctor --fix' \u0111\u1EC3 add gitignore",
1529
+ fixable: !settingsGitignored,
1530
+ fix: settingsGitignored ? void 0 : async () => {
1531
+ const addition = "\n# Avatar v1.7.0 \u2014 Security: settings.json ch\u1EE9a raw API key, KH\xD4NG commit.\n.claude/settings.json\n.claude/settings.json.backup-*\n";
1532
+ await fs6.appendFile(gitignorePath, addition);
1533
+ }
1534
+ });
1535
+ }
1536
+ const pythonCheck = spawnSync6("which", ["python"]);
1537
+ const python3Check = spawnSync6("which", ["python3"]);
1538
+ const hasPython = pythonCheck.status === 0;
1539
+ const hasPython3 = python3Check.status === 0;
1540
+ if (hasPython3 && !hasPython) {
1541
+ checks.push({
1542
+ name: "Python binary alias",
1543
+ status: "warn",
1544
+ detail: `Ch\u1EC9 c\xF3 python3 (modern macOS). Pack scripts th\u01B0\u1EDDng ref 'python' \u2192 suggest: ln -s ${python3Check.stdout.toString().trim()} ~/.local/bin/python`,
1545
+ fixable: false
1546
+ });
1547
+ } else if (hasPython) {
1548
+ checks.push({
1549
+ name: "Python binary",
1550
+ status: "ok",
1551
+ detail: `python: ${pythonCheck.stdout.toString().trim()}`,
1552
+ fixable: false
1553
+ });
1554
+ } else if (hasPython3) {
1555
+ checks.push({
1556
+ name: "Python binary",
1557
+ status: "ok",
1558
+ detail: `python3: ${python3Check.stdout.toString().trim()}`,
1559
+ fixable: false
1560
+ });
1561
+ }
1562
+ const settingsPath = join11(cwd, ".claude", "settings.json");
1563
+ if (await pathExists(settingsPath)) {
1564
+ try {
1565
+ const settingsRaw = await fs6.readFile(settingsPath, "utf8");
1566
+ const settings = JSON.parse(settingsRaw);
1567
+ if (settings.statusLine?.command) {
1568
+ const cmd = settings.statusLine.command.trim();
1569
+ const match = cmd.match(/^(node|python|python3|bash|sh)\s+([^\s]+)/);
1570
+ if (match) {
1571
+ const refFile = match[2];
1572
+ const fullPath = refFile.startsWith("/") ? refFile : join11(cwd, refFile);
1573
+ const fileExists = await pathExists(fullPath);
1574
+ checks.push({
1575
+ name: "statusLine command",
1576
+ status: fileExists ? "ok" : "fail",
1577
+ detail: fileExists ? `ref OK: ${refFile}` : `BROKEN: settings.json ref '${refFile}' nh\u01B0ng file kh\xF4ng t\u1ED3n t\u1EA1i. Strip field statusLine ho\u1EB7c fix path.`,
1578
+ fixable: false
1579
+ });
1580
+ }
1581
+ }
1582
+ } catch {
1583
+ }
1509
1584
  }
1510
1585
  const which = spawnSync6("which", ["claude"]);
1511
1586
  const hasClaudeCli = which.status === 0;
@@ -2359,6 +2434,31 @@ async function ensureTeamPackAccessWithRetry(args) {
2359
2434
  }
2360
2435
  }
2361
2436
 
2437
+ // src/lib/pick-latest-stable-semver-tag.ts
2438
+ var SEMVER_REGEX3 = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
2439
+ function parseSemVerTag(tag) {
2440
+ const match = tag.match(SEMVER_REGEX3);
2441
+ if (!match) return null;
2442
+ const [, major, minor, patch, prerelease] = match;
2443
+ return {
2444
+ raw: tag,
2445
+ major: Number.parseInt(major ?? "0", 10),
2446
+ minor: Number.parseInt(minor ?? "0", 10),
2447
+ patch: Number.parseInt(patch ?? "0", 10),
2448
+ prerelease: prerelease ?? null
2449
+ };
2450
+ }
2451
+ function pickLatestStableSemVerTag(tags, includePrerelease = false) {
2452
+ const parsed = tags.map(parseSemVerTag).filter((t) => t !== null).filter((t) => includePrerelease || t.prerelease === null);
2453
+ if (parsed.length === 0) return null;
2454
+ parsed.sort((a, b) => {
2455
+ if (a.major !== b.major) return a.major - b.major;
2456
+ if (a.minor !== b.minor) return a.minor - b.minor;
2457
+ return a.patch - b.patch;
2458
+ });
2459
+ return parsed[parsed.length - 1]?.raw ?? null;
2460
+ }
2461
+
2362
2462
  // src/lib/resolve-team-pack-repo-url.ts
2363
2463
  var ORG_DEFAULT = "git@github.com:nalvn/team-ai-pack.git";
2364
2464
  function resolveTeamPackRepoUrl() {
@@ -2405,7 +2505,9 @@ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
2405
2505
  }
2406
2506
  let target = tag ?? null;
2407
2507
  if (!target) {
2408
- target = await latestTag(join16(projectRoot, TEAM_PACK_RELATIVE_PATH));
2508
+ const submoduleDir = join16(projectRoot, TEAM_PACK_RELATIVE_PATH);
2509
+ const allTags = await listTags(submoduleDir);
2510
+ target = pickLatestStableSemVerTag(allTags);
2409
2511
  }
2410
2512
  if (target) {
2411
2513
  await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
@@ -2414,7 +2516,7 @@ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
2414
2516
  }
2415
2517
  async function readPinnedPackVersion(projectRoot) {
2416
2518
  const submoduleRoot = join16(projectRoot, TEAM_PACK_RELATIVE_PATH);
2417
- const tag = await latestTag(submoduleRoot);
2519
+ const tag = await tagAtHead(submoduleRoot);
2418
2520
  if (tag) return tag;
2419
2521
  const sha = await currentCommitSha(submoduleRoot);
2420
2522
  return sha.slice(0, 7);
@@ -3097,6 +3199,16 @@ function linkExistingRemoteToWorkspace(args) {
3097
3199
  // src/lib/merge-pack-settings-into-project-settings.ts
3098
3200
  import { promises as fs9 } from "fs";
3099
3201
  import { join as join17 } from "path";
3202
+ async function isStatusLineCommandResolvable(workspacePath, command) {
3203
+ const trimmed = command.trim();
3204
+ const match = trimmed.match(/^(node|python|python3|bash|sh)\s+([^\s]+)/);
3205
+ if (!match) {
3206
+ return true;
3207
+ }
3208
+ const filePath = match[2];
3209
+ const fullPath = filePath.startsWith("/") ? filePath : join17(workspacePath, filePath);
3210
+ return await pathExists(fullPath);
3211
+ }
3100
3212
  function backupFilename(originalPath) {
3101
3213
  const d = /* @__PURE__ */ new Date();
3102
3214
  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")}`;
@@ -3157,8 +3269,18 @@ async function mergePackSettingsIntoProjectSettings(workspacePath) {
3157
3269
  const changes = [];
3158
3270
  const merged = { ...userSettings };
3159
3271
  if (packTemplate.statusLine && !userSettings.statusLine) {
3160
- merged.statusLine = packTemplate.statusLine;
3161
- changes.push("statusLine added");
3272
+ const statusLineOk = await isStatusLineCommandResolvable(
3273
+ workspacePath,
3274
+ packTemplate.statusLine.command
3275
+ );
3276
+ if (statusLineOk) {
3277
+ merged.statusLine = packTemplate.statusLine;
3278
+ changes.push("statusLine added");
3279
+ } else {
3280
+ changes.push(
3281
+ `statusLine SKIPPED (file ref '${packTemplate.statusLine.command}' kh\xF4ng t\u1ED3n t\u1EA1i)`
3282
+ );
3283
+ }
3162
3284
  }
3163
3285
  if (typeof packTemplate.includeCoAuthoredBy === "boolean" && typeof userSettings.includeCoAuthoredBy !== "boolean") {
3164
3286
  merged.includeCoAuthoredBy = packTemplate.includeCoAuthoredBy;
@@ -4545,9 +4667,11 @@ async function listCommitsBetween(packDir, fromSha, toRef) {
4545
4667
  }
4546
4668
  }
4547
4669
  async function buildSyncPreview(packDir, claudeDir, targetVersion) {
4670
+ const currentTagOrNull = await tagAtHead(packDir);
4548
4671
  const currentSha = await currentCommitSha(packDir);
4549
- const currentVersion = currentSha.slice(0, 7);
4550
- const target = targetVersion ?? await latestTag(packDir) ?? "HEAD";
4672
+ const currentVersion = currentTagOrNull ?? currentSha.slice(0, 7);
4673
+ const allTags = await listTags(packDir);
4674
+ const target = targetVersion ?? pickLatestStableSemVerTag(allTags) ?? "HEAD";
4551
4675
  const commits = await listCommitsBetween(packDir, currentSha, target);
4552
4676
  const mountStatuses = [];
4553
4677
  for (const dir of TEAM_PACK_MOUNT_DIRS) {
@@ -4584,10 +4708,13 @@ async function syncAction(opts) {
4584
4708
  `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.`
4585
4709
  );
4586
4710
  }
4587
- const targetVersion = opts.version ?? await latestTag(packDir);
4711
+ const allTags = await listTags(packDir);
4712
+ const targetVersion = opts.version ?? pickLatestStableSemVerTag(allTags);
4588
4713
  if (!targetVersion) {
4589
4714
  log.error(
4590
- "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."
4715
+ `Kh\xF4ng t\xECm th\u1EA5y stable SemVer tag (vMAJOR.MINOR.PATCH) trong team-ai-pack submodule.
4716
+ Tags hi\u1EC7n c\xF3: ${allTags.length > 0 ? allTags.join(", ") : "(none)"}
4717
+ Pass --version <tag> r\xF5 r\xE0ng, ho\u1EB7c tag pack theo SemVer convention.`
4591
4718
  );
4592
4719
  process.exit(1);
4593
4720
  return;
@@ -4840,7 +4967,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
4840
4967
  }
4841
4968
 
4842
4969
  // src/commands/uninstall.ts
4843
- var CLI_VERSION = "1.6.4";
4970
+ var CLI_VERSION = "1.7.1";
4844
4971
  function registerUninstallCommand(program2) {
4845
4972
  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) => {
4846
4973
  try {
@@ -4922,7 +5049,7 @@ function printUninstallSuccessBox(backupPath) {
4922
5049
  }
4923
5050
 
4924
5051
  // src/index.ts
4925
- var CLI_VERSION2 = "1.6.4";
5052
+ var CLI_VERSION2 = "1.7.1";
4926
5053
  var program = new Command();
4927
5054
  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(
4928
5055
  "beforeAll",