@hasna/configs 0.2.2 → 0.2.4

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
@@ -2064,7 +2064,7 @@ var require_commander = __commonJS((exports) => {
2064
2064
  });
2065
2065
 
2066
2066
  // src/types/index.ts
2067
- var ConfigNotFoundError, ProfileNotFoundError, ConfigApplyError;
2067
+ var ConfigNotFoundError, ProfileNotFoundError, ConfigApplyError, TemplateRenderError;
2068
2068
  var init_types = __esm(() => {
2069
2069
  ConfigNotFoundError = class ConfigNotFoundError extends Error {
2070
2070
  constructor(id) {
@@ -2084,6 +2084,12 @@ var init_types = __esm(() => {
2084
2084
  this.name = "ConfigApplyError";
2085
2085
  }
2086
2086
  };
2087
+ TemplateRenderError = class TemplateRenderError extends Error {
2088
+ constructor(message) {
2089
+ super(message);
2090
+ this.name = "TemplateRenderError";
2091
+ }
2092
+ };
2087
2093
  });
2088
2094
 
2089
2095
  // src/db/database.ts
@@ -3015,6 +3021,61 @@ var init_sync = __esm(() => {
3015
3021
  ];
3016
3022
  });
3017
3023
 
3024
+ // src/lib/template.ts
3025
+ var exports_template = {};
3026
+ __export(exports_template, {
3027
+ renderTemplate: () => renderTemplate,
3028
+ parseTemplateVars: () => parseTemplateVars,
3029
+ isTemplate: () => isTemplate,
3030
+ extractTemplateVars: () => extractTemplateVars
3031
+ });
3032
+ function parseTemplateVars(content) {
3033
+ const names = new Set;
3034
+ let match;
3035
+ VAR_PATTERN.lastIndex = 0;
3036
+ while ((match = VAR_PATTERN.exec(content)) !== null) {
3037
+ names.add(match[1]);
3038
+ }
3039
+ return Array.from(names);
3040
+ }
3041
+ function extractTemplateVars(content) {
3042
+ const vars = new Map;
3043
+ let match;
3044
+ VAR_PATTERN.lastIndex = 0;
3045
+ while ((match = VAR_PATTERN.exec(content)) !== null) {
3046
+ const name = match[1];
3047
+ const description = match[2] ?? null;
3048
+ if (!vars.has(name)) {
3049
+ vars.set(name, { name, description, required: true });
3050
+ }
3051
+ }
3052
+ return Array.from(vars.values());
3053
+ }
3054
+ function renderTemplate(content, vars) {
3055
+ const missing = [];
3056
+ VAR_PATTERN.lastIndex = 0;
3057
+ let match;
3058
+ while ((match = VAR_PATTERN.exec(content)) !== null) {
3059
+ const name = match[1];
3060
+ if (!(name in vars))
3061
+ missing.push(name);
3062
+ }
3063
+ if (missing.length > 0) {
3064
+ throw new TemplateRenderError(`Missing required template variables: ${missing.join(", ")}`);
3065
+ }
3066
+ VAR_PATTERN.lastIndex = 0;
3067
+ return content.replace(VAR_PATTERN, (_match, name) => vars[name] ?? "");
3068
+ }
3069
+ function isTemplate(content) {
3070
+ VAR_PATTERN.lastIndex = 0;
3071
+ return VAR_PATTERN.test(content);
3072
+ }
3073
+ var VAR_PATTERN;
3074
+ var init_template = __esm(() => {
3075
+ init_types();
3076
+ VAR_PATTERN = /\{\{([A-Z0-9_]+)(?::([^}]*))?\}\}/g;
3077
+ });
3078
+
3018
3079
  // node_modules/commander/esm.mjs
3019
3080
  var import__ = __toESM(require_commander(), 1);
3020
3081
  var {
@@ -3220,24 +3281,8 @@ async function importConfigs(bundlePath, opts = {}) {
3220
3281
  }
3221
3282
  }
3222
3283
 
3223
- // src/lib/template.ts
3224
- init_types();
3225
- var VAR_PATTERN = /\{\{([A-Z0-9_]+)(?::([^}]*))?\}\}/g;
3226
- function extractTemplateVars(content) {
3227
- const vars = new Map;
3228
- let match;
3229
- VAR_PATTERN.lastIndex = 0;
3230
- while ((match = VAR_PATTERN.exec(content)) !== null) {
3231
- const name = match[1];
3232
- const description = match[2] ?? null;
3233
- if (!vars.has(name)) {
3234
- vars.set(name, { name, description, required: true });
3235
- }
3236
- }
3237
- return Array.from(vars.values());
3238
- }
3239
-
3240
3284
  // src/cli/index.tsx
3285
+ init_template();
3241
3286
  import { createRequire } from "module";
3242
3287
  var pkg = createRequire(import.meta.url)("../../package.json");
3243
3288
  function fmtConfig(c, format) {
@@ -3307,7 +3352,7 @@ program.command("add <path>").description("Ingest a file into the config DB").op
3307
3352
  }
