@nalvietnam/avatar-cli 1.6.4 → 1.7.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
@@ -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": {
@@ -1494,11 +1504,12 @@ async function runChecks(cwd) {
1494
1504
  });
1495
1505
  }
1496
1506
  const gitignorePath = join11(cwd, ".gitignore");
1507
+ let gitignoreContent = "";
1497
1508
  if (gitRepo) {
1498
1509
  let gitignoreOk = false;
1499
1510
  if (await pathExists(gitignorePath)) {
1500
- const content = await fs6.readFile(gitignorePath, "utf8");
1501
- gitignoreOk = content.includes(".claude/_pending/");
1511
+ gitignoreContent = await fs6.readFile(gitignorePath, "utf8");
1512
+ gitignoreOk = gitignoreContent.includes(".claude/_pending/");
1502
1513
  }
1503
1514
  checks.push({
1504
1515
  name: ".gitignore Avatar entries",
@@ -1506,6 +1517,66 @@ async function runChecks(cwd) {
1506
1517
  detail: gitignoreOk ? "c\xF3 .claude/_pending/, .claude/_backup/" : "thi\u1EBFu entries",
1507
1518
  fixable: false
1508
1519
  });
1520
+ const settingsGitignored = gitignoreContent.includes(".claude/settings.json") || gitignoreContent.includes("/.claude/settings.json") || gitignoreContent.includes(".claude/*.json");
1521
+ checks.push({
1522
+ name: "\u{1F512} settings.json gitignored",
1523
+ status: settingsGitignored ? "ok" : "fail",
1524
+ 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",
1525
+ fixable: !settingsGitignored,
1526
+ fix: settingsGitignored ? void 0 : async () => {
1527
+ 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";
1528
+ await fs6.appendFile(gitignorePath, addition);
1529
+ }
1530
+ });
1531
+ }
1532
+ const pythonCheck = spawnSync6("which", ["python"]);
1533
+ const python3Check = spawnSync6("which", ["python3"]);
1534
+ const hasPython = pythonCheck.status === 0;
1535
+ const hasPython3 = python3Check.status === 0;
1536
+ if (hasPython3 && !hasPython) {
1537
+ checks.push({
1538
+ name: "Python binary alias",
1539
+ status: "warn",
1540
+ 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`,
1541
+ fixable: false
1542
+ });
1543
+ } else if (hasPython) {
1544
+ checks.push({
1545
+ name: "Python binary",
1546
+ status: "ok",
1547
+ detail: `python: ${pythonCheck.stdout.toString().trim()}`,
1548
+ fixable: false
1549
+ });
1550
+ } else if (hasPython3) {
1551
+ checks.push({
1552
+ name: "Python binary",
1553
+ status: "ok",
1554
+ detail: `python3: ${python3Check.stdout.toString().trim()}`,
1555
+ fixable: false
1556
+ });
1557
+ }
1558
+ const settingsPath = join11(cwd, ".claude", "settings.json");
1559
+ if (await pathExists(settingsPath)) {
1560
+ try {
1561
+ const settingsRaw = await fs6.readFile(settingsPath, "utf8");
1562
+ const settings = JSON.parse(settingsRaw);
1563
+ if (settings.statusLine?.command) {
1564
+ const cmd = settings.statusLine.command.trim();
1565
+ const match = cmd.match(/^(node|python|python3|bash|sh)\s+([^\s]+)/);
1566
+ if (match) {
1567
+ const refFile = match[2];
1568
+ const fullPath = refFile.startsWith("/") ? refFile : join11(cwd, refFile);
1569
+ const fileExists = await pathExists(fullPath);
1570
+ checks.push({
1571
+ name: "statusLine command",
1572
+ status: fileExists ? "ok" : "fail",
1573
+ 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.`,
1574
+ fixable: false
1575
+ });
1576
+ }
1577
+ }
1578
+ } catch {
1579
+ }
1509
1580
  }
1510
1581
  const which = spawnSync6("which", ["claude"]);
1511
1582
  const hasClaudeCli = which.status === 0;
@@ -3097,6 +3168,16 @@ function linkExistingRemoteToWorkspace(args) {
3097
3168
  // src/lib/merge-pack-settings-into-project-settings.ts
3098
3169
  import { promises as fs9 } from "fs";
3099
3170
  import { join as join17 } from "path";
3171
+ async function isStatusLineCommandResolvable(workspacePath, command) {
3172
+ const trimmed = command.trim();
3173
+ const match = trimmed.match(/^(node|python|python3|bash|sh)\s+([^\s]+)/);
3174
+ if (!match) {
3175
+ return true;
3176
+ }
3177
+ const filePath = match[2];
3178
+ const fullPath = filePath.startsWith("/") ? filePath : join17(workspacePath, filePath);
3179
+ return await pathExists(fullPath);
3180
+ }
3100
3181
  function backupFilename(originalPath) {
3101
3182
  const d = /* @__PURE__ */ new Date();
3102
3183
  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 +3238,18 @@ async function mergePackSettingsIntoProjectSettings(workspacePath) {
3157
3238
  const changes = [];
3158
3239
  const merged = { ...userSettings };
3159
3240
  if (packTemplate.statusLine && !userSettings.statusLine) {
3160
- merged.statusLine = packTemplate.statusLine;
3161
- changes.push("statusLine added");
3241
+ const statusLineOk = await isStatusLineCommandResolvable(
3242
+ workspacePath,
3243
+ packTemplate.statusLine.command
3244
+ );
3245
+ if (statusLineOk) {
3246
+ merged.statusLine = packTemplate.statusLine;
3247
+ changes.push("statusLine added");
3248
+ } else {
3249
+ changes.push(
3250
+ `statusLine SKIPPED (file ref '${packTemplate.statusLine.command}' kh\xF4ng t\u1ED3n t\u1EA1i)`
3251
+ );
3252
+ }
3162
3253
  }
3163
3254
  if (typeof packTemplate.includeCoAuthoredBy === "boolean" && typeof userSettings.includeCoAuthoredBy !== "boolean") {
3164
3255
  merged.includeCoAuthoredBy = packTemplate.includeCoAuthoredBy;
@@ -4840,7 +4931,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
4840
4931
  }
4841
4932
 
4842
4933
  // src/commands/uninstall.ts
4843
- var CLI_VERSION = "1.6.4";
4934
+ var CLI_VERSION = "1.7.0";
4844
4935
  function registerUninstallCommand(program2) {
4845
4936
  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
4937
  try {
@@ -4922,7 +5013,7 @@ function printUninstallSuccessBox(backupPath) {
4922
5013
  }
4923
5014
 
4924
5015
  // src/index.ts
4925
- var CLI_VERSION2 = "1.6.4";
5016
+ var CLI_VERSION2 = "1.7.0";
4926
5017
  var program = new Command();
4927
5018
  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
5019
  "beforeAll",