@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 +376 -115
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +399 -108
- package/dist/lib/redact.d.ts +29 -0
- package/dist/lib/redact.d.ts.map +1 -0
- package/dist/lib/redact.test.d.ts +2 -0
- package/dist/lib/redact.test.d.ts.map +1 -0
- package/dist/lib/sync-dir.d.ts +13 -0
- package/dist/lib/sync-dir.d.ts.map +1 -0
- package/dist/lib/sync.d.ts +24 -8
- package/dist/lib/sync.d.ts.map +1 -1
- package/dist/mcp/index.js +82 -88
- package/dist/server/index.js +82 -88
- package/package.json +1 -1
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
|
|
528
|
-
import { extname, join as
|
|
529
|
-
import { homedir as
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
|
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: [`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
645
|
-
const configs = listConfigs(undefined, d).filter((c) => c.target_path && (c.target_path.startsWith(
|
|
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
|
-
|
|
653
|
-
|
|
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(
|
|
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 (!
|
|
907
|
+
if (!existsSync4(path))
|
|
668
908
|
return `(file not found on disk: ${path})`;
|
|
669
|
-
const diskContent =
|
|
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
|
|
681
|
-
if (s ===
|
|
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 (
|
|
688
|
-
lines.push(`+${
|
|
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
|
|
696
|
-
import { join as
|
|
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 =
|
|
702
|
-
const tmpDir =
|
|
703
|
-
const contentsDir =
|
|
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(
|
|
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(
|
|
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 (
|
|
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
|
|
734
|
-
import { join as
|
|
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 =
|
|
740
|
-
const tmpDir =
|
|
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 =
|
|
754
|
-
if (!
|
|
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(
|
|
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 =
|
|
761
|
-
const content =
|
|
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 (
|
|
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
|