3308
3353
  const rawContent = readFileSync5(abs, "utf-8");
3309
3354
  const fmt = detectFormat(abs);
3310
- const { content, redacted, isTemplate } = redactContent(rawContent, fmt);
3355
+ const { content, redacted, isTemplate: isTemplate2 } = redactContent(rawContent, fmt);
3311
3356
  const targetPath = abs.startsWith(homedir4()) ? abs.replace(homedir4(), "~") : abs;
3312
3357
  const name = opts.name || filePath.split("/").pop();
3313
3358
  const config = createConfig({
@@ -3318,7 +3363,7 @@ program.command("add <path>").description("Ingest a file into the config DB").op
3318
3363
  target_path: opts.kind === "reference" ? null : targetPath,
3319
3364
  format: fmt,
3320
3365
  content,
3321
- is_template: (opts.template ?? false) || isTemplate
3366
+ is_template: (opts.template ?? false) || isTemplate2
3322
3367
  });
3323
3368
  console.log(chalk.green("\u2713") + ` Added: ${chalk.bold(config.name)} ${chalk.dim(`(${config.slug})`)}`);
3324
3369
  if (redacted.length > 0) {
@@ -3583,6 +3628,54 @@ templateCmd.command("vars <id>").description("Show template variables").action(a
3583
3628
  process.exit(1);
3584
3629
  }
3585
3630
  });
3631
+ templateCmd.command("render <id>").description("Render a template config with variables and optionally apply to disk").option("--var <vars...>", "set variables as KEY=VALUE pairs").option("--env", "use environment variables to fill template vars").option("--apply", "write rendered output to target_path").option("--dry-run", "preview rendered output without writing").action(async (id, opts) => {
3632
+ try {
3633
+ const { renderTemplate: renderTemplate2 } = await Promise.resolve().then(() => (init_template(), exports_template));
3634
+ const c = getConfig(id);
3635
+ const vars = {};
3636
+ if (opts.var) {
3637
+ for (const kv of opts.var) {
3638
+ const eq = kv.indexOf("=");
3639
+ if (eq === -1) {
3640
+ console.error(chalk.red(`Invalid --var: ${kv} (expected KEY=VALUE)`));
3641
+ process.exit(1);
3642
+ }
3643
+ vars[kv.slice(0, eq)] = kv.slice(eq + 1);
3644
+ }
3645
+ }
3646
+ if (opts.env) {
3647
+ const { extractTemplateVars: extractTemplateVars2 } = await Promise.resolve().then(() => (init_template(), exports_template));
3648
+ for (const v of extractTemplateVars2(c.content)) {
3649
+ if (!(v.name in vars) && process.env[v.name]) {
3650
+ vars[v.name] = process.env[v.name];
3651
+ }
3652
+ }
3653
+ }
3654
+ const rendered = renderTemplate2(c.content, vars);
3655
+ if (opts.apply || opts.dryRun) {
3656
+ if (!c.target_path) {
3657
+ console.error(chalk.red("No target_path \u2014 cannot apply reference configs"));
3658
+ process.exit(1);
3659
+ }
3660
+ if (opts.dryRun) {
3661
+ console.log(chalk.yellow("[dry-run]") + ` Would write to ${expandPath(c.target_path)}`);
3662
+ console.log(rendered);
3663
+ } else {
3664
+ const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync5 } = await import("fs");
3665
+ const { dirname: dirname3 } = await import("path");
3666
+ const path = expandPath(c.target_path);
3667
+ mkdirSync5(dirname3(path), { recursive: true });
3668
+ writeFileSync3(path, rendered, "utf-8");
3669
+ console.log(chalk.green("\u2713") + ` Rendered and applied to ${path}`);
3670
+ }
3671
+ } else {
3672
+ console.log(rendered);
3673
+ }
3674
+ } catch (e) {
3675
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
3676
+ process.exit(1);
3677
+ }
3678
+ });
3586
3679
  program.command("scan [id]").description("Scan configs for secrets. Defaults to known configs only.").option("--fix", "redact found secrets in-place").option("--all", "scan every config in the DB (slow on large DBs)").option("-c, --category <cat>", "scan only a specific category").action(async (id, opts) => {
3587
3680
  let configs;
3588
3681
  if (id) {
@@ -3620,8 +3713,8 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
3620
3713
  for (const s of secrets)
3621
3714
  console.log(` line ${s.line}: ${chalk.red(s.varName)} \u2014 ${s.reason}`);
3622
3715
  if (opts.fix) {
3623
- const { content, isTemplate } = redactContent(c.content, fmt);
3624
- updateConfig(c.id, { content, is_template: isTemplate });
3716
+ const { content, isTemplate: isTemplate2 } = redactContent(c.content, fmt);
3717
+ updateConfig(c.id, { content, is_template: isTemplate2 });
3625
3718
  console.log(chalk.green(" \u2713 Redacted."));
3626
3719
  }
3627
3720
  }
package/dist/mcp/index.js CHANGED
@@ -1,32 +1,39 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
-
4
- // src/mcp/index.ts
5
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true,
9
+ configurable: true,
10
+ set: (newValue) => all[name] = () => newValue
11
+ });
12
+ };
13
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
8
14
 
