@hasna/configs 0.1.5 → 0.2.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/cli/index.js CHANGED
@@ -2127,6 +2127,9 @@ function getDatabase(path) {
2127
2127
  _db = db;
2128
2128
  return db;
2129
2129
  }
2130
+ function resetDatabase() {
2131
+ _db = null;
2132
+ }
2130
2133
  function applyMigrations(db) {
2131
2134
  let currentVersion = 0;
2132
2135
  try {
@@ -2715,17 +2718,81 @@ var exports_sync = {};
2715
2718
  __export(exports_sync, {
2716
2719
  syncToDisk: () => syncToDisk,
2717
2720
  syncToDir: () => syncToDir,
2721
+ syncProject: () => syncProject,
2718
2722
  syncKnown: () => syncKnown,
2719
2723
  syncFromDir: () => syncFromDir,
2720
2724
  diffConfig: () => diffConfig,
2721
2725
  detectFormat: () => detectFormat,
2722
2726
  detectCategory: () => detectCategory,
2723
2727
  detectAgent: () => detectAgent,
2728
+ PROJECT_CONFIG_FILES: () => PROJECT_CONFIG_FILES,
2724
2729
  KNOWN_CONFIGS: () => KNOWN_CONFIGS
2725
2730
  });
2726
2731
  import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
2727
2732
  import { extname, join as join3 } from "path";
2728
2733
  import { homedir as homedir3 } from "os";
2734
+ async function syncProject(opts) {
2735
+ const d = opts.db || getDatabase();
2736
+ const absDir = expandPath(opts.projectDir);
2737
+ const projectName = absDir.split("/").pop() || "project";
2738
+ const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
2739
+ const allConfigs = listConfigs(undefined, d);
2740
+ for (const pf of PROJECT_CONFIG_FILES) {
2741
+ const abs = join3(absDir, pf.file);
2742
+ if (!existsSync4(abs))
2743
+ continue;
2744
+ try {
2745
+ const rawContent = readFileSync3(abs, "utf-8");
2746
+ if (rawContent.length > 500000) {
2747
+ result.skipped.push(pf.file);
2748
+ continue;
2749
+ }
2750
+ const { content, isTemplate } = redactContent(rawContent, pf.format);
2751
+ const name = `${projectName}/${pf.file}`;
2752
+ const targetPath = abs.replace(homedir3(), "~");
2753
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2754
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
2755
+ if (!existing) {
2756
+ if (!opts.dryRun)
2757
+ createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate }, d);
2758
+ result.added++;
2759
+ } else if (existing.content !== content) {
2760
+ if (!opts.dryRun)
2761
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
2762
+ result.updated++;
2763
+ } else {
2764
+ result.unchanged++;
2765
+ }
2766
+ } catch {
2767
+ result.skipped.push(pf.file);
2768
+ }
2769
+ }
2770
+ const rulesDir = join3(absDir, ".claude", "rules");
2771
+ if (existsSync4(rulesDir)) {
2772
+ const mdFiles = readdirSync2(rulesDir).filter((f) => f.endsWith(".md"));
2773
+ for (const f of mdFiles) {
2774
+ const abs = join3(rulesDir, f);
2775
+ const raw = readFileSync3(abs, "utf-8");
2776
+ const { content, isTemplate } = redactContent(raw, "markdown");
2777
+ const name = `${projectName}/rules/${f}`;
2778
+ const targetPath = abs.replace(homedir3(), "~");
2779
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2780
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
2781
+ if (!existing) {
2782
+ if (!opts.dryRun)
2783
+ createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate }, d);
2784
+ result.added++;
2785
+ } else if (existing.content !== content) {
2786
+ if (!opts.dryRun)
2787
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
2788
+ result.updated++;
2789
+ } else {
2790
+ result.unchanged++;
2791
+ }
2792
+ }
2793
+ }
2794
+ return result;
2795
+ }
2729
2796
  async function syncKnown(opts = {}) {
2730
2797
  const d = opts.db || getDatabase();
2731
2798
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
@@ -2904,7 +2971,7 @@ function detectFormat(filePath) {
2904
2971
  return "ini";
2905
2972
  return "text";
2906
2973
  }
2907
- var KNOWN_CONFIGS;
2974
+ var KNOWN_CONFIGS, PROJECT_CONFIG_FILES;
2908
2975
  var init_sync = __esm(() => {
2909
2976
  init_database();
2910
2977
  init_configs();
@@ -2931,6 +2998,15 @@ var init_sync = __esm(() => {
2931
2998
  { path: "~/.npmrc", name: "npmrc", category: "tools", agent: "npm", format: "ini" },
2932
2999
  { path: "~/.bunfig.toml", name: "bunfig", category: "tools", agent: "global", format: "toml" }
2933
3000
  ];
3001
+ PROJECT_CONFIG_FILES = [
3002
+ { file: "CLAUDE.md", category: "rules", agent: "claude", format: "markdown" },
3003
+ { file: ".claude/settings.json", category: "agent", agent: "claude", format: "json" },
3004
+ { file: ".claude/settings.local.json", category: "agent", agent: "claude", format: "json" },
3005
+ { file: ".mcp.json", category: "mcp", agent: "claude", format: "json" },
3006
+ { file: "AGENTS.md", category: "rules", agent: "codex", format: "markdown" },
3007
+ { file: ".codex/AGENTS.md", category: "rules", agent: "codex", format: "markdown" },
3008
+ { file: "GEMINI.md", category: "rules", agent: "gemini", format: "markdown" }
3009
+ ];
2934
3010
  });
2935
3011
 
2936
3012
  // node_modules/commander/esm.mjs
@@ -3022,6 +3098,7 @@ function getProfileConfigs(profileIdOrSlug, db) {
3022
3098
 
3023
3099
  // src/cli/index.tsx
3024
3100
  init_snapshots();
3101
+ init_database();
3025
3102
  init_apply();
3026
3103
  init_sync();
3027
3104
  init_redact();
@@ -3171,7 +3248,8 @@ function fmtConfig(c, format) {
3171
3248
  ].filter(Boolean).join(`
3172
3249
  `);
3173
3250
  }
3174
- program.command("list").alias("ls").description("List stored configs").option("-c, --category <cat>", "filter by category").option("-a, --agent <agent>", "filter by agent").option("-k, --kind <kind>", "filter by kind (file|reference)").option("-t, --tag <tag>", "filter by tag").option("-s, --search <query>", "search name/description/content").option("-f, --format <fmt>", "output format: table|json|compact", "table").action(async (opts) => {
3251
+ program.command("list").alias("ls").description("List stored configs").option("-c, --category <cat>", "filter by category").option("-a, --agent <agent>", "filter by agent").option("-k, --kind <kind>", "filter by kind (file|reference)").option("-t, --tag <tag>", "filter by tag").option("-s, --search <query>", "search name/description/content").option("-f, --format <fmt>", "output format: table|json|compact", "table").option("--brief", "shorthand for --format compact").action(async (opts) => {
3252
+ const fmt = opts.brief ? "compact" : opts.format;
3175
3253
  const configs = listConfigs({
3176
3254
  category: opts.category,
3177
3255
  agent: opts.agent,
@@ -3183,13 +3261,13 @@ program.command("list").alias("ls").description("List stored configs").option("-
3183
3261
  console.log(chalk.dim("No configs found."));
3184
3262
  return;
3185
3263
  }
3186
- if (opts.format === "json") {
3264
+ if (fmt === "json") {
3187
3265
  console.log(JSON.stringify(configs, null, 2));
3188
3266
  return;
3189
3267
  }
3190
3268
  for (const c of configs) {
3191
- console.log(fmtConfig(c, opts.format));
3192
- if (opts.format === "table")
3269
+ console.log(fmtConfig(c, fmt));
3270
+ if (fmt === "table")
3193
3271
  console.log();
3194
3272
  }
3195
3273
  console.log(chalk.dim(`${configs.length} config(s)`));
@@ -3256,17 +3334,33 @@ program.command("apply <id>").description("Apply a config to its target_path on
3256
3334
  process.exit(1);
3257
3335
  }
3258
3336
  });
3259
- program.command("diff <id>").description("Show diff between stored config and disk").action(async (id) => {
3337
+ program.command("diff [id]").description("Show diff between stored config and disk (omit id for --all)").option("--all", "diff every known config against disk").action(async (id, opts) => {
3260
3338
  try {
3261
- const config = getConfig(id);
3262
- const diff = diffConfig(config);
3263
- console.log(diff);
3339
+ if (id) {
3340
+ const config = getConfig(id);
3341
+ console.log(diffConfig(config));
3342
+ return;
3343
+ }
3344
+ const configs = listConfigs({ kind: "file" });
3345
+ let drifted = 0;
3346
+ for (const c of configs) {
3347
+ if (!c.target_path)
3348
+ continue;
3349
+ const diff = diffConfig(c);
3350
+ if (diff.includes("no diff") || diff.includes("not found"))
3351
+ continue;
3352
+ drifted++;
3353
+ console.log(chalk.bold(c.slug) + chalk.dim(` (${c.target_path})`));
3354
+ console.log(diff);
3355
+ console.log();
3356
+ }
3357
+ console.log(chalk.dim(`${drifted}/${configs.length} drifted`));
3264
3358
  } catch (e) {
3265
3359
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
3266
3360
  process.exit(1);
3267
3361
  }
3268
3362
  });
3269
- 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) => {
3363
+ 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("-p, --project [dir]", "sync project-scoped configs (CLAUDE.md, .mcp.json, etc.) from a project dir").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) => {
3270
3364
  if (opts.list) {
3271
3365
  const targets = KNOWN_CONFIGS.filter((k) => {
3272
3366
  if (opts.agent && k.agent !== opts.agent)
@@ -3281,6 +3375,12 @@ program.command("sync").description("Sync known AI coding configs from disk into
3281
3375
  }
3282
3376
  return;
3283
3377
  }
3378
+ if (opts.project) {
3379
+ const dir = typeof opts.project === "string" ? opts.project : process.cwd();
3380
+ const result = await syncProject({ projectDir: dir, dryRun: opts.dryRun });
3381
+ console.log(chalk.green("\u2713") + ` Project sync: +${result.added} updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
3382
+ return;
3383
+ }
3284
3384
  if (opts.toDisk) {
3285
3385
  const result = await syncToDisk({ dryRun: opts.dryRun, agent: opts.agent, category: opts.category });
3286
3386
  console.log(chalk.green("\u2713") + ` Written to disk: updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
@@ -3332,13 +3432,22 @@ program.command("whoami").description("Show setup summary").action(async () => {
3332
3432
  }
3333
3433
  });
3334
3434
  var profileCmd = program.command("profile").description("Manage config profiles (named bundles)");
3335
- profileCmd.command("list").description("List all profiles").action(async () => {
3435
+ profileCmd.command("list").description("List all profiles").option("--brief", "compact one-line output").option("-f, --format <fmt>", "table|json|compact", "table").action(async (opts) => {
3436
+ const fmt = opts.brief ? "compact" : opts.format;
3336
3437
  const profiles = listProfiles();
3337
3438
  if (profiles.length === 0) {
3338
3439
  console.log(chalk.dim("No profiles."));
3339
3440
  return;
3340
3441
  }
3442
+ if (fmt === "json") {
3443
+ console.log(JSON.stringify(profiles, null, 2));
3444
+ return;
3445
+ }
3341
3446
  for (const p of profiles) {
3447
+ if (fmt === "compact") {
3448
+ console.log(`${p.slug} ${getProfileConfigs(p.id).length} configs`);
3449
+ continue;
3450
+ }
3342
3451
  const configs = getProfileConfigs(p.id);
3343
3452
  console.log(`${chalk.bold(p.name)} ${chalk.dim(`(${p.slug})`)} \u2014 ${configs.length} config(s)`);
3344
3453
  if (p.description)
@@ -3518,5 +3627,260 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
3518
3627
  Run with --fix to redact in-place.`));
3519
3628
  }
3520
3629
  });
3630
+ var mcpCmd = program.command("mcp").description("Install/remove MCP server for AI agents");
3631
+ mcpCmd.command("install").alias("add").description("Install configs MCP server into an agent").option("--claude", "install into Claude Code").option("--codex", "install into Codex").option("--gemini", "install into Gemini").option("--all", "install into all agents").action(async (opts) => {
3632
+ const targets = opts.all ? ["claude", "codex", "gemini"] : [
3633
+ ...opts.claude ? ["claude"] : [],
3634
+ ...opts.codex ? ["codex"] : [],
3635
+ ...opts.gemini ? ["gemini"] : []
3636
+ ];
3637
+ if (targets.length === 0) {
3638
+ console.log(chalk.dim("Specify --claude, --codex, --gemini, or --all"));
3639
+ return;
3640
+ }
3641
+ for (const target of targets) {
3642
+ try {
3643
+ if (target === "claude") {
3644
+ const proc = Bun.spawn(["claude", "mcp", "add", "--transport", "stdio", "--scope", "user", "configs", "--", "configs-mcp"], { stdout: "inherit", stderr: "inherit" });
3645
+ await proc.exited;
3646
+ console.log(chalk.green("\u2713") + " Installed into Claude Code");
3647
+ } else if (target === "codex") {
3648
+ const { appendFileSync, existsSync: ex } = await import("fs");
3649
+ const { join: j } = await import("path");
3650
+ const configPath = j(homedir4(), ".codex", "config.toml");
3651
+ const block = `
3652
+ [mcp_servers.configs]
3653
+ command = "configs-mcp"
3654
+ args = []
3655
+ `;
3656
+ if (ex(configPath)) {
3657
+ const content = readFileSync5(configPath, "utf-8");
3658
+ if (content.includes("[mcp_servers.configs]")) {
3659
+ console.log(chalk.dim("= Already installed in Codex"));
3660
+ continue;
3661
+ }
3662
+ }
3663
+ appendFileSync(configPath, block);
3664
+ console.log(chalk.green("\u2713") + " Installed into Codex");
3665
+ } else if (target === "gemini") {
3666
+ const { readFileSync: rf, writeFileSync: wf, existsSync: ex } = await import("fs");
3667
+ const { join: j } = await import("path");
3668
+ const configPath = j(homedir4(), ".gemini", "settings.json");
3669
+ let settings = {};
3670
+ if (ex(configPath)) {
3671
+ try {
3672
+ settings = JSON.parse(rf(configPath, "utf-8"));
3673
+ } catch {}
3674
+ }
3675
+ const mcpServers = settings["mcpServers"] ?? {};
3676
+ mcpServers["configs"] = { command: "configs-mcp", args: [] };
3677
+ settings["mcpServers"] = mcpServers;
3678
+ wf(configPath, JSON.stringify(settings, null, 2) + `
3679
+ `, "utf-8");
3680
+ console.log(chalk.green("\u2713") + " Installed into Gemini");
3681
+ }
3682
+ } catch (e) {
3683
+ console.error(chalk.red(`\u2717 Failed to install into ${target}: ${e instanceof Error ? e.message : String(e)}`));
3684
+ }
3685
+ }
3686
+ });
3687
+ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP server from agents").option("--claude", "remove from Claude Code").option("--all", "remove from all agents").action(async (opts) => {
3688
+ if (opts.claude || opts.all) {
3689
+ const proc = Bun.spawn(["claude", "mcp", "remove", "configs"], { stdout: "inherit", stderr: "inherit" });
3690
+ await proc.exited;
3691
+ console.log(chalk.green("\u2713") + " Removed from Claude Code");
3692
+ }
3693
+ });
3694
+ program.command("init").description("First-time setup: sync all known configs, create default profile").option("--force", "delete existing DB and start fresh").action(async (opts) => {
3695
+ const dbPath = join6(homedir4(), ".configs", "configs.db");
3696
+ if (opts.force && existsSync7(dbPath)) {
3697
+ const { rmSync: rmSync3 } = await import("fs");
3698
+ rmSync3(dbPath);
3699
+ console.log(chalk.dim("Deleted existing DB."));
3700
+ resetDatabase();
3701
+ }
3702
+ console.log(chalk.bold(`@hasna/configs \u2014 initializing
3703
+ `));
3704
+ const result = await syncKnown({});
3705
+ console.log(chalk.green("\u2713") + ` Synced: +${result.added} updated:${result.updated} unchanged:${result.unchanged}`);
3706
+ if (result.skipped.length > 0) {
3707
+ console.log(chalk.dim(" skipped: " + result.skipped.join(", ")));
3708
+ }
3709
+ const refs = [
3710
+ { slug: "workspace-structure", name: "Workspace Structure", category: "workspace", content: `# Workspace Structure
3711
+
3712
+ See ~/.claude/rules/workspace.md for full conventions.`, desc: "~/Workspace/ hierarchy and naming" },
3713
+ { slug: "secrets-schema", name: "Secrets Schema", category: "secrets_schema", content: `# .secrets Schema
3714
+
3715
+ Location: ~/.secrets (sourced by ~/.zshrc)
3716
+ Format: export KEY_NAME="value"
3717
+
3718
+ Keys: ANTHROPIC_API_KEY, OPENAI_API_KEY, EXA_API_KEY, NPM_TOKEN, GITHUB_TOKEN`, desc: "Shape of ~/.secrets (no values)" }
3719
+ ];
3720
+ for (const ref of refs) {
3721
+ try {
3722
+ getConfig(ref.slug);
3723
+ } catch {
3724
+ createConfig({ name: ref.name, category: ref.category, agent: "global", format: "markdown", content: ref.content, kind: "reference", description: ref.desc });
3725
+ }
3726
+ }
3727
+ try {
3728
+ getProfile("my-setup");
3729
+ } catch {
3730
+ const p = createProfile({ name: "my-setup", description: "Default profile with all known configs" });
3731
+ const allConfigs = listConfigs();
3732
+ for (const c of allConfigs)
3733
+ addConfigToProfile(p.id, c.id);
3734
+ console.log(chalk.green("\u2713") + ` Created profile "my-setup" with ${allConfigs.length} configs`);
3735
+ }
3736
+ const stats = getConfigStats();
3737
+ console.log(chalk.bold(`
3738
+ DB stats:`));
3739
+ for (const [key, count] of Object.entries(stats)) {
3740
+ if (count > 0)
3741
+ console.log(` ${key.padEnd(18)} ${count}`);
3742
+ }
3743
+ console.log(chalk.dim(`
3744
+ DB: ${dbPath}`));
3745
+ });
3746
+ program.command("status").description("Health check: total configs, drift from disk, unredacted secrets").action(async () => {
3747
+ const dbPath = join6(homedir4(), ".configs", "configs.db");
3748
+ const stats = getConfigStats();
3749
+ const { statSync: st } = await import("fs");
3750
+ const dbSize = existsSync7(dbPath) ? st(dbPath).size : 0;
3751
+ console.log(chalk.bold("@hasna/configs") + chalk.dim(` v${pkg.version}`));
3752
+ console.log(chalk.cyan("DB:") + ` ${dbPath} (${(dbSize / 1024).toFixed(1)}KB)`);
3753
+ console.log(chalk.cyan("Total:") + ` ${stats["total"] || 0} configs
3754
+ `);
3755
+ const allKnown = listConfigs({ kind: "file" });
3756
+ let drifted = 0;
3757
+ let missing = 0;
3758
+ let secrets = 0;
3759
+ let templates = 0;
3760
+ for (const c of allKnown) {
3761
+ if (!c.target_path)
3762
+ continue;
3763
+ const path = expandPath(c.target_path);
3764
+ if (!existsSync7(path)) {
3765
+ missing++;
3766
+ continue;
3767
+ }
3768
+ const disk = readFileSync5(path, "utf-8");
3769
+ const { content: redactedDisk } = redactContent(disk, c.format);
3770
+ if (redactedDisk !== c.content)
3771
+ drifted++;
3772
+ if (c.is_template)
3773
+ templates++;
3774
+ const found = scanSecrets(c.content, c.format);
3775
+ secrets += found.length;
3776
+ }
3777
+ console.log(chalk.cyan("Drifted:") + ` ${drifted === 0 ? chalk.green("0") : chalk.yellow(String(drifted))} (stored \u2260 disk)`);
3778
+ console.log(chalk.cyan("Missing:") + ` ${missing === 0 ? chalk.green("0") : chalk.yellow(String(missing))} (file not on disk)`);
3779
+ console.log(chalk.cyan("Secrets:") + ` ${secrets === 0 ? chalk.green("0 \u2713") : chalk.red(String(secrets) + " \u26A0")} unredacted`);
3780
+ console.log(chalk.cyan("Templates:") + ` ${templates} (with {{VAR}} placeholders)`);
3781
+ });
3782
+ program.command("backup").description("Export configs to a timestamped backup file").action(async () => {
3783
+ const { mkdirSync: mk } = await import("fs");
3784
+ const backupDir = join6(homedir4(), ".configs", "backups");
3785
+ mk(backupDir, { recursive: true });
3786
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 19);
3787
+ const outPath = join6(backupDir, `configs-${ts}.tar.gz`);
3788
+ const result = await exportConfigs(outPath);
3789
+ const { statSync: st } = await import("fs");
3790
+ const size = st(outPath).size;
3791
+ console.log(chalk.green("\u2713") + ` Backup: ${result.count} configs \u2192 ${outPath} (${(size / 1024).toFixed(1)}KB)`);
3792
+ });
3793
+ program.command("restore <file>").description("Restore configs from a backup file").option("--overwrite", "overwrite existing configs (default: skip)").action(async (file, opts) => {
3794
+ const result = await importConfigs(file, { conflict: opts.overwrite ? "overwrite" : "skip" });
3795
+ console.log(chalk.green("\u2713") + ` Restored: +${result.created} updated:${result.updated} skipped:${result.skipped}`);
3796
+ if (result.errors.length > 0) {
3797
+ for (const e of result.errors)
3798
+ console.log(chalk.red(" " + e));
3799
+ }
3800
+ });
3801
+ program.command("doctor").description("Validate configs: syntax, permissions, missing files, secrets").action(async () => {
3802
+ let issues = 0;
3803
+ const pass = (msg) => console.log(chalk.green(" \u2713 ") + msg);
3804
+ const fail = (msg) => {
3805
+ issues++;
3806
+ console.log(chalk.red(" \u2717 ") + msg);
3807
+ };
3808
+ console.log(chalk.bold(`Config Doctor
3809
+ `));
3810
+ console.log(chalk.cyan("Known files on disk:"));
3811
+ for (const k of KNOWN_CONFIGS) {
3812
+ if (k.rulesDir) {
3813
+ existsSync7(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : fail(`${k.rulesDir}/ not found`);
3814
+ } else {
3815
+ existsSync7(expandPath(k.path)) ? pass(k.path) : fail(`${k.path} not found`);
3816
+ }
3817
+ }
3818
+ const allConfigs = listConfigs();
3819
+ console.log(chalk.cyan(`
3820
+ Stored configs (${allConfigs.length}):`));
3821
+ let validCount = 0;
3822
+ for (const c of allConfigs) {
3823
+ if (c.format === "json") {
3824
+ try {
3825
+ JSON.parse(c.content);
3826
+ validCount++;
3827
+ } catch {
3828
+ fail(`${c.slug}: invalid JSON`);
3829
+ }
3830
+ } else {
3831
+ validCount++;
3832
+ }
3833
+ }
3834
+ pass(`${validCount}/${allConfigs.length} valid syntax`);
3835
+ let secretCount = 0;
3836
+ for (const c of allConfigs) {
3837
+ const found = scanSecrets(c.content, c.format);
3838
+ secretCount += found.length;
3839
+ }
3840
+ secretCount === 0 ? pass("No unredacted secrets") : fail(`${secretCount} unredacted secret(s) \u2014 run \`configs scan --fix\``);
3841
+ console.log(`
3842
+ ${issues === 0 ? chalk.green("\u2713 All checks passed") : chalk.yellow(`${issues} issue(s) found`)}`);
3843
+ });
3844
+ program.command("completions [shell]").description("Output shell completion script (zsh or bash)").action(async (shell) => {
3845
+ const sh = shell || "zsh";
3846
+ if (sh === "zsh") {
3847
+ console.log(`#compdef configs
3848
+ _configs() {
3849
+ local -a commands
3850
+ commands=(
3851
+ 'list:List stored configs'
3852
+ 'show:Show a config'
3853
+ 'add:Ingest a file into the DB'
3854
+ 'apply:Apply a config to disk'
3855
+ 'diff:Show diff stored vs disk'
3856
+ 'sync:Sync known configs from disk'
3857
+ 'export:Export as tar.gz'
3858
+ 'import:Import from tar.gz'
3859
+ 'whoami:Setup summary'
3860
+ 'status:Health check'
3861
+ 'init:First-time setup'
3862
+ 'scan:Scan for secrets'
3863
+ 'profile:Manage profiles'
3864
+ 'snapshot:Version history'
3865
+ 'template:Template operations'
3866
+ 'mcp:Install MCP server'
3867
+ 'backup:Export to timestamped backup'
3868
+ 'restore:Import from backup'
3869
+ 'doctor:Validate configs'
3870
+ 'completions:Output shell completions'
3871
+ )
3872
+ _describe 'command' commands
3873
+ }
3874
+ compdef _configs configs`);
3875
+ } else {
3876
+ console.log(`# bash completion for configs
3877
+ _configs_completions() {
3878
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
3879
+ local commands="list show add apply diff sync export import whoami status init scan profile snapshot template mcp backup restore doctor completions"
3880
+ COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
3881
+ }
3882
+ complete -F _configs_completions configs`);
3883
+ }
3884
+ });
3521
3885
  program.version(pkg.version).name("configs");
3522
3886
  program.parse(process.argv);
@@ -11,6 +11,18 @@ export interface KnownConfig {
11
11
  rulesDir?: string;
12
12
  }
13
13
  export declare const KNOWN_CONFIGS: KnownConfig[];
14
+ export declare const PROJECT_CONFIG_FILES: {
15
+ file: string;
16
+ category: ConfigCategory;
17
+ agent: ConfigAgent;
18
+ format: ConfigFormat;
19
+ }[];
20
+ export interface SyncProjectOptions {
21
+ db?: ReturnType<typeof getDatabase>;
22
+ dryRun?: boolean;
23
+ projectDir: string;
24
+ }
25
+ export declare function syncProject(opts: SyncProjectOptions): Promise<SyncResult>;
14
26
  export interface SyncKnownOptions {
15
27
  db?: ReturnType<typeof getDatabase>;
16
28
  dryRun?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/lib/sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACvG,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAShD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,WAAW,EAiCtC,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,EAAE,CAAC,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,wBAAsB,SAAS,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4EhF;AAGD,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,wBAAsB,UAAU,CAAC,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CAgBlF;AAGD,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAqBjD;AAGD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAU/D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CASzD;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,CAQ3D;AAGD,OAAO,EAAE,MAAM,EAAE,CAAC;AAClB,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/lib/sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACvG,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAShD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,WAAW,EAiCtC,CAAC;AAIF,eAAO,MAAM,oBAAoB;;cAC2B,cAAc;WAAsB,WAAW;YAAwB,YAAY;GAO9I,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,EAAE,CAAC,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,CAAC,CAuD/E;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,CAAC,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,wBAAsB,SAAS,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4EhF;AAGD,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,wBAAsB,UAAU,CAAC,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CAgBlF;AAGD,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAqBjD;AAGD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAU/D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CASzD;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,CAQ3D;AAGD,OAAO,EAAE,MAAM,EAAE,CAAC;AAClB,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":";;;;;AA2MA,wBAAgD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":";;;;;AAkPA,wBAAgD"}
@@ -2083,14 +2083,64 @@ async function applyConfigs(configs, opts = {}) {
2083
2083
  return results;
2084
2084
  }
2085
2085
 
2086
- // src/lib/sync.ts
2087
- import { extname, join as join3 } from "path";
2088
- import { homedir as homedir3 } from "os";
2089
-
2090
2086
  // src/lib/sync-dir.ts
2091
2087
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
2092
- import { join as join2, relative } from "path";
2088
+ import { join as join3, relative } from "path";
2089
+ import { homedir as homedir3 } from "os";
2090
+
2091
+ // src/lib/sync.ts
2092
+ import { extname, join as join2 } from "path";
2093
2093
  import { homedir as homedir2 } from "os";
2094
+ function detectCategory(filePath) {
2095
+ const p = filePath.toLowerCase().replace(homedir2(), "~");
2096
+ if (p.includes("/.claude/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("gemini.md"))
2097
+ return "rules";
2098
+ if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.gemini/") || p.includes("/.cursor/"))
2099
+ return "agent";
2100
+ if (p.includes(".mcp.json") || p.includes("mcp"))
2101
+ return "mcp";
2102
+ if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc") || p.includes(".bash_profile"))
2103
+ return "shell";
2104
+ if (p.includes(".gitconfig") || p.includes(".gitignore"))
2105
+ return "git";
2106
+ if (p.includes(".npmrc") || p.includes("tsconfig") || p.includes("bunfig"))
2107
+ return "tools";
2108
+ if (p.includes(".secrets"))
2109
+ return "secrets_schema";
2110
+ return "tools";
2111
+ }
2112
+ function detectAgent(filePath) {
2113
+ const p = filePath.toLowerCase().replace(homedir2(), "~");
2114
+ if (p.includes("/.claude/") || p.endsWith("claude.md"))
2115
+ return "claude";
2116
+ if (p.includes("/.codex/") || p.endsWith("agents.md"))
2117
+ return "codex";
2118
+ if (p.includes("/.gemini/") || p.endsWith("gemini.md"))
2119
+ return "gemini";
2120
+ if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc"))
2121
+ return "zsh";
2122
+ if (p.includes(".gitconfig") || p.includes(".gitignore"))
2123
+ return "git";
2124
+ if (p.includes(".npmrc"))
2125
+ return "npm";
2126
+ return "global";
2127
+ }
2128
+ function detectFormat(filePath) {
2129
+ const ext = extname(filePath).toLowerCase();
2130
+ if (ext === ".json")
2131
+ return "json";
2132
+ if (ext === ".toml")
2133
+ return "toml";
2134
+ if (ext === ".yaml" || ext === ".yml")
2135
+ return "yaml";
2136
+ if (ext === ".md" || ext === ".markdown")
2137
+ return "markdown";
2138
+ if (ext === ".ini" || ext === ".cfg")
2139
+ return "ini";
2140
+ return "text";
2141
+ }
2142
+
2143
+ // src/lib/sync-dir.ts
2094
2144
  var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
2095
2145
  function shouldSkip(p) {
2096
2146
  return SKIP.some((s) => p.includes(s));
@@ -2100,9 +2150,9 @@ async function syncFromDir(dir, opts = {}) {
2100
2150
  const absDir = expandPath(dir);
2101
2151
  if (!existsSync3(absDir))
2102
2152
  return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
2103
- const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join2(absDir, f)).filter((f) => statSync(f).isFile());
2153
+ const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join3(absDir, f)).filter((f) => statSync(f).isFile());
2104
2154
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
2105
- const home = homedir2();
2155
+ const home = homedir3();
2106
2156
  const allConfigs = listConfigs(undefined, d);
2107
2157
  for (const file of files) {
2108
2158
  if (shouldSkip(file)) {
@@ -2136,7 +2186,7 @@ async function syncFromDir(dir, opts = {}) {
2136
2186
  }
2137
2187
  async function syncToDir(dir, opts = {}) {
2138
2188
  const d = opts.db || getDatabase();
2139
- const home = homedir2();
2189
+ const home = homedir3();
2140
2190
  const absDir = expandPath(dir);
2141
2191
  const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
2142
2192
  const configs = listConfigs(undefined, d).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
@@ -2155,7 +2205,7 @@ async function syncToDir(dir, opts = {}) {
2155
2205
  }
2156
2206
  function walkDir(dir, files = []) {
2157
2207
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2158
- const full = join2(dir, entry.name);
2208
+ const full = join3(dir, entry.name);
2159
2209
  if (shouldSkip(full))
2160
2210
  continue;
2161
2211
  if (entry.isDirectory())
@@ -2165,57 +2215,10 @@ function walkDir(dir, files = []) {
2165
2215
  }
2166
2216
  return files;
2167
2217
  }
2168
- // src/lib/sync.ts
2169
- function detectCategory(filePath) {
2170
- const p = filePath.toLowerCase().replace(homedir3(), "~");
2171
- if (p.includes("/.claude/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("gemini.md"))
2172
- return "rules";
2173
- if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.gemini/") || p.includes("/.cursor/"))
2174
- return "agent";
2175
- if (p.includes(".mcp.json") || p.includes("mcp"))
2176
- return "mcp";
2177
- if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc") || p.includes(".bash_profile"))
2178
- return "shell";
2179
- if (p.includes(".gitconfig") || p.includes(".gitignore"))
2180
- return "git";
2181
- if (p.includes(".npmrc") || p.includes("tsconfig") || p.includes("bunfig"))
2182
- return "tools";
2183
- if (p.includes(".secrets"))
2184
- return "secrets_schema";
2185
- return "tools";
2186
- }
2187
- function detectAgent(filePath) {
2188
- const p = filePath.toLowerCase().replace(homedir3(), "~");
2189
- if (p.includes("/.claude/") || p.endsWith("claude.md"))
2190
- return "claude";
2191
- if (p.includes("/.codex/") || p.endsWith("agents.md"))
2192
- return "codex";
2193
- if (p.includes("/.gemini/") || p.endsWith("gemini.md"))
2194
- return "gemini";
2195
- if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc"))
2196
- return "zsh";
2197
- if (p.includes(".gitconfig") || p.includes(".gitignore"))
2198
- return "git";
2199
- if (p.includes(".npmrc"))
2200
- return "npm";
2201
- return "global";
2202
- }
2203
- function detectFormat(filePath) {
2204
- const ext = extname(filePath).toLowerCase();
2205
- if (ext === ".json")
2206
- return "json";
2207
- if (ext === ".toml")
2208
- return "toml";
2209
- if (ext === ".yaml" || ext === ".yml")
2210
- return "yaml";
2211
- if (ext === ".md" || ext === ".markdown")
2212
- return "markdown";
2213
- if (ext === ".ini" || ext === ".cfg")
2214
- return "ini";
2215
- return "text";
2216
- }
2217
2218
 
2218
2219
  // src/server/index.ts
2220
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
2221
+ import { join as join4, extname as extname2 } from "path";
2219
2222
  var PORT = Number(process.env["CONFIGS_PORT"] ?? 3457);
2220
2223
  function pickFields(obj, fields) {
2221
2224
  if (!fields)
@@ -2376,8 +2379,38 @@ app.post("/api/machines", async (c) => {
2376
2379
  return c.json({ error: e instanceof Error ? e.message : String(e) }, 422);
2377
2380
  }
2378
2381
  });
2379
- app.get("/health", (c) => c.json({ ok: true, version: "0.1.0" }));
2380
- console.log(`configs-serve listening on http://localhost:${PORT}`);
2382
+ app.get("/health", (c) => c.json({ ok: true, version: "0.1.5" }));
2383
+ var MIME = { ".html": "text/html", ".js": "application/javascript", ".css": "text/css", ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png", ".ico": "image/x-icon" };
2384
+ function findDashboardDir() {
2385
+ const candidates = [
2386
+ join4(import.meta.dir, "../../dashboard/dist"),
2387
+ join4(import.meta.dir, "../dashboard/dist"),
2388
+ join4(import.meta.dir, "../../../dashboard/dist")
2389
+ ];
2390
+ for (const dir of candidates) {
2391
+ if (existsSync4(join4(dir, "index.html")))
2392
+ return dir;
2393
+ }
2394
+ return null;
2395
+ }
2396
+ var dashDir = findDashboardDir();
2397
+ if (dashDir) {
2398
+ app.get("/*", (c) => {
2399
+ const url = new URL(c.req.url);
2400
+ let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
2401
+ let absPath = join4(dashDir, filePath);
2402
+ if (!existsSync4(absPath))
2403
+ absPath = join4(dashDir, "index.html");
2404
+ if (!existsSync4(absPath))
2405
+ return c.json({ error: "Not found" }, 404);
2406
+ const content = readFileSync3(absPath);
2407
+ const ext = extname2(absPath);
2408
+ return new Response(content, {
2409
+ headers: { "Content-Type": MIME[ext] || "application/octet-stream" }
2410
+ });
2411
+ });
2412
+ }
2413
+ console.log(`configs-serve listening on http://localhost:${PORT}${dashDir ? " (dashboard: /" : " (no dashboard found)"}`);
2381
2414
  var server_default = { port: PORT, fetch: app.fetch };
2382
2415
  export {
2383
2416
  server_default as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/configs",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "AI coding agent configuration manager — store, version, apply, and share all your AI coding configs. CLI + MCP + REST API + Dashboard.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",