@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/index.js CHANGED
@@ -524,83 +524,199 @@ async function applyConfigs(configs, opts = {}) {
524
524
  return results;
525
525
  }
526
526
  // src/lib/sync.ts
527
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
528
- import { extname, join as join2, relative } from "path";
529
- import { homedir as homedir2 } from "os";
530
- function detectCategory(filePath) {
531
- const p = filePath.toLowerCase().replace(homedir2(), "~");
532
- if (p.includes("/.claude/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("gemini.md"))
533
- return "rules";
534
- if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.gemini/") || p.includes("/.cursor/"))
535
- return "agent";
536
- if (p.includes(".mcp.json") || p.includes("mcp"))
537
- return "mcp";
538
- if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc") || p.includes(".bash_profile"))
539
- return "shell";
540
- if (p.includes(".gitconfig") || p.includes(".gitignore"))
541
- return "git";
542
- if (p.includes(".npmrc") || p.includes("tsconfig") || p.includes("bunfig"))
543
- return "tools";
544
- if (p.includes(".secrets"))
545
- return "secrets_schema";
546
- return "tools";
547
- }
548
- function detectAgent(filePath) {
549
- const p = filePath.toLowerCase().replace(homedir2(), "~");
550
- if (p.includes("/.claude/") || p.endsWith("claude.md"))
551
- return "claude";
552
- if (p.includes("/.codex/") || p.endsWith("agents.md"))
553
- return "codex";
554
- if (p.includes("/.gemini/") || p.endsWith("gemini.md"))
555
- return "gemini";
556
- if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc"))
557
- return "zsh";
558
- if (p.includes(".gitconfig") || p.includes(".gitignore"))
559
- return "git";
560
- if (p.includes(".npmrc"))
561
- return "npm";
562
- return "global";
527
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
528
+ import { extname, join as join3 } from "path";
529
+ import { homedir as homedir3 } from "os";
530
+
531
+ // src/lib/redact.ts
532
+ 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;
533
+ var VALUE_PATTERNS = [
534
+ { re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
535
+ { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
536
+ { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
537
+ { re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
538
+ { re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
539
+ { re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
540
+ { re: /ey[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\./, reason: "JWT token" },
541
+ { re: /AKIA[0-9A-Z]{16}/, reason: "AWS access key" }
542
+ ];
543
+ var MIN_SECRET_VALUE_LEN = 8;
544
+ function redactShell(content) {
545
+ const redacted = [];
546
+ const lines = content.split(`
547
+ `);
548
+ const out = [];
549
+ for (let i = 0;i < lines.length; i++) {
550
+ const line = lines[i];
551
+ const m = line.match(/^(\s*(?:export\s+)?)([A-Z][A-Z0-9_]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
552
+ if (m) {
553
+ const [, prefix, key, eq, quote, value] = m;
554
+ if (shouldRedactKeyValue(key, value)) {
555
+ const reason = reasonFor(key, value);
556
+ redacted.push({ varName: key, line: i + 1, reason });
557
+ out.push(`${prefix}${key}${eq}${quote}{{${key}}}${quote}`);
558
+ continue;
559
+ }
560
+ }
561
+ out.push(line);
562
+ }
563
+ return { content: out.join(`
564
+ `), redacted, isTemplate: redacted.length > 0 };
563
565
  }
564
- function detectFormat(filePath) {
565
- const ext = extname(filePath).toLowerCase();
566
- if (ext === ".json")
567
- return "json";
568
- if (ext === ".toml")
569
- return "toml";
570
- if (ext === ".yaml" || ext === ".yml")
571
- return "yaml";
572
- if (ext === ".md" || ext === ".markdown")
573
- return "markdown";
574
- if (ext === ".ini" || ext === ".cfg")
575
- return "ini";
576
- return "text";
566
+ function redactJson(content) {
567
+ const redacted = [];
568
+ const lines = content.split(`
569
+ `);
570
+ const out = [];
571
+ for (let i = 0;i < lines.length; i++) {
572
+ const line = lines[i];
573
+ const m = line.match(/^(\s*"([^"]+)"\s*:\s*)"([^"]+)"(,?)(\s*)$/);
574
+ if (m) {
575
+ const [, prefix, key, value, comma, trail] = m;
576
+ if (shouldRedactKeyValue(key, value)) {
577
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
578
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
579
+ out.push(`${prefix}"{{${varName}}}"${comma}${trail}`);
580
+ continue;
581
+ }
582
+ }
583
+ let newLine = line;
584
+ for (const { re, reason } of VALUE_PATTERNS) {
585
+ newLine = newLine.replace(re, (match) => {
586
+ const varName = `REDACTED_${reason.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
587
+ redacted.push({ varName, line: i + 1, reason });
588
+ return `{{${varName}}}`;
589
+ });
590
+ }
591
+ out.push(newLine);
592
+ }
593
+ return { content: out.join(`
594
+ `), redacted, isTemplate: redacted.length > 0 };
577
595
  }
578
- var SKIP_PATTERNS = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
579
- function shouldSkip(p) {
580
- return SKIP_PATTERNS.some((pat) => p.includes(pat));
596
+ function redactToml(content) {
597
+ const redacted = [];
598
+ const lines = content.split(`
599
+ `);
600
+ const out = [];
601
+ for (let i = 0;i < lines.length; i++) {
602
+ const line = lines[i];
603
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
604
+ if (m) {
605
+ const [, indent, key, eq, quote, value] = m;
606
+ if (shouldRedactKeyValue(key, value)) {
607
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
608
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
609
+ out.push(`${indent}${key}${eq}${quote}{{${varName}}}${quote}`);
610
+ continue;
611
+ }
612
+ }
613
+ out.push(line);
614
+ }
615
+ return { content: out.join(`
616
+ `), redacted, isTemplate: redacted.length > 0 };
581
617
  }
582
- function walkDir(dir, files = []) {
583
- const entries = readdirSync(dir, { withFileTypes: true });
584
- for (const entry of entries) {
585
- const full = join2(dir, entry.name);
586
- if (shouldSkip(full))
618
+ function redactIni(content) {
619
+ const redacted = [];
620
+ const lines = content.split(`
621
+ `);
622
+ const out = [];
623
+ for (let i = 0;i < lines.length; i++) {
624
+ const line = lines[i];
625
+ const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
626
+ if (authM) {
627
+ redacted.push({ varName: "NPM_AUTH_TOKEN", line: i + 1, reason: "npm auth token" });
628
+ out.push(`${authM[1]}{{NPM_AUTH_TOKEN}}`);
587
629
  continue;
588
- if (entry.isDirectory()) {
589
- walkDir(full, files);
590
- } else if (entry.isFile()) {
591
- files.push(full);
592
630
  }
631
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
632
+ if (m) {
633
+ const [, indent, key, eq, value] = m;
634
+ if (shouldRedactKeyValue(key, value)) {
635
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
636
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
637
+ out.push(`${indent}${key}${eq}{{${varName}}}`);
638
+ continue;
639
+ }
640
+ }
641
+ out.push(line);
593
642
  }
594
- return files;
643
+ return { content: out.join(`
644
+ `), redacted, isTemplate: redacted.length > 0 };
645
+ }
646
+ function redactGeneric(content) {
647
+ const redacted = [];
648
+ const lines = content.split(`
649
+ `);
650
+ const out = [];
651
+ for (let i = 0;i < lines.length; i++) {
652
+ let line = lines[i];
653
+ for (const { re, reason } of VALUE_PATTERNS) {
654
+ line = line.replace(re, (match) => {
655
+ const varName = reason.toUpperCase().replace(/[^A-Z0-9]/g, "_");
656
+ redacted.push({ varName, line: i + 1, reason });
657
+ return `{{${varName}}}`;
658
+ });
659
+ }
660
+ out.push(line);
661
+ }
662
+ return { content: out.join(`
663
+ `), redacted, isTemplate: redacted.length > 0 };
664
+ }
665
+ function shouldRedactKeyValue(key, value) {
666
+ if (!value || value.startsWith("{{"))
667
+ return false;
668
+ if (value.length < MIN_SECRET_VALUE_LEN)
669
+ return false;
670
+ if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
671
+ return false;
672
+ if (SECRET_KEY_PATTERN.test(key))
673
+ return true;
674
+ for (const { re } of VALUE_PATTERNS) {
675
+ if (re.test(value))
676
+ return true;
677
+ }
678
+ return false;
679
+ }
680
+ function reasonFor(key, value) {
681
+ if (SECRET_KEY_PATTERN.test(key))
682
+ return `secret key name: ${key}`;
683
+ for (const { re, reason } of VALUE_PATTERNS) {
684
+ if (re.test(value))
685
+ return reason;
686
+ }
687
+ return "secret value pattern";
688
+ }
689
+ function redactContent(content, format) {
690
+ switch (format) {
691
+ case "shell":
692
+ return redactShell(content);
693
+ case "json":
694
+ return redactJson(content);
695
+ case "toml":
696
+ return redactToml(content);
697
+ case "ini":
698
+ return redactIni(content);
699
+ default:
700
+ return redactGeneric(content);
701
+ }
702
+ }
703
+
704
+ // src/lib/sync-dir.ts
705
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
706
+ import { join as join2, relative } from "path";
707
+ import { homedir as homedir2 } from "os";
708
+ var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
709
+ function shouldSkip(p) {
710
+ return SKIP.some((s) => p.includes(s));
595
711
  }
596
712
  async function syncFromDir(dir, opts = {}) {
597
713
  const d = opts.db || getDatabase();
598
714
  const absDir = expandPath(dir);
599
- if (!existsSync3(absDir)) {
600
- return { added: 0, updated: 0, unchanged: 0, skipped: [`Directory not found: ${absDir}`] };
601
- }
715
+ if (!existsSync3(absDir))
716
+ return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
602
717
  const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join2(absDir, f)).filter((f) => statSync(f).isFile());
603
718
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
719
+ const home = homedir2();
604
720
  const allConfigs = listConfigs(undefined, d);
605
721
  for (const file of files) {
606
722
  if (shouldSkip(file)) {
@@ -609,25 +725,19 @@ async function syncFromDir(dir, opts = {}) {
609
725
  }
610
726
  try {
611
727
  const content = readFileSync2(file, "utf-8");
612
- const targetPath = file.startsWith(homedir2()) ? file.replace(homedir2(), "~") : file;
728
+ if (content.length > 500000) {
729
+ result.skipped.push(file + " (too large)");
730
+ continue;
731
+ }
732
+ const targetPath = file.replace(home, "~");
613
733
  const existing = allConfigs.find((c) => c.target_path === targetPath);
614
734
  if (!existing) {
615
- if (!opts.dryRun) {
616
- const name = relative(absDir, file);
617
- createConfig({
618
- name,
619
- category: detectCategory(file),
620
- agent: detectAgent(file),
621
- target_path: targetPath,
622
- format: detectFormat(file),
623
- content
624
- }, d);
625
- }
735
+ if (!opts.dryRun)
736
+ createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content }, d);
626
737
  result.added++;
627
738
  } else if (existing.content !== content) {
628
- if (!opts.dryRun) {
739
+ if (!opts.dryRun)
629
740
  updateConfig(existing.id, { content }, d);
630
- }
631
741
  result.updated++;
632
742
  } else {
633
743
  result.unchanged++;
@@ -640,22 +750,152 @@ async function syncFromDir(dir, opts = {}) {
640
750
  }
641
751
  async function syncToDir(dir, opts = {}) {
642
752
  const d = opts.db || getDatabase();
753
+ const home = homedir2();
643
754
  const absDir = expandPath(dir);
644
- const normalizedDir = dir.startsWith("~/") ? dir : absDir.replace(homedir2(), "~");
645
- const configs = listConfigs(undefined, d).filter((c) => c.target_path && (c.target_path.startsWith(normalizedDir) || c.target_path.startsWith(absDir)));
755
+ const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
756
+ const configs = listConfigs(undefined, d).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
646
757
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
647
758
  for (const config of configs) {
648
759
  if (config.kind === "reference")
649
760
  continue;
650
761
  try {
651
762
  const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
652
- if (r.changed) {
653
- existsSync3(expandPath(config.target_path)) ? result.updated++ : result.added++;
763
+ r.changed ? result.updated++ : result.unchanged++;
764
+ } catch {
765
+ result.skipped.push(config.target_path || config.id);
766
+ }
767
+ }
768
+ return result;
769
+ }
770
+ function walkDir(dir, files = []) {
771
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
772
+ const full = join2(dir, entry.name);
773
+ if (shouldSkip(full))
774
+ continue;
775
+ if (entry.isDirectory())
776
+ walkDir(full, files);
777
+ else if (entry.isFile())
778
+ files.push(full);
779
+ }
780
+ return files;
781
+ }
782
+
783
+ // src/lib/sync.ts
784
+ var KNOWN_CONFIGS = [
785
+ { path: "~/.claude/CLAUDE.md", name: "claude-claude-md", category: "rules", agent: "claude", format: "markdown" },
786
+ { path: "~/.claude/settings.json", name: "claude-settings", category: "agent", agent: "claude", format: "json" },
787
+ { path: "~/.claude/settings.local.json", name: "claude-settings-local", category: "agent", agent: "claude", format: "json" },
788
+ { path: "~/.claude/keybindings.json", name: "claude-keybindings", category: "agent", agent: "claude", format: "json" },
789
+ { path: "~/.claude/rules", name: "claude-rules", category: "rules", agent: "claude", rulesDir: "~/.claude/rules" },
790
+ { path: "~/.codex/config.toml", name: "codex-config", category: "agent", agent: "codex", format: "toml" },
791
+ { path: "~/.codex/AGENTS.md", name: "codex-agents-md", category: "rules", agent: "codex", format: "markdown" },
792
+ { path: "~/.gemini/settings.json", name: "gemini-settings", category: "agent", agent: "gemini", format: "json" },
793
+ { path: "~/.gemini/GEMINI.md", name: "gemini-gemini-md", category: "rules", agent: "gemini", format: "markdown" },
794
+ { path: "~/.claude.json", name: "claude-json", category: "mcp", agent: "claude", format: "json", description: "Claude Code global config (includes MCP server entries)" },
795
+ { path: "~/.zshrc", name: "zshrc", category: "shell", agent: "zsh" },
796
+ { path: "~/.zprofile", name: "zprofile", category: "shell", agent: "zsh" },
797
+ { path: "~/.bashrc", name: "bashrc", category: "shell", agent: "zsh" },
798
+ { path: "~/.bash_profile", name: "bash-profile", category: "shell", agent: "zsh" },
799
+ { path: "~/.gitconfig", name: "gitconfig", category: "git", agent: "git", format: "ini" },
800
+ { path: "~/.gitignore_global", name: "gitignore-global", category: "git", agent: "git" },
801
+ { path: "~/.npmrc", name: "npmrc", category: "tools", agent: "npm", format: "ini" },
802
+ { path: "~/.bunfig.toml", name: "bunfig", category: "tools", agent: "global", format: "toml" }
803
+ ];
804
+ async function syncKnown(opts = {}) {
805
+ const d = opts.db || getDatabase();
806
+ const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
807
+ const home = homedir3();
808
+ let targets = KNOWN_CONFIGS;
809
+ if (opts.agent)
810
+ targets = targets.filter((k) => k.agent === opts.agent);
811
+ if (opts.category)
812
+ targets = targets.filter((k) => k.category === opts.category);
813
+ const allConfigs = listConfigs(undefined, d);
814
+ for (const known of targets) {
815
+ if (known.rulesDir) {
816
+ const absDir = expandPath(known.rulesDir);
817
+ if (!existsSync4(absDir)) {
818
+ result.skipped.push(known.rulesDir);
819
+ continue;
820
+ }
821
+ const mdFiles = readdirSync2(absDir).filter((f) => f.endsWith(".md"));
822
+ for (const f of mdFiles) {
823
+ const abs2 = join3(absDir, f);
824
+ const targetPath = abs2.replace(home, "~");
825
+ const raw = readFileSync3(abs2, "utf-8");
826
+ const { content, isTemplate } = redactContent(raw, "markdown");
827
+ const name = `claude-rules-${f}`;
828
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
829
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
830
+ if (!existing) {
831
+ if (!opts.dryRun)
832
+ createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate }, d);
833
+ result.added++;
834
+ } else if (existing.content !== content) {
835
+ if (!opts.dryRun)
836
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
837
+ result.updated++;
838
+ } else {
839
+ result.unchanged++;
840
+ }
841
+ }
842
+ continue;
843
+ }
844
+ const abs = expandPath(known.path);
845
+ if (!existsSync4(abs)) {
846
+ result.skipped.push(known.path);
847
+ continue;
848
+ }
849
+ try {
850
+ const rawContent = readFileSync3(abs, "utf-8");
851
+ if (rawContent.length > 500000) {
852
+ result.skipped.push(known.path + " (too large)");
853
+ continue;
854
+ }
855
+ const fmt = known.format ?? detectFormat(abs);
856
+ const { content, isTemplate } = redactContent(rawContent, fmt);
857
+ const targetPath = abs.replace(home, "~");
858
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
859
+ if (!existing) {
860
+ if (!opts.dryRun) {
861
+ createConfig({
862
+ name: known.name,
863
+ category: known.category,
864
+ agent: known.agent,
865
+ format: fmt,
866
+ content,
867
+ target_path: known.kind === "reference" ? null : targetPath,
868
+ kind: known.kind ?? "file",
869
+ description: known.description,
870
+ is_template: isTemplate
871
+ }, d);
872
+ }
873
+ result.added++;
874
+ } else if (existing.content !== content) {
875
+ if (!opts.dryRun)
876
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
877
+ result.updated++;
654
878
  } else {
655
879
  result.unchanged++;
656
880
  }
657
881
  } catch {
658
- result.skipped.push(config.target_path || config.id);
882
+ result.skipped.push(known.path);
883
+ }
884
+ }
885
+ return result;
886
+ }
887
+ async function syncToDisk(opts = {}) {
888
+ const d = opts.db || getDatabase();
889
+ const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
890
+ let configs = listConfigs({ kind: "file", ...opts.agent ? { agent: opts.agent } : {}, ...opts.category ? { category: opts.category } : {} }, d);
891
+ for (const config of configs) {
892
+ if (!config.target_path)
893
+ continue;
894
+ try {
895
+ const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
896
+ r.changed ? result.updated++ : result.unchanged++;
897
+ } catch {
898
+ result.skipped.push(config.target_path);
659
899
  }
660
900
  }
661
901
  return result;
@@ -664,9 +904,9 @@ function diffConfig(config) {
664
904
  if (!config.target_path)
665
905
  return "(reference \u2014 no target path)";
666
906
  const path = expandPath(config.target_path);
667
- if (!existsSync3(path))
907
+ if (!existsSync4(path))
668
908
  return `(file not found on disk: ${path})`;
669
- const diskContent = readFileSync2(path, "utf-8");
909
+ const diskContent = readFileSync3(path, "utf-8");
670
910
  if (diskContent === config.content)
671
911
  return "(no diff \u2014 identical)";
672
912
  const stored = config.content.split(`
@@ -677,30 +917,78 @@ function diffConfig(config) {
677
917
  const maxLen = Math.max(stored.length, disk.length);
678
918
  for (let i = 0;i < maxLen; i++) {
679
919
  const s = stored[i];
680
- const d = disk[i];
681
- if (s === d) {
920
+ const dk = disk[i];
921
+ if (s === dk) {
682
922
  if (s !== undefined)
683
923
  lines.push(` ${s}`);
684
924
  } else {
685
925
  if (s !== undefined)
686
926
  lines.push(`-${s}`);
687
- if (d !== undefined)
688
- lines.push(`+${d}`);
927
+ if (dk !== undefined)
928
+ lines.push(`+${dk}`);
689
929
  }
690
930
  }
691
931
  return lines.join(`
692
932
  `);
693
933
  }
934
+ function detectCategory(filePath) {
935
+ const p = filePath.toLowerCase().replace(homedir3(), "~");
936
+ if (p.includes("/.claude/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("gemini.md"))
937
+ return "rules";
938
+ if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.gemini/") || p.includes("/.cursor/"))
939
+ return "agent";
940
+ if (p.includes(".mcp.json") || p.includes("mcp"))
941
+ return "mcp";
942
+ if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc") || p.includes(".bash_profile"))
943
+ return "shell";
944
+ if (p.includes(".gitconfig") || p.includes(".gitignore"))
945
+ return "git";
946
+ if (p.includes(".npmrc") || p.includes("tsconfig") || p.includes("bunfig"))
947
+ return "tools";
948
+ if (p.includes(".secrets"))
949
+ return "secrets_schema";
950
+ return "tools";
951
+ }
952
+ function detectAgent(filePath) {
953
+ const p = filePath.toLowerCase().replace(homedir3(), "~");
954
+ if (p.includes("/.claude/") || p.endsWith("claude.md"))
955
+ return "claude";
956
+ if (p.includes("/.codex/") || p.endsWith("agents.md"))
957
+ return "codex";
958
+ if (p.includes("/.gemini/") || p.endsWith("gemini.md"))
959
+ return "gemini";
960
+ if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc"))
961
+ return "zsh";
962
+ if (p.includes(".gitconfig") || p.includes(".gitignore"))
963
+ return "git";
964
+ if (p.includes(".npmrc"))
965
+ return "npm";
966
+ return "global";
967
+ }
968
+ function detectFormat(filePath) {
969
+ const ext = extname(filePath).toLowerCase();
970
+ if (ext === ".json")
971
+ return "json";
972
+ if (ext === ".toml")
973
+ return "toml";
974
+ if (ext === ".yaml" || ext === ".yml")
975
+ return "yaml";
976
+ if (ext === ".md" || ext === ".markdown")
977
+ return "markdown";
978
+ if (ext === ".ini" || ext === ".cfg")
979
+ return "ini";
980
+ return "text";
981
+ }
694
982
  // src/lib/export.ts
695
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
696
- import { join as join3, resolve as resolve4 } from "path";
983
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
984
+ import { join as join4, resolve as resolve3 } from "path";
697
985
  import { tmpdir } from "os";
698
986
  async function exportConfigs(outputPath, opts = {}) {
699
987
  const d = opts.db || getDatabase();
700
988
  const configs = listConfigs(opts.filter, d);
701
- const absOutput = resolve4(outputPath);
702
- const tmpDir = join3(tmpdir(), `configs-export-${Date.now()}`);
703
- const contentsDir = join3(tmpDir, "contents");
989
+ const absOutput = resolve3(outputPath);
990
+ const tmpDir = join4(tmpdir(), `configs-export-${Date.now()}`);
991
+ const contentsDir = join4(tmpDir, "contents");
704
992
  try {
705
993
  mkdirSync3(contentsDir, { recursive: true });
706
994
  const manifest = {
@@ -708,10 +996,10 @@ async function exportConfigs(outputPath, opts = {}) {
708
996
  exported_at: now(),
709
997
  configs: configs.map(({ content: _content, ...meta }) => meta)
710
998
  };
711
- writeFileSync2(join3(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
999
+ writeFileSync2(join4(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
712
1000
  for (const config of configs) {
713
1001
  const fileName = `${config.slug}.${config.format === "text" ? "txt" : config.format}`;
714
- writeFileSync2(join3(contentsDir, fileName), config.content, "utf-8");
1002
+ writeFileSync2(join4(contentsDir, fileName), config.content, "utf-8");
715
1003
  }
716
1004
  const proc = Bun.spawn(["tar", "czf", absOutput, "-C", tmpDir, "."], {
717
1005
  stdout: "pipe",
@@ -724,20 +1012,20 @@ async function exportConfigs(outputPath, opts = {}) {
724
1012
  }
725
1013
  return { path: absOutput, count: configs.length };
726
1014
  } finally {
727
- if (existsSync4(tmpDir)) {
1015
+ if (existsSync5(tmpDir)) {
728
1016
  rmSync(tmpDir, { recursive: true, force: true });
729
1017
  }
730
1018
  }
731
1019
  }
732
1020
  // src/lib/import.ts
733
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
734
- import { join as join4, resolve as resolve5 } from "path";
1021
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync as rmSync2 } from "fs";
1022
+ import { join as join5, resolve as resolve4 } from "path";
735
1023
  import { tmpdir as tmpdir2 } from "os";
736
1024
  async function importConfigs(bundlePath, opts = {}) {
737
1025
  const d = opts.db || getDatabase();
738
1026
  const conflict = opts.conflict ?? "skip";
739
- const absPath = resolve5(bundlePath);
740
- const tmpDir = join4(tmpdir2(), `configs-import-${Date.now()}`);
1027
+ const absPath = resolve4(bundlePath);
1028
+ const tmpDir = join5(tmpdir2(), `configs-import-${Date.now()}`);
741
1029
  const result = { created: 0, updated: 0, skipped: 0, errors: [] };
742
1030
  try {
743
1031
  mkdirSync4(tmpDir, { recursive: true });
@@ -750,15 +1038,15 @@ async function importConfigs(bundlePath, opts = {}) {
750
1038
  const stderr = await new Response(proc.stderr).text();
751
1039
  throw new Error(`tar extraction failed: ${stderr}`);
752
1040
  }
753
- const manifestPath = join4(tmpDir, "manifest.json");
754
- if (!existsSync5(manifestPath))
1041
+ const manifestPath = join5(tmpDir, "manifest.json");
1042
+ if (!existsSync6(manifestPath))
755
1043
  throw new Error("Invalid bundle: missing manifest.json");
756
- const manifest = JSON.parse(readFileSync3(manifestPath, "utf-8"));
1044
+ const manifest = JSON.parse(readFileSync4(manifestPath, "utf-8"));
757
1045
  for (const meta of manifest.configs) {
758
1046
  try {
759
1047
  const ext = meta.format === "text" ? "txt" : meta.format;
760
- const contentFile = join4(tmpDir, "contents", `${meta.slug}.${ext}`);
761
- const content = existsSync5(contentFile) ? readFileSync3(contentFile, "utf-8") : "";
1048
+ const contentFile = join5(tmpDir, "contents", `${meta.slug}.${ext}`);
1049
+ const content = existsSync6(contentFile) ? readFileSync4(contentFile, "utf-8") : "";
762
1050
  let existing = null;
763
1051
  try {
764
1052
  existing = getConfig(meta.slug, d);
@@ -791,7 +1079,7 @@ async function importConfigs(bundlePath, opts = {}) {
791
1079
  }
792
1080
  return result;
793
1081
  } finally {
794
- if (existsSync5(tmpDir)) {
1082
+ if (existsSync6(tmpDir)) {
795
1083
  rmSync2(tmpDir, { recursive: true, force: true });
796
1084
  }
797
1085
  }
@@ -844,7 +1132,9 @@ export {
844
1132
  updateProfile,
845
1133
  updateMachineApplied,
846
1134
  updateConfig,
1135
+ syncToDisk,
847
1136
  syncToDir,
1137
+ syncKnown,
848
1138
  syncFromDir,
849
1139
  slugify,
850
1140
  resetDatabase,
@@ -887,6 +1177,7 @@ export {
887
1177
  addConfigToProfile,
888
1178
  TemplateRenderError,
889
1179
  ProfileNotFoundError,
1180
+ KNOWN_CONFIGS,
890
1181
  ConfigNotFoundError,
891
1182
  ConfigApplyError,
892
1183
  CONFIG_KINDS,
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Secret redaction engine.
3
+ *
4
+ * Detects and replaces sensitive values with {{VARNAME}} template placeholders
5
+ * so they are NEVER stored in the DB. The config becomes a template that can
6
+ * be rendered with real values at apply-time.
7
+ *
8
+ * Strategy:
9
+ * 1. Key-name matching — if an assignment's LHS looks like a secret key name
10
+ * 2. Value-pattern matching — known token formats (npm, GitHub, Anthropic, etc.)
11
+ * regardless of key name
12
+ */
13
+ export interface RedactResult {
14
+ content: string;
15
+ redacted: RedactedVar[];
16
+ isTemplate: boolean;
17
+ }
18
+ export interface RedactedVar {
19
+ varName: string;
20
+ line: number;
21
+ reason: string;
22
+ }
23
+ export type RedactFormat = "shell" | "json" | "toml" | "ini" | "markdown" | "text" | "yaml";
24
+ export declare function redactContent(content: string, format: RedactFormat): RedactResult;
25
+ /** Detect secrets without modifying content. Returns list of findings. */
26
+ export declare function scanSecrets(content: string, format: RedactFormat): RedactedVar[];
27
+ /** Returns true if content contains any detectable secrets. */
28
+ export declare function hasSecrets(content: string, format: RedactFormat): boolean;
29
+ //# sourceMappingURL=redact.d.ts.map