9
15
  // src/types/index.ts
10
- class ConfigNotFoundError extends Error {
11
- constructor(id) {
12
- super(`Config not found: ${id}`);
13
- this.name = "ConfigNotFoundError";
14
- }
15
- }
16
-
17
- class ProfileNotFoundError extends Error {
18
- constructor(id) {
19
- super(`Profile not found: ${id}`);
20
- this.name = "ProfileNotFoundError";
21
- }
22
- }
23
-
24
- class ConfigApplyError extends Error {
25
- constructor(message) {
26
- super(message);
27
- this.name = "ConfigApplyError";
28
- }
29
- }
16
+ var ConfigNotFoundError, ProfileNotFoundError, ConfigApplyError;
17
+ var init_types = __esm(() => {
18
+ ConfigNotFoundError = class ConfigNotFoundError extends Error {
19
+ constructor(id) {
20
+ super(`Config not found: ${id}`);
21
+ this.name = "ConfigNotFoundError";
22
+ }
23
+ };
24
+ ProfileNotFoundError = class ProfileNotFoundError extends Error {
25
+ constructor(id) {
26
+ super(`Profile not found: ${id}`);
27
+ this.name = "ProfileNotFoundError";
28
+ }
29
+ };
30
+ ConfigApplyError = class ConfigApplyError extends Error {
31
+ constructor(message) {
32
+ super(message);
33
+ this.name = "ConfigApplyError";
34
+ }
35
+ };
36
+ });
30
37
 
31
38
  // src/db/database.ts
32
39
  import { Database } from "bun:sqlite";
