@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 +115 -22
- package/dist/mcp/index.js +558 -63
- package/package.json +1 -1
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) ||
|
|
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:
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
class ConfigApplyError extends Error {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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/
|
|
368
|
-
|
|
369
|
-
|
|
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.
|
|
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",
|