@hasna/configs 0.1.0 → 0.1.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/cli/index.js CHANGED
@@ -2073,7 +2073,7 @@ var {
2073
2073
  import chalk from "chalk";
2074
2074
  import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
2075
2075
  import { homedir as homedir3 } from "os";
2076
- import { join as join5, resolve as resolve6 } from "path";
2076
+ import { join as join5, resolve as resolve5 } from "path";
2077
2077
 
2078
2078
  // src/types/index.ts
2079
2079
  class ConfigNotFoundError extends Error {
@@ -2503,138 +2503,303 @@ async function applyConfigs(configs, opts = {}) {
2503
2503
  }
2504
2504
 
2505
2505
  // src/lib/sync.ts
2506
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
2507
- import { extname, join as join2, relative } from "path";
2506
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "fs";
2507
+ import { extname, join as join2 } from "path";
2508
2508
  import { homedir as homedir2 } from "os";
2509
- function detectCategory(filePath) {
2510
- const p = filePath.toLowerCase().replace(homedir2(), "~");
2511
- if (p.includes("/.claude/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("gemini.md"))
2512
- return "rules";
2513
- if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.gemini/") || p.includes("/.cursor/"))
2514
- return "agent";
2515
- if (p.includes(".mcp.json") || p.includes("mcp"))
2516
- return "mcp";
2517
- if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc") || p.includes(".bash_profile"))
2518
- return "shell";
2519
- if (p.includes(".gitconfig") || p.includes(".gitignore"))
2520
- return "git";
2521
- if (p.includes(".npmrc") || p.includes("tsconfig") || p.includes("bunfig"))
2522
- return "tools";
2523
- if (p.includes(".secrets"))
2524
- return "secrets_schema";
2525
- return "tools";
2526
- }
2527
- function detectAgent(filePath) {
2528
- const p = filePath.toLowerCase().replace(homedir2(), "~");
2529
- if (p.includes("/.claude/") || p.endsWith("claude.md"))
2530
- return "claude";
2531
- if (p.includes("/.codex/") || p.endsWith("agents.md"))
2532
- return "codex";
2533
- if (p.includes("/.gemini/") || p.endsWith("gemini.md"))
2534
- return "gemini";
2535
- if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc"))
2536
- return "zsh";
2537
- if (p.includes(".gitconfig") || p.includes(".gitignore"))
2538
- return "git";
2539
- if (p.includes(".npmrc"))
2540
- return "npm";
2541
- return "global";
2509
+
2510
+ // src/lib/redact.ts
2511
+ var SECRET_KEY_PATTERN = /^(.*_?API_?KEY|.*_?TOKEN|.*_?SECRET|.*_?PASSWORD|.*_?PASSWD|.*_?CREDENTIAL|.*_?AUTH(?:_TOKEN|_KEY|ORIZATION)?|.*_?PRIVATE_?KEY|.*_?ACCESS_?KEY|.*_?CLIENT_?SECRET|.*_?SIGNING_?KEY|.*_?ENCRYPTION_?KEY|.*_AUTH_TOKEN)$/i;
2512
+ var VALUE_PATTERNS = [
2513
+ { re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
2514
+ { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
2515
+ { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
2516
+ { re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
2517
+ { re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
2518
+ { re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
2519
+ { re: /ey[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\./, reason: "JWT token" },
2520
+ { re: /AKIA[0-9A-Z]{16}/, reason: "AWS access key" }
2521
+ ];
2522
+ var MIN_SECRET_VALUE_LEN = 8;
2523
+ function redactShell(content) {
2524
+ const redacted = [];
2525
+ const lines = content.split(`
2526
+ `);
2527
+ const out = [];
2528
+ for (let i = 0;i < lines.length; i++) {
2529
+ const line = lines[i];
2530
+ const m = line.match(/^(\s*(?:export\s+)?)([A-Z][A-Z0-9_]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
2531
+ if (m) {
2532
+ const [, prefix, key, eq, quote, value] = m;
2533
+ if (shouldRedactKeyValue(key, value)) {
2534
+ const reason = reasonFor(key, value);
2535
+ redacted.push({ varName: key, line: i + 1, reason });
2536
+ out.push(`${prefix}${key}${eq}${quote}{{${key}}}${quote}`);
2537
+ continue;
2538
+ }
2539
+ }
2540
+ out.push(line);
2541
+ }
2542
+ return { content: out.join(`
2543
+ `), redacted, isTemplate: redacted.length > 0 };
2542
2544
  }
2543
- function detectFormat(filePath) {
2544
- const ext = extname(filePath).toLowerCase();
2545
- if (ext === ".json")
2546
- return "json";
2547
- if (ext === ".toml")
2548
- return "toml";
2549
- if (ext === ".yaml" || ext === ".yml")
2550
- return "yaml";
2551
- if (ext === ".md" || ext === ".markdown")
2552
- return "markdown";
2553
- if (ext === ".ini" || ext === ".cfg")
2554
- return "ini";
2555
- return "text";
2545
+ function redactJson(content) {
2546
+ const redacted = [];
2547
+ const lines = content.split(`
2548
+ `);
2549
+ const out = [];
2550
+ for (let i = 0;i < lines.length; i++) {
2551
+ const line = lines[i];
2552
+ const m = line.match(/^(\s*"([^"]+)"\s*:\s*)"([^"]+)"(,?)(\s*)$/);
2553
+ if (m) {
2554
+ const [, prefix, key, value, comma, trail] = m;
2555
+ if (shouldRedactKeyValue(key, value)) {
2556
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2557
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
2558
+ out.push(`${prefix}"{{${varName}}}"${comma}${trail}`);
2559
+ continue;
2560
+ }
2561
+ }
2562
+ let newLine = line;
2563
+ for (const { re, reason } of VALUE_PATTERNS) {
2564
+ newLine = newLine.replace(re, (match) => {
2565
+ const varName = `REDACTED_${reason.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
2566
+ redacted.push({ varName, line: i + 1, reason });
2567
+ return `{{${varName}}}`;
2568
+ });
2569
+ }
2570
+ out.push(newLine);
2571
+ }
2572
+ return { content: out.join(`
2573
+ `), redacted, isTemplate: redacted.length > 0 };
2556
2574
  }
2557
- var SKIP_PATTERNS = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
2558
- function shouldSkip(p) {
2559
- return SKIP_PATTERNS.some((pat) => p.includes(pat));
2575
+ function redactToml(content) {
2576
+ const redacted = [];
2577
+ const lines = content.split(`
2578
+ `);
2579
+ const out = [];
2580
+ for (let i = 0;i < lines.length; i++) {
2581
+ const line = lines[i];
2582
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
2583
+ if (m) {
2584
+ const [, indent, key, eq, quote, value] = m;
2585
+ if (shouldRedactKeyValue(key, value)) {
2586
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2587
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
2588
+ out.push(`${indent}${key}${eq}${quote}{{${varName}}}${quote}`);
2589
+ continue;
2590
+ }
2591
+ }
2592
+ out.push(line);
2593
+ }
2594
+ return { content: out.join(`
2595
+ `), redacted, isTemplate: redacted.length > 0 };
2560
2596
  }
2561
- function walkDir(dir, files = []) {
2562
- const entries = readdirSync(dir, { withFileTypes: true });
2563
- for (const entry of entries) {
2564
- const full = join2(dir, entry.name);
2565
- if (shouldSkip(full))
2597
+ function redactIni(content) {
2598
+ const redacted = [];
2599
+ const lines = content.split(`
2600
+ `);
2601
+ const out = [];
2602
+ for (let i = 0;i < lines.length; i++) {
2603
+ const line = lines[i];
2604
+ const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
2605
+ if (authM) {
2606
+ redacted.push({ varName: "NPM_AUTH_TOKEN", line: i + 1, reason: "npm auth token" });
2607
+ out.push(`${authM[1]}{{NPM_AUTH_TOKEN}}`);
2566
2608
  continue;
2567
- if (entry.isDirectory()) {
2568
- walkDir(full, files);
2569
- } else if (entry.isFile()) {
2570
- files.push(full);
2571
2609
  }
2610
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
2611
+ if (m) {
2612
+ const [, indent, key, eq, value] = m;
2613
+ if (shouldRedactKeyValue(key, value)) {
2614
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2615
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
2616
+ out.push(`${indent}${key}${eq}{{${varName}}}`);
2617
+ continue;
2618
+ }
2619
+ }
2620
+ out.push(line);
2572
2621
  }
2573
- return files;
2622
+ return { content: out.join(`
2623
+ `), redacted, isTemplate: redacted.length > 0 };
2574
2624
  }
2575
- async function syncFromDir(dir, opts = {}) {
2576
- const d = opts.db || getDatabase();
2577
- const absDir = expandPath(dir);
2578
- if (!existsSync3(absDir)) {
2579
- return { added: 0, updated: 0, unchanged: 0, skipped: [`Directory not found: ${absDir}`] };
2625
+ function redactGeneric(content) {
2626
+ const redacted = [];
2627
+ const lines = content.split(`
2628
+ `);
2629
+ const out = [];
2630
+ for (let i = 0;i < lines.length; i++) {
2631
+ let line = lines[i];
2632
+ for (const { re, reason } of VALUE_PATTERNS) {
2633
+ line = line.replace(re, (match) => {
2634
+ const varName = reason.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2635
+ redacted.push({ varName, line: i + 1, reason });
2636
+ return `{{${varName}}}`;
2637
+ });
2638
+ }
2639
+ out.push(line);
2640
+ }
2641
+ return { content: out.join(`
2642
+ `), redacted, isTemplate: redacted.length > 0 };
2643
+ }
2644
+ function shouldRedactKeyValue(key, value) {
2645
+ if (!value || value.startsWith("{{"))
2646
+ return false;
2647
+ if (value.length < MIN_SECRET_VALUE_LEN)
2648
+ return false;
2649
+ if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
2650
+ return false;
2651
+ if (SECRET_KEY_PATTERN.test(key))
2652
+ return true;
2653
+ for (const { re } of VALUE_PATTERNS) {
2654
+ if (re.test(value))
2655
+ return true;
2580
2656
  }
2581
- const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join2(absDir, f)).filter((f) => statSync(f).isFile());
2657
+ return false;
2658
+ }
2659
+ function reasonFor(key, value) {
2660
+ if (SECRET_KEY_PATTERN.test(key))
2661
+ return `secret key name: ${key}`;
2662
+ for (const { re, reason } of VALUE_PATTERNS) {
2663
+ if (re.test(value))
2664
+ return reason;
2665
+ }
2666
+ return "secret value pattern";
2667
+ }
2668
+ function redactContent(content, format) {
2669
+ switch (format) {
2670
+ case "shell":
2671
+ return redactShell(content);
2672
+ case "json":
2673
+ return redactJson(content);
2674
+ case "toml":
2675
+ return redactToml(content);
2676
+ case "ini":
2677
+ return redactIni(content);
2678
+ default:
2679
+ return redactGeneric(content);
2680
+ }
2681
+ }
2682
+ function scanSecrets(content, format) {
2683
+ const r = redactContent(content, format);
2684
+ return r.redacted;
2685
+ }
2686
+
2687
+ // src/lib/sync.ts
2688
+ var KNOWN_CONFIGS = [
2689
+ { path: "~/.claude/CLAUDE.md", name: "claude-claude-md", category: "rules", agent: "claude", format: "markdown" },
2690
+ { path: "~/.claude/settings.json", name: "claude-settings", category: "agent", agent: "claude", format: "json" },
2691
+ { path: "~/.claude/settings.local.json", name: "claude-settings-local", category: "agent", agent: "claude", format: "json" },
2692
+ { path: "~/.claude/keybindings.json", name: "claude-keybindings", category: "agent", agent: "claude", format: "json" },
2693
+ { path: "~/.claude/rules", name: "claude-rules", category: "rules", agent: "claude", rulesDir: "~/.claude/rules" },
2694
+ { path: "~/.codex/config.toml", name: "codex-config", category: "agent", agent: "codex", format: "toml" },
2695
+ { path: "~/.codex/AGENTS.md", name: "codex-agents-md", category: "rules", agent: "codex", format: "markdown" },
2696
+ { path: "~/.gemini/settings.json", name: "gemini-settings", category: "agent", agent: "gemini", format: "json" },
2697
+ { path: "~/.gemini/GEMINI.md", name: "gemini-gemini-md", category: "rules", agent: "gemini", format: "markdown" },
2698
+ { path: "~/.claude.json", name: "claude-json", category: "mcp", agent: "claude", format: "json", description: "Claude Code global config (includes MCP server entries)" },
2699
+ { path: "~/.zshrc", name: "zshrc", category: "shell", agent: "zsh" },
2700
+ { path: "~/.zprofile", name: "zprofile", category: "shell", agent: "zsh" },
2701
+ { path: "~/.bashrc", name: "bashrc", category: "shell", agent: "zsh" },
2702
+ { path: "~/.bash_profile", name: "bash-profile", category: "shell", agent: "zsh" },
2703
+ { path: "~/.gitconfig", name: "gitconfig", category: "git", agent: "git", format: "ini" },
2704
+ { path: "~/.gitignore_global", name: "gitignore-global", category: "git", agent: "git" },
2705
+ { path: "~/.npmrc", name: "npmrc", category: "tools", agent: "npm", format: "ini" },
2706
+ { path: "~/.bunfig.toml", name: "bunfig", category: "tools", agent: "global", format: "toml" }
2707
+ ];
2708
+ async function syncKnown(opts = {}) {
2709
+ const d = opts.db || getDatabase();
2582
2710
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
2711
+ const home = homedir2();
2712
+ let targets = KNOWN_CONFIGS;
2713
+ if (opts.agent)
2714
+ targets = targets.filter((k) => k.agent === opts.agent);
2715
+ if (opts.category)
2716
+ targets = targets.filter((k) => k.category === opts.category);
2583
2717
  const allConfigs = listConfigs(undefined, d);
2584
- for (const file of files) {
2585
- if (shouldSkip(file)) {
2586
- result.skipped.push(file);
2718
+ for (const known of targets) {
2719
+ if (known.rulesDir) {
2720
+ const absDir = expandPath(known.rulesDir);
2721
+ if (!existsSync3(absDir)) {
2722
+ result.skipped.push(known.rulesDir);
2723
+ continue;
2724
+ }
2725
+ const mdFiles = readdirSync(absDir).filter((f) => f.endsWith(".md"));
2726
+ for (const f of mdFiles) {
2727
+ const abs2 = join2(absDir, f);
2728
+ const targetPath = abs2.replace(home, "~");
2729
+ const raw = readFileSync2(abs2, "utf-8");
2730
+ const { content, isTemplate } = redactContent(raw, "markdown");
2731
+ const name = `claude-rules-${f}`;
2732
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2733
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
2734
+ if (!existing) {
2735
+ if (!opts.dryRun)
2736
+ createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate }, d);
2737
+ result.added++;
2738
+ } else if (existing.content !== content) {
2739
+ if (!opts.dryRun)
2740
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
2741
+ result.updated++;
2742
+ } else {
2743
+ result.unchanged++;
2744
+ }
2745
+ }
2746
+ continue;
2747
+ }
2748
+ const abs = expandPath(known.path);
2749
+ if (!existsSync3(abs)) {
2750
+ result.skipped.push(known.path);
2587
2751
  continue;
2588
2752
  }
2589
2753
  try {
2590
- const content = readFileSync2(file, "utf-8");
2591
- const targetPath = file.startsWith(homedir2()) ? file.replace(homedir2(), "~") : file;
2592
- const existing = allConfigs.find((c) => c.target_path === targetPath);
2754
+ const rawContent = readFileSync2(abs, "utf-8");
2755
+ if (rawContent.length > 500000) {
2756
+ result.skipped.push(known.path + " (too large)");
2757
+ continue;
2758
+ }
2759
+ const fmt = known.format ?? detectFormat(abs);
2760
+ const { content, isTemplate } = redactContent(rawContent, fmt);
2761
+ const targetPath = abs.replace(home, "~");
2762
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
2593
2763
  if (!existing) {
2594
2764
  if (!opts.dryRun) {
2595
- const name = relative(absDir, file);
2596
2765
  createConfig({
2597
- name,
2598
- category: detectCategory(file),
2599
- agent: detectAgent(file),
2600
- target_path: targetPath,
2601
- format: detectFormat(file),
2602
- content
2766
+ name: known.name,
2767
+ category: known.category,
2768
+ agent: known.agent,
2769
+ format: fmt,
2770
+ content,
2771
+ target_path: known.kind === "reference" ? null : targetPath,
2772
+ kind: known.kind ?? "file",
2773
+ description: known.description,
2774
+ is_template: isTemplate
2603
2775
  }, d);
2604
2776
  }
2605
2777
  result.added++;
2606
2778
  } else if (existing.content !== content) {
2607
- if (!opts.dryRun) {
2608
- updateConfig(existing.id, { content }, d);
2609
- }
2779
+ if (!opts.dryRun)
2780
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
2610
2781
  result.updated++;
2611
2782
  } else {
2612
2783
  result.unchanged++;
2613
2784
  }
2614
2785
  } catch {
2615
- result.skipped.push(file);
2786
+ result.skipped.push(known.path);
2616
2787
  }
2617
2788
  }
2618
2789
  return result;
2619
2790
  }
2620
- async function syncToDir(dir, opts = {}) {
2791
+ async function syncToDisk(opts = {}) {
2621
2792
  const d = opts.db || getDatabase();
2622
- const absDir = expandPath(dir);
2623
- const normalizedDir = dir.startsWith("~/") ? dir : absDir.replace(homedir2(), "~");
2624
- const configs = listConfigs(undefined, d).filter((c) => c.target_path && (c.target_path.startsWith(normalizedDir) || c.target_path.startsWith(absDir)));
2625
2793
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
2794
+ let configs = listConfigs({ kind: "file", ...opts.agent ? { agent: opts.agent } : {}, ...opts.category ? { category: opts.category } : {} }, d);
2626
2795
  for (const config of configs) {
2627
- if (config.kind === "reference")
2796
+ if (!config.target_path)
2628
2797
  continue;
2629
2798
  try {
2630
2799
  const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
2631
- if (r.changed) {
2632
- existsSync3(expandPath(config.target_path)) ? result.updated++ : result.added++;
2633
- } else {
2634
- result.unchanged++;
2635
- }
2800
+ r.changed ? result.updated++ : result.unchanged++;
2636
2801
  } catch {
2637
- result.skipped.push(config.target_path || config.id);
2802
+ result.skipped.push(config.target_path);
2638
2803
  }
2639
2804
  }
2640
2805
  return result;
@@ -2656,29 +2821,77 @@ function diffConfig(config) {
2656
2821
  const maxLen = Math.max(stored.length, disk.length);
2657
2822
  for (let i = 0;i < maxLen; i++) {
2658
2823
  const s = stored[i];
2659
- const d = disk[i];
2660
- if (s === d) {
2824
+ const dk = disk[i];
2825
+ if (s === dk) {
2661
2826
  if (s !== undefined)
2662
2827
  lines.push(` ${s}`);
2663
2828
  } else {
2664
2829
  if (s !== undefined)
2665
2830
  lines.push(`-${s}`);
2666
- if (d !== undefined)
2667
- lines.push(`+${d}`);
2831
+ if (dk !== undefined)
2832
+ lines.push(`+${dk}`);
2668
2833
  }
2669
2834
  }
2670
2835
  return lines.join(`
2671
2836
  `);
2672
2837
  }
2838
+ function detectCategory(filePath) {
2839
+ const p = filePath.toLowerCase().replace(homedir2(), "~");
2840
+ if (p.includes("/.claude/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("gemini.md"))
2841
+ return "rules";
2842
+ if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.gemini/") || p.includes("/.cursor/"))
2843
+ return "agent";
2844
+ if (p.includes(".mcp.json") || p.includes("mcp"))
2845
+ return "mcp";
2846
+ if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc") || p.includes(".bash_profile"))
2847
+ return "shell";
2848
+ if (p.includes(".gitconfig") || p.includes(".gitignore"))
2849
+ return "git";
2850
+ if (p.includes(".npmrc") || p.includes("tsconfig") || p.includes("bunfig"))
2851
+ return "tools";
2852
+ if (p.includes(".secrets"))
2853
+ return "secrets_schema";
2854
+ return "tools";
2855
+ }
2856
+ function detectAgent(filePath) {
2857
+ const p = filePath.toLowerCase().replace(homedir2(), "~");
2858
+ if (p.includes("/.claude/") || p.endsWith("claude.md"))
2859
+ return "claude";
2860
+ if (p.includes("/.codex/") || p.endsWith("agents.md"))
2861
+ return "codex";
2862
+ if (p.includes("/.gemini/") || p.endsWith("gemini.md"))
2863
+ return "gemini";
2864
+ if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc"))
2865
+ return "zsh";
2866
+ if (p.includes(".gitconfig") || p.includes(".gitignore"))
2867
+ return "git";
2868
+ if (p.includes(".npmrc"))
2869
+ return "npm";
2870
+ return "global";
2871
+ }
2872
+ function detectFormat(filePath) {
2873
+ const ext = extname(filePath).toLowerCase();
2874
+ if (ext === ".json")
2875
+ return "json";
2876
+ if (ext === ".toml")
2877
+ return "toml";
2878
+ if (ext === ".yaml" || ext === ".yml")
2879
+ return "yaml";
2880
+ if (ext === ".md" || ext === ".markdown")
2881
+ return "markdown";
2882
+ if (ext === ".ini" || ext === ".cfg")
2883
+ return "ini";
2884
+ return "text";
2885
+ }
2673
2886
 
2674
2887
  // src/lib/export.ts
2675
2888
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
2676
- import { join as join3, resolve as resolve4 } from "path";
2889
+ import { join as join3, resolve as resolve3 } from "path";
2677
2890
  import { tmpdir } from "os";
2678
2891
  async function exportConfigs(outputPath, opts = {}) {
2679
2892
  const d = opts.db || getDatabase();
2680
2893
  const configs = listConfigs(opts.filter, d);
2681
- const absOutput = resolve4(outputPath);
2894
+ const absOutput = resolve3(outputPath);
2682
2895
  const tmpDir = join3(tmpdir(), `configs-export-${Date.now()}`);
2683
2896
  const contentsDir = join3(tmpDir, "contents");
2684
2897
  try {
@@ -2712,12 +2925,12 @@ async function exportConfigs(outputPath, opts = {}) {
2712
2925
 
2713
2926
  // src/lib/import.ts
2714
2927
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
2715
- import { join as join4, resolve as resolve5 } from "path";
2928
+ import { join as join4, resolve as resolve4 } from "path";
2716
2929
  import { tmpdir as tmpdir2 } from "os";
2717
2930
  async function importConfigs(bundlePath, opts = {}) {
2718
2931
  const d = opts.db || getDatabase();
2719
2932
  const conflict = opts.conflict ?? "skip";
2720
- const absPath = resolve5(bundlePath);
2933
+ const absPath = resolve4(bundlePath);
2721
2934
  const tmpDir = join4(tmpdir2(), `configs-import-${Date.now()}`);
2722
2935
  const result = { created: 0, updated: 0, skipped: 0, errors: [] };
2723
2936
  try {
@@ -2855,12 +3068,14 @@ program.command("show <id>").description("Show a config's content and metadata")
2855
3068
  }
2856
3069
  });
2857
3070
  program.command("add <path>").description("Ingest a file into the config DB").option("-n, --name <name>", "config name (defaults to filename)").option("-c, --category <cat>", "category override").option("-a, --agent <agent>", "agent override").option("-k, --kind <kind>", "kind: file|reference", "file").option("--template", "mark as template (has {{VAR}} placeholders)").action(async (filePath, opts) => {
2858
- const abs = resolve6(filePath);
3071
+ const abs = resolve5(filePath);
2859
3072
  if (!existsSync6(abs)) {
2860
3073
  console.error(chalk.red(`File not found: ${abs}`));
2861
3074
  process.exit(1);
2862
3075
  }
2863
- const content = readFileSync4(abs, "utf-8");
3076
+ const rawContent = readFileSync4(abs, "utf-8");
3077
+ const fmt = detectFormat(abs);
3078
+ const { content, redacted, isTemplate } = redactContent(rawContent, fmt);
2864
3079
  const targetPath = abs.startsWith(homedir3()) ? abs.replace(homedir3(), "~") : abs;
2865
3080
  const name = opts.name || filePath.split("/").pop();
2866
3081
  const config = createConfig({
@@ -2869,11 +3084,17 @@ program.command("add <path>").description("Ingest a file into the config DB").op
2869
3084
  category: opts.category ?? detectCategory(abs),
2870
3085
  agent: opts.agent ?? detectAgent(abs),
2871
3086
  target_path: opts.kind === "reference" ? null : targetPath,
2872
- format: detectFormat(abs),
3087
+ format: fmt,
2873
3088
  content,
2874
- is_template: opts.template ?? false
3089
+ is_template: (opts.template ?? false) || isTemplate
2875
3090
  });
2876
3091
  console.log(chalk.green("\u2713") + ` Added: ${chalk.bold(config.name)} ${chalk.dim(`(${config.slug})`)}`);
3092
+ if (redacted.length > 0) {
3093
+ console.log(chalk.yellow(` \u26A0 Redacted ${redacted.length} secret(s):`));
3094
+ for (const r of redacted)
3095
+ console.log(chalk.yellow(` line ${r.line}: {{${r.varName}}} \u2014 ${r.reason}`));
3096
+ console.log(chalk.dim(" Config stored as a template. Use `configs template vars` to see placeholders."));
3097
+ }
2877
3098
  });
2878
3099
  program.command("apply <id>").description("Apply a config to its target_path on disk").option("--dry-run", "preview without writing").option("--force", "overwrite even if unchanged").action(async (id, opts) => {
2879
3100
  try {
@@ -2897,14 +3118,30 @@ program.command("diff <id>").description("Show diff between stored config and di
2897
3118
  process.exit(1);
2898
3119
  }
2899
3120
  });
2900
- program.command("sync").description("Bulk sync a directory with the DB").option("-d, --dir <dir>", "directory to sync (default: ~/.claude)", "~/.claude").option("--from-disk", "read files from disk into DB (default)").option("--to-disk", "apply DB configs back to disk").option("--dry-run", "preview without writing").action(async (opts) => {
2901
- const toDisk = opts.toDisk;
2902
- if (toDisk) {
2903
- const result = await syncToDir(opts.dir, { dryRun: opts.dryRun });
2904
- console.log(chalk.green(`\u2713`) + ` Synced to disk: +${result.added} updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
3121
+ program.command("sync").description("Sync known AI coding configs from disk into DB (claude, codex, gemini, zsh, git, npm)").option("-a, --agent <agent>", "only sync configs for this agent (claude|codex|gemini|zsh|git|npm)").option("-c, --category <cat>", "only sync configs in this category").option("--to-disk", "apply DB configs back to disk instead").option("--dry-run", "preview without writing").option("--list", "show which files would be synced without doing anything").action(async (opts) => {
3122
+ if (opts.list) {
3123
+ const targets = KNOWN_CONFIGS.filter((k) => {
3124
+ if (opts.agent && k.agent !== opts.agent)
3125
+ return false;
3126
+ if (opts.category && k.category !== opts.category)
3127
+ return false;
3128
+ return true;
3129
+ });
3130
+ console.log(chalk.bold(`Known configs (${targets.length}):`));
3131
+ for (const k of targets) {
3132
+ console.log(` ${chalk.cyan(k.rulesDir ? k.rulesDir + "/*.md" : k.path)} ${chalk.dim(`[${k.category}/${k.agent}]`)}`);
3133
+ }
3134
+ return;
3135
+ }
3136
+ if (opts.toDisk) {
3137
+ const result = await syncToDisk({ dryRun: opts.dryRun, agent: opts.agent, category: opts.category });
3138
+ console.log(chalk.green("\u2713") + ` Written to disk: updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
2905
3139
  } else {
2906
- const result = await syncFromDir(opts.dir, { dryRun: opts.dryRun });
2907
- console.log(chalk.green(`\u2713`) + ` Synced from disk: +${result.added} updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
3140
+ const result = await syncKnown({ dryRun: opts.dryRun, agent: opts.agent, category: opts.category });
3141
+ console.log(chalk.green("\u2713") + ` Synced: +${result.added} updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
3142
+ if (result.skipped.length > 0) {
3143
+ console.log(chalk.dim(" skipped (not found): " + result.skipped.join(", ")));
3144
+ }
2908
3145
  }
2909
3146
  });
2910
3147
  program.command("export").description("Export configs as a tar.gz bundle").option("-o, --output <path>", "output file", "./configs-export.tar.gz").option("-c, --category <cat>", "filter by category").action(async (opts) => {
@@ -3083,5 +3320,29 @@ templateCmd.command("vars <id>").description("Show template variables").action(a
3083
3320
  process.exit(1);
3084
3321
  }
3085
3322
  });
3323
+ program.command("scan [id]").description("Scan configs for secrets. Omit id to scan all.").option("--fix", "redact found secrets in-place").action(async (id, opts) => {
3324
+ const configs = id ? [getConfig(id)] : listConfigs({ kind: "file" });
3325
+ let total = 0;
3326
+ for (const c of configs) {
3327
+ const secrets = scanSecrets(c.content, c.format);
3328
+ if (secrets.length === 0)
3329
+ continue;
3330
+ total += secrets.length;
3331
+ console.log(chalk.yellow(`\u26A0 ${c.slug}`) + chalk.dim(` \u2014 ${secrets.length} secret(s):`));
3332
+ for (const s of secrets)
3333
+ console.log(` line ${s.line}: ${chalk.red(s.varName)} \u2014 ${s.reason}`);
3334
+ if (opts.fix) {
3335
+ const { content, isTemplate } = redactContent(c.content, c.format);
3336
+ updateConfig(c.id, { content, is_template: isTemplate });
3337
+ console.log(chalk.green(" \u2713 Redacted and updated."));
3338
+ }
3339
+ }
3340
+ if (total === 0) {
3341
+ console.log(chalk.green("\u2713") + " No secrets detected.");
3342
+ } else if (!opts.fix) {
3343
+ console.log(chalk.yellow(`
3344
+ Run with --fix to redact in-place.`));
3345
+ }
3346
+ });
3086
3347
  program.version(pkg.version).name("configs");
3087
3348
  program.parse(process.argv);
package/dist/index.d.ts CHANGED
@@ -6,8 +6,10 @@ export { registerMachine, updateMachineApplied, listMachines, currentHostname, c
6
6
  export { getDatabase, resetDatabase, uuid, now, slugify } from "./db/database.js";
7
7
  export { applyConfig, applyConfigs, expandPath } from "./lib/apply.js";
8
8
  export type { ApplyOptions } from "./lib/apply.js";
9
- export { syncFromDir, syncToDir, diffConfig, detectCategory, detectAgent, detectFormat } from "./lib/sync.js";
10
- export type { SyncFromDirOptions, SyncToDirOptions } from "./lib/sync.js";
9
+ export { syncKnown, syncToDisk, diffConfig, detectCategory, detectAgent, detectFormat, KNOWN_CONFIGS } from "./lib/sync.js";
10
+ export { syncFromDir, syncToDir } from "./lib/sync-dir.js";
11
+ export type { SyncKnownOptions, SyncToDiskOptions } from "./lib/sync.js";
12
+ export type { SyncFromDirOptions } from "./lib/sync-dir.js";
11
13
  export { exportConfigs } from "./lib/export.js";
12
14
  export { importConfigs } from "./lib/import.js";
13
15
  export type { ExportOptions } from "./lib/export.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGlI,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGrH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGzK,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGnH,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAGlF,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACvE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGnD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC9G,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAG1E,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGnE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACvG,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGlI,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGrH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGzK,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGnH,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAGlF,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACvE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGnD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC5H,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACzE,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAG5D,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGnE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACvG,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}