@@ -57,8 +64,35 @@ function now() {
57
64
  function slugify(name) {
58
65
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
59
66
  }
60
- var MIGRATIONS = [
61
- `
67
+ function getDatabase(path) {
68
+ if (_db)
69
+ return _db;
70
+ const dbPath = path || getDbPath();
71
+ ensureDir(dbPath);
72
+ const db = new Database(dbPath);
73
+ db.run("PRAGMA journal_mode = WAL");
74
+ db.run("PRAGMA foreign_keys = ON");
75
+ applyMigrations(db);
76
+ _db = db;
77
+ return db;
78
+ }
79
+ function applyMigrations(db) {
80
+ let currentVersion = 0;
81
+ try {
82
+ const row = db.query("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get();
83
+ currentVersion = row?.version ?? 0;
84
+ } catch {
85
+ currentVersion = 0;
86
+ }
87
+ for (let i = currentVersion;i < MIGRATIONS.length; i++) {
88
+ db.run(MIGRATIONS[i]);
89
+ db.run(`INSERT OR REPLACE INTO schema_version (version) VALUES (${i + 1})`);
90
+ }
91
+ }
92
+ var MIGRATIONS, _db = null;
93
+ var init_database = __esm(() => {
94
+ MIGRATIONS = [
95
+ `
62
96
  CREATE TABLE IF NOT EXISTS configs (
63
97
  id TEXT PRIMARY KEY,
64
98
  name TEXT NOT NULL,
@@ -116,33 +150,8 @@ var MIGRATIONS = [
116
150
 
117
151
  INSERT OR IGNORE INTO schema_version (version) VALUES (1);
118
152
  `
119
- ];
120
- var _db = null;
121
- function getDatabase(path) {
122
- if (_db)
123
- return _db;
124
- const dbPath = path || getDbPath();
125
- ensureDir(dbPath);
126
- const db = new Database(dbPath);
127
- db.run("PRAGMA journal_mode = WAL");
128
- db.run("PRAGMA foreign_keys = ON");
129
- applyMigrations(db);
130
- _db = db;
131
- return db;
132
- }
133
- function applyMigrations(db) {
134
- let currentVersion = 0;
135
- try {
136
- const row = db.query("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get();
137
- currentVersion = row?.version ?? 0;
138
- } catch {
139
- currentVersion = 0;
140
- }
141
- for (let i = currentVersion;i < MIGRATIONS.length; i++) {
142
- db.run(MIGRATIONS[i]);
143
- db.run(`INSERT OR REPLACE INTO schema_version (version) VALUES (${i + 1})`);
144
- }
145
- }
153
+ ];
154
+ });
146
155
 
147
156
  // src/db/configs.ts
148
157
  function rowToConfig(row) {
@@ -295,11 +304,20 @@ function updateConfig(idOrSlug, input, db) {
295
304
  d.run(`UPDATE configs SET ${updates.join(", ")} WHERE id = ?`, params);
296
305
  return getConfigById(existing.id, d);
297
306
  }
298
-
299
- // src/lib/apply.ts
300
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
301
- import { dirname as dirname2, resolve as resolve2 } from "path";
302
- import { homedir } from "os";
307
+ function getConfigStats(db) {
308
+ const d = db || getDatabase();
309
+ const rows = d.query("SELECT category, COUNT(*) as count FROM configs GROUP BY category").all();
310
+ const stats = { total: 0 };
311
+ for (const row of rows) {
312
+ stats[row.category] = row.count;
313
+ stats["total"] = (stats["total"] || 0) + row.count;
314
+ }
315
+ return stats;
316
+ }
317
+ var init_configs = __esm(() => {
318
+ init_types();
319
+ init_database();
320
+ });
303
321
 
304
322
  // src/db/snapshots.ts
305
323
  function createSnapshot(configId, content, version, db) {
@@ -317,8 +335,14 @@ function getSnapshotByVersion(configId, version, db) {
317
335
  const d = db || getDatabase();
318
336
  return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
319
337
  }
338
+ var init_snapshots = __esm(() => {
339
+ init_database();
340
+ });
320
341
 
321
342
  // src/lib/apply.ts
343
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
344
+ import { dirname as dirname2, resolve as resolve2 } from "path";
345
+ import { homedir } from "os";
322
346
  function expandPath(p) {
323
347
  if (p.startsWith("~/")) {
324
348
  return resolve2(homedir(), p.slice(2));
@@ -363,16 +387,192 @@ async function applyConfigs(configs, opts = {}) {
363
387
  }
364
388
  return results;
365
389
  }
390
+ var init_apply = __esm(() => {
391
+ init_types();
392
+ init_database();
393
+ init_configs();
394
+ init_snapshots();
395
+ });
366
396
 
367
- // src/lib/sync.ts
368
- import { extname, join as join3 } from "path";
369
- import { homedir as homedir3 } from "os";
397
+ // src/lib/redact.ts
398
+ function redactShell(content) {
399
+ const redacted = [];
400
+ const lines = content.split(`
401
+ `);
402
+ const out = [];
403
+ for (let i = 0;i < lines.length; i++) {
404
+ const line = lines[i];
405
+ const m = line.match(/^(\s*(?:export\s+)?)([A-Z][A-Z0-9_]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
406
+ if (m) {
407
+ const [, prefix, key, eq, quote, value] = m;
408
+ if (shouldRedactKeyValue(key, value)) {
409
+ const reason = reasonFor(key, value);
410
+ redacted.push({ varName: key, line: i + 1, reason });
411
+ out.push(`${prefix}${key}${eq}${quote}{{${key}}}${quote}`);
412
+ continue;
413
+ }
414
+ }
415
+ out.push(line);
416
+ }
417
+ return { content: out.join(`
418
+ `), redacted, isTemplate: redacted.length > 0 };
419
+ }
420
+ function redactJson(content) {
421
+ const redacted = [];
422
+ const lines = content.split(`
423
+ `);
424
+ const out = [];
425
+ for (let i = 0;i < lines.length; i++) {
426
+ const line = lines[i];
427
+ const m = line.match(/^(\s*"([^"]+)"\s*:\s*)"([^"]+)"(,?)(\s*)$/);
428
+ if (m) {
429
+ const [, prefix, key, value, comma, trail] = m;
430
+ if (shouldRedactKeyValue(key, value)) {
431
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
432
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
433
+ out.push(`${prefix}"{{${varName}}}"${comma}${trail}`);
434
+ continue;
435
+ }
436
+ }
437
+ let newLine = line;
438
+ for (const { re, reason } of VALUE_PATTERNS) {
439
+ newLine = newLine.replace(re, (match) => {
440
+ const varName = `REDACTED_${reason.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
441
+ redacted.push({ varName, line: i + 1, reason });
442
+ return `{{${varName}}}`;
443
+ });
444
+ }
445
+ out.push(newLine);
446
+ }
447
+ return { content: out.join(`
448
+ `), redacted, isTemplate: redacted.length > 0 };
449
+ }
450
+ function redactToml(content) {
451
+ const redacted = [];
452
+ const lines = content.split(`
453
+ `);
454
+ const out = [];
455
+ for (let i = 0;i < lines.length; i++) {
456
+ const line = lines[i];
457
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
458
+ if (m) {
459
+ const [, indent, key, eq, quote, value] = m;
460
+ if (shouldRedactKeyValue(key, value)) {
461
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
462
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
463
+ out.push(`${indent}${key}${eq}${quote}{{${varName}}}${quote}`);
464
+ continue;
465
+ }
466
+ }
467
+ out.push(line);
468
+ }
469
+ return { content: out.join(`
470
+ `), redacted, isTemplate: redacted.length > 0 };
471
+ }
472
+ function redactIni(content) {
473
+ const redacted = [];
474
+ const lines = content.split(`
475
+ `);
476
+ const out = [];
477
+ for (let i = 0;i < lines.length; i++) {
478
+ const line = lines[i];
479
+ const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
480
+ if (authM && !authM[2].startsWith("{{")) {
481
+ redacted.push({ varName: "NPM_AUTH_TOKEN", line: i + 1, reason: "npm auth token" });
482
+ out.push(`${authM[1]}{{NPM_AUTH_TOKEN}}`);
483
+ continue;
484
+ }
485
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
486
+ if (m) {
487
+ const [, indent, key, eq, value] = m;
488
+ if (shouldRedactKeyValue(key, value)) {
489
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
490
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
491
+ out.push(`${indent}${key}${eq}{{${varName}}}`);
492
+ continue;
493
+ }
494
+ }
495
+ out.push(line);
496
+ }
497
+ return { content: out.join(`
498
+ `), redacted, isTemplate: redacted.length > 0 };
499
+ }
500
+ function redactGeneric(content) {
501
+ const redacted = [];
502
+ const lines = content.split(`
503
+ `);
504
+ const out = [];
505
+ for (let i = 0;i < lines.length; i++) {
506
+ let line = lines[i];
507
+ for (const { re, reason } of VALUE_PATTERNS) {
508
+ line = line.replace(re, (match) => {
509
+ const varName = reason.toUpperCase().replace(/[^A-Z0-9]/g, "_");
510
+ redacted.push({ varName, line: i + 1, reason });
511
+ return `{{${varName}}}`;
512
+ });
513
+ }
514
+ out.push(line);
515
+ }
516
+ return { content: out.join(`
517
+ `), redacted, isTemplate: redacted.length > 0 };
518
+ }
519
+ function shouldRedactKeyValue(key, value) {
520
+ if (!value || value.startsWith("{{"))
521
+ return false;
522
+ if (value.length < MIN_SECRET_VALUE_LEN)
523
+ return false;
524
+ if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
525
+ return false;
526
+ if (SECRET_KEY_PATTERN.test(key))
527
+ return true;
528
+ for (const { re } of VALUE_PATTERNS) {
529
+ if (re.test(value))
530
+ return true;
531
+ }
532
+ return false;
533
+ }
534
+ function reasonFor(key, value) {
535
+ if (SECRET_KEY_PATTERN.test(key))
536
+ return `secret key name: ${key}`;
537
+ for (const { re, reason } of VALUE_PATTERNS) {
538
+ if (re.test(value))
539
+ return reason;
540
+ }
541
+ return "secret value pattern";
542
+ }
543
+ function redactContent(content, format) {
544
+ switch (format) {
545
+ case "shell":
546
+ return redactShell(content);
547
+ case "json":
548
+ return redactJson(content);
549
+ case "toml":
550
+ return redactToml(content);
551
+ case "ini":
552
+ return redactIni(content);
553
+ default:
554
+ return redactGeneric(content);
555
+ }
556
+ }
557
+ var SECRET_KEY_PATTERN, VALUE_PATTERNS, MIN_SECRET_VALUE_LEN = 8;
558
+ var init_redact = __esm(() => {
559
+ 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;
560
+ VALUE_PATTERNS = [
561
+ { re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
562
+ { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
563
+ { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
564
+ { re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
565
+ { re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
566
+ { re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
567
+ { re: /ey[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\./, reason: "JWT token" },
568
+ { re: /AKIA[0-9A-Z]{16}/, reason: "AWS access key" }
569
+ ];
570
+ });
370
571
 
371
572
  // src/lib/sync-dir.ts
372
573
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
373
574
  import { join as join2, relative } from "path";
374
575
  import { homedir as homedir2 } from "os";
375
- var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
376
576
  function shouldSkip(p) {
377
577
  return SKIP.some((s) => p.includes(s));
378
578
  }
@@ -446,7 +646,225 @@ function walkDir(dir, files = []) {
446
646
  }
447
647
  return files;
448
648
  }
649
+ var SKIP;
650
+ var init_sync_dir = __esm(() => {
651
+ init_database();
652
+ init_configs();
653
+ init_apply();
654
+ init_sync();
655
+ SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
656
+ });
657
+
449
658
  // src/lib/sync.ts
659
+ var exports_sync = {};
660
+ __export(exports_sync, {
661
+ syncToDisk: () => syncToDisk,
662
+ syncToDir: () => syncToDir,
663
+ syncProject: () => syncProject,
664
+ syncKnown: () => syncKnown,
665
+ syncFromDir: () => syncFromDir,
666
+ diffConfig: () => diffConfig,
667
+ detectFormat: () => detectFormat,
668
+ detectCategory: () => detectCategory,
669
+ detectAgent: () => detectAgent,
670
+ PROJECT_CONFIG_FILES: () => PROJECT_CONFIG_FILES,
671
+ KNOWN_CONFIGS: () => KNOWN_CONFIGS
672
+ });
673
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
674
+ import { extname, join as join3 } from "path";
675
+ import { homedir as homedir3 } from "os";
676
+ async function syncProject(opts) {
677
+ const d = opts.db || getDatabase();
678
+ const absDir = expandPath(opts.projectDir);
679
+ const projectName = absDir.split("/").pop() || "project";
680
+ const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
681
+ const allConfigs = listConfigs(undefined, d);
682
+ for (const pf of PROJECT_CONFIG_FILES) {
683
+ const abs = join3(absDir, pf.file);
684
+ if (!existsSync4(abs))
685
+ continue;
686
+ try {
687
+ const rawContent = readFileSync3(abs, "utf-8");
688
+ if (rawContent.length > 500000) {
689
+ result.skipped.push(pf.file);
690
+ continue;
691
+ }
692
+ const { content, isTemplate } = redactContent(rawContent, pf.format);
693
+ const name = `${projectName}/${pf.file}`;
694
+ const targetPath = abs.replace(homedir3(), "~");
695
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
696
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
697
+ if (!existing) {
698
+ if (!opts.dryRun)
699
+ createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate }, d);
700
+ result.added++;
701
+ } else if (existing.content !== content) {
702
+ if (!opts.dryRun)
703
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
704
+ result.updated++;
705
+ } else {
706
+ result.unchanged++;
707
+ }
708
+ } catch {
709
+ result.skipped.push(pf.file);
710
+ }
711
+ }
712
+ const rulesDir = join3(absDir, ".claude", "rules");
713
+ if (existsSync4(rulesDir)) {
714
+ const mdFiles = readdirSync2(rulesDir).filter((f) => f.endsWith(".md"));
715
+ for (const f of mdFiles) {
716
+ const abs = join3(rulesDir, f);
717
+ const raw = readFileSync3(abs, "utf-8");
718
+ const { content, isTemplate } = redactContent(raw, "markdown");
719
+ const name = `${projectName}/rules/${f}`;
720
+ const targetPath = abs.replace(homedir3(), "~");
721
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
722
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
723
+ if (!existing) {
724
+ if (!opts.dryRun)
725
+ createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate }, d);
726
+ result.added++;
727
+ } else if (existing.content !== content) {
728
+ if (!opts.dryRun)
729
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
730
+ result.updated++;
731
+ } else {
732
+ result.unchanged++;
733
+ }
734
+ }
735
+ }
736
+ return result;
737
+ }
738
+ async function syncKnown(opts = {}) {
739
+ const d = opts.db || getDatabase();
740
+ const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
741
+ const home = homedir3();
742
+ let targets = KNOWN_CONFIGS;
743
+ if (opts.agent)
744
+ targets = targets.filter((k) => k.agent === opts.agent);
745
+ if (opts.category)
746
+ targets = targets.filter((k) => k.category === opts.category);
747
+ const allConfigs = listConfigs(undefined, d);
748
+ for (const known of targets) {
749
+ if (known.rulesDir) {
750
+ const absDir = expandPath(known.rulesDir);
751
+ if (!existsSync4(absDir)) {
752
+ result.skipped.push(known.rulesDir);
753
+ continue;
754
+ }
755
+ const mdFiles = readdirSync2(absDir).filter((f) => f.endsWith(".md"));
756
+ for (const f of mdFiles) {
757
+ const abs2 = join3(absDir, f);
758
+ const targetPath = abs2.replace(home, "~");
759
+ const raw = readFileSync3(abs2, "utf-8");
760
+ const { content, isTemplate } = redactContent(raw, "markdown");
761
+ const name = `claude-rules-${f}`;
762
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
763
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
764
+ if (!existing) {
765
+ if (!opts.dryRun)
766
+ createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate }, d);
767
+ result.added++;
768
+ } else if (existing.content !== content) {
769
+ if (!opts.dryRun)
770
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
771
+ result.updated++;
772
+ } else {
773
+ result.unchanged++;
774
+ }
775
+ }
776
+ continue;
777
+ }
778
+ const abs = expandPath(known.path);
779
+ if (!existsSync4(abs)) {
780
+ result.skipped.push(known.path);
781
+ continue;
782
+ }
783
+ try {
784
+ const rawContent = readFileSync3(abs, "utf-8");
785
+ if (rawContent.length > 500000) {
786
+ result.skipped.push(known.path + " (too large)");
787
+ continue;
788
+ }
789
+ const fmt = known.format ?? detectFormat(abs);
790
+ const { content, isTemplate } = redactContent(rawContent, fmt);
791
+ const targetPath = abs.replace(home, "~");
792
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
793
+ if (!existing) {
794
+ if (!opts.dryRun) {
795
+ createConfig({
796
+ name: known.name,
797
+ category: known.category,
798
+ agent: known.agent,
799
+ format: fmt,
800
+ content,
801
+ target_path: known.kind === "reference" ? null : targetPath,
802
+ kind: known.kind ?? "file",
803
+ description: known.description,
804
+ is_template: isTemplate
805
+ }, d);
806
+ }
807
+ result.added++;
808
+ } else if (existing.content !== content) {
809
+ if (!opts.dryRun)
810
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
811
+ result.updated++;
812
+ } else {
813
+ result.unchanged++;
814
+ }
815
+ } catch {
816
+ result.skipped.push(known.path);
817
+ }
818
+ }
819
+ return result;
820
+ }
821
+ async function syncToDisk(opts = {}) {
822
+ const d = opts.db || getDatabase();
823
+ const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
824
+ let configs = listConfigs({ kind: "file", ...opts.agent ? { agent: opts.agent } : {}, ...opts.category ? { category: opts.category } : {} }, d);
825
+ for (const config of configs) {
826
+ if (!config.target_path)
827
+ continue;
828
+ try {
829
+ const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
830
+ r.changed ? result.updated++ : result.unchanged++;
831
+ } catch {
832
+ result.skipped.push(config.target_path);
833
+ }
834
+ }
835
+ return result;
836
+ }
837
+ function diffConfig(config) {
838
+ if (!config.target_path)
839
+ return "(reference \u2014 no target path)";
840
+ const path = expandPath(config.target_path);
841
+ if (!existsSync4(path))
842
+ return `(file not found on disk: ${path})`;
843
+ const diskContent = readFileSync3(path, "utf-8");
844
+ if (diskContent === config.content)
845
+ return "(no diff \u2014 identical)";
846
+ const stored = config.content.split(`
847
+ `);
848
+ const disk = diskContent.split(`
849
+ `);
850
+ const lines = [`--- stored (DB)`, `+++ disk (${path})`];
851
+ const maxLen = Math.max(stored.length, disk.length);
852
+ for (let i = 0;i < maxLen; i++) {
853
+ const s = stored[i];
854
+ const dk = disk[i];
855
+ if (s === dk) {
856
+ if (s !== undefined)
857
+ lines.push(` ${s}`);
858
+ } else {
859
+ if (s !== undefined)
860
+ lines.push(`-${s}`);
861
+ if (dk !== undefined)
862
+ lines.push(`+${dk}`);
863
+ }
864
+ }
865
+ return lines.join(`
866
+ `);
867
+ }
450
868
  function detectCategory(filePath) {
451
869
  const p = filePath.toLowerCase().replace(homedir3(), "~");
452
870
  if (p.includes("/.claude/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("gemini.md"))
@@ -495,8 +913,56 @@ function detectFormat(filePath) {
495
913
  return "ini";
496
914
  return "text";
497
915
  }
916
+ var KNOWN_CONFIGS, PROJECT_CONFIG_FILES;
917
+ var init_sync = __esm(() => {
918
+ init_database();
919
+ init_configs();
920
+ init_apply();
921
+ init_redact();
922
+ init_sync_dir();
923
+ KNOWN_CONFIGS = [
924
+ { path: "~/.claude/CLAUDE.md", name: "claude-claude-md", category: "rules", agent: "claude", format: "markdown" },
925
+ { path: "~/.claude/settings.json", name: "claude-settings", category: "agent", agent: "claude", format: "json" },
926
+ { path: "~/.claude/settings.local.json", name: "claude-settings-local", category: "agent", agent: "claude", format: "json" },
927
+ { path: "~/.claude/keybindings.json", name: "claude-keybindings", category: "agent", agent: "claude", format: "json" },
928
+ { path: "~/.claude/rules", name: "claude-rules", category: "rules", agent: "claude", rulesDir: "~/.claude/rules" },
929
+ { path: "~/.codex/config.toml", name: "codex-config", category: "agent", agent: "codex", format: "toml" },
930
+ { path: "~/.codex/AGENTS.md", name: "codex-agents-md", category: "rules", agent: "codex", format: "markdown" },
931
+ { path: "~/.gemini/settings.json", name: "gemini-settings", category: "agent", agent: "gemini", format: "json" },
932
+ { path: "~/.gemini/GEMINI.md", name: "gemini-gemini-md", category: "rules", agent: "gemini", format: "markdown" },
933
+ { path: "~/.claude.json", name: "claude-json", category: "mcp", agent: "claude", format: "json", description: "Claude Code global config (includes MCP server entries)" },
934
+ { path: "~/.zshrc", name: "zshrc", category: "shell", agent: "zsh" },
935
+ { path: "~/.zprofile", name: "zprofile", category: "shell", agent: "zsh" },
936
+ { path: "~/.bashrc", name: "bashrc", category: "shell", agent: "zsh" },
937
+ { path: "~/.bash_profile", name: "bash-profile", category: "shell", agent: "zsh" },
938
+ { path: "~/.gitconfig", name: "gitconfig", category: "git", agent: "git", format: "ini" },
939
+ { path: "~/.gitignore_global", name: "gitignore-global", category: "git", agent: "git" },
940
+ { path: "~/.npmrc", name: "npmrc", category: "tools", agent: "npm", format: "ini" },
941
+ { path: "~/.bunfig.toml", name: "bunfig", category: "tools", agent: "global", format: "toml" }
942
+ ];
943
+ PROJECT_CONFIG_FILES = [
944
+ { file: "CLAUDE.md", category: "rules", agent: "claude", format: "markdown" },
945
+ { file: ".claude/settings.json", category: "agent", agent: "claude", format: "json" },
946
+ { file: ".claude/settings.local.json", category: "agent", agent: "claude", format: "json" },
947
+ { file: ".mcp.json", category: "mcp", agent: "claude", format: "json" },
948
+ { file: "AGENTS.md", category: "rules", agent: "codex", format: "markdown" },
949
+ { file: ".codex/AGENTS.md", category: "rules", agent: "codex", format: "markdown" },
950
+ { file: "GEMINI.md", category: "rules", agent: "gemini", format: "markdown" }
951
+ ];
952
+ });
953
+
954
+ // src/mcp/index.ts
955
+ init_configs();
956
+ init_apply();
957
+ init_sync();
958
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
959
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
960
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
498
961
 
499
962
  // src/db/profiles.ts
963
+ init_types();
964
+ init_database();
965
+ init_configs();
500
966
  function rowToProfile(row) {
501
967
  return { ...row };
502
968
  }
@@ -522,6 +988,8 @@ function getProfileConfigs(profileIdOrSlug, db) {
522
988
  }
523
989
 
524
990
  // src/mcp/index.ts
991
+ init_apply();
992
+ init_snapshots();
525
993
  var TOOL_DOCS = {
526
994
  list_configs: "List configs. Params: category?, agent?, kind?, search?. Returns array of config objects.",
527
995
  get_config: "Get a config by id or slug. Returns full config including content.",
@@ -532,6 +1000,8 @@ var TOOL_DOCS = {
532
1000
  list_profiles: "List all profiles. Returns array of profile objects.",
533
1001
  apply_profile: "Apply all configs in a profile to disk. Params: id_or_slug, dry_run?. Returns array of apply results.",
534
1002
  get_snapshot: "Get snapshot(s) for a config. Params: config_id_or_slug, version?. Returns latest snapshot or specific version.",
1003
+ get_status: "Single-call orientation. Returns: total configs, counts by category, drifted count, unredacted secrets, templates, DB path.",
1004
+ sync_known: "Sync all known config files from disk into DB. Params: agent?, category?. Replaces sync_directory for standard use.",
535
1005
  search_tools: "Search tool descriptions. Params: query. Returns matching tool names and descriptions.",
536
1006
  describe_tools: "Get full descriptions for tools. Params: names? (array). Returns tool docs."
537
1007
  };
@@ -545,6 +1015,8 @@ var LEAN_TOOLS = [
545
1015
  { name: "list_profiles", inputSchema: { type: "object", properties: {} } },
546
1016
  { name: "apply_profile", inputSchema: { type: "object", properties: { id_or_slug: { type: "string" }, dry_run: { type: "boolean" } }, required: ["id_or_slug"] } },
547
1017
  { name: "get_snapshot", inputSchema: { type: "object", properties: { config_id_or_slug: { type: "string" }, version: { type: "number" } }, required: ["config_id_or_slug"] } },
1018
+ { name: "get_status", inputSchema: { type: "object", properties: {} } },
1019
+ { name: "sync_known", inputSchema: { type: "object", properties: { agent: { type: "string" }, category: { type: "string" } } } },
548
1020
  { name: "search_tools", inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] } },
549
1021
  { name: "describe_tools", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" } } } } }
550
1022
  ];
@@ -628,6 +1100,29 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
628
1100
  const snaps = listSnapshots(config.id);
629
1101
  return ok(snaps[0] ?? null);
630
1102
  }
1103
+ case "get_status": {
1104
+ const stats = getConfigStats();
1105
+ const allConfigs = listConfigs({ kind: "file" });
1106
+ let drifted = 0, secrets = 0, templates = 0;
1107
+ for (const c of allConfigs) {
1108
+ if (c.is_template)
1109
+ templates++;
1110
+ }
1111
+ return ok({
1112
+ total: stats["total"] || 0,
1113
+ by_category: Object.fromEntries(Object.entries(stats).filter(([k]) => k !== "total")),
1114
+ templates,
1115
+ db_path: process.env["CONFIGS_DB_PATH"] || "~/.configs/configs.db"
1116
+ });
1117
+ }
1118
+ case "sync_known": {
1119
+ const { syncKnown: syncKnown2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
1120
+ const result = await syncKnown2({
1121
+ agent: args["agent"] || undefined,
1122
+ category: args["category"] || undefined
1123
+ });
1124
+ return ok(result);
1125
+ }
631
1126
  case "search_tools": {
632
1127
  const query = (args["query"] || "").toLowerCase();
633
1128
  const matches = Object.entries(TOOL_DOCS).filter(([k, v]) => k.includes(query) || v.toLowerCase().includes(query)).map(([name2, description]) => ({ name: name2, description }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/configs",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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",