@hasna/configs 0.1.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/README.md +264 -0
- package/dashboard/dist/assets/index-DQ3P1g1z.css +1 -0
- package/dashboard/dist/assets/index-DbXmAL_d.js +11 -0
- package/dashboard/dist/index.html +14 -0
- package/dashboard/dist/vite.svg +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +3087 -0
- package/dist/db/configs.d.ts +10 -0
- package/dist/db/configs.d.ts.map +1 -0
- package/dist/db/configs.test.d.ts +2 -0
- package/dist/db/configs.test.d.ts.map +1 -0
- package/dist/db/database.d.ts +7 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/machines.d.ts +8 -0
- package/dist/db/machines.d.ts.map +1 -0
- package/dist/db/machines.test.d.ts +2 -0
- package/dist/db/machines.test.d.ts.map +1 -0
- package/dist/db/profiles.d.ts +11 -0
- package/dist/db/profiles.d.ts.map +1 -0
- package/dist/db/profiles.test.d.ts +2 -0
- package/dist/db/profiles.test.d.ts.map +1 -0
- package/dist/db/snapshots.d.ts +8 -0
- package/dist/db/snapshots.d.ts.map +1 -0
- package/dist/db/snapshots.test.d.ts +2 -0
- package/dist/db/snapshots.test.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +896 -0
- package/dist/lib/apply.d.ts +11 -0
- package/dist/lib/apply.d.ts.map +1 -0
- package/dist/lib/apply.test.d.ts +2 -0
- package/dist/lib/apply.test.d.ts.map +1 -0
- package/dist/lib/export.d.ts +12 -0
- package/dist/lib/export.d.ts.map +1 -0
- package/dist/lib/import.d.ts +14 -0
- package/dist/lib/import.d.ts.map +1 -0
- package/dist/lib/sync.d.ts +19 -0
- package/dist/lib/sync.d.ts.map +1 -0
- package/dist/lib/sync.test.d.ts +2 -0
- package/dist/lib/sync.test.d.ts.map +1 -0
- package/dist/lib/template.d.ts +10 -0
- package/dist/lib/template.d.ts.map +1 -0
- package/dist/lib/template.test.d.ts +2 -0
- package/dist/lib/template.test.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +662 -0
- package/dist/mcp/mcp.test.d.ts +2 -0
- package/dist/mcp/mcp.test.d.ts.map +1 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2390 -0
- package/dist/server/server.test.d.ts +2 -0
- package/dist/server/server.test.d.ts.map +1 -0
- package/dist/types/index.d.ts +152 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
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";
|
|
8
|
+
|
|
9
|
+
// 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
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/db/database.ts
|
|
32
|
+
import { Database } from "bun:sqlite";
|
|
33
|
+
import { existsSync, mkdirSync } from "fs";
|
|
34
|
+
import { dirname, join, resolve } from "path";
|
|
35
|
+
import { randomUUID } from "crypto";
|
|
36
|
+
function getDbPath() {
|
|
37
|
+
if (process.env["CONFIGS_DB_PATH"]) {
|
|
38
|
+
return process.env["CONFIGS_DB_PATH"];
|
|
39
|
+
}
|
|
40
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
41
|
+
return join(home, ".configs", "configs.db");
|
|
42
|
+
}
|
|
43
|
+
function ensureDir(filePath) {
|
|
44
|
+
if (filePath === ":memory:" || filePath.startsWith("file::memory:"))
|
|
45
|
+
return;
|
|
46
|
+
const dir = dirname(resolve(filePath));
|
|
47
|
+
if (!existsSync(dir)) {
|
|
48
|
+
mkdirSync(dir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function uuid() {
|
|
52
|
+
return randomUUID();
|
|
53
|
+
}
|
|
54
|
+
function now() {
|
|
55
|
+
return new Date().toISOString();
|
|
56
|
+
}
|
|
57
|
+
function slugify(name) {
|
|
58
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
59
|
+
}
|
|
60
|
+
var MIGRATIONS = [
|
|
61
|
+
`
|
|
62
|
+
CREATE TABLE IF NOT EXISTS configs (
|
|
63
|
+
id TEXT PRIMARY KEY,
|
|
64
|
+
name TEXT NOT NULL,
|
|
65
|
+
slug TEXT NOT NULL UNIQUE,
|
|
66
|
+
kind TEXT NOT NULL DEFAULT 'file',
|
|
67
|
+
category TEXT NOT NULL,
|
|
68
|
+
agent TEXT NOT NULL DEFAULT 'global',
|
|
69
|
+
target_path TEXT,
|
|
70
|
+
format TEXT NOT NULL DEFAULT 'text',
|
|
71
|
+
content TEXT NOT NULL DEFAULT '',
|
|
72
|
+
description TEXT,
|
|
73
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
74
|
+
is_template INTEGER NOT NULL DEFAULT 0,
|
|
75
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
76
|
+
created_at TEXT NOT NULL,
|
|
77
|
+
updated_at TEXT NOT NULL,
|
|
78
|
+
synced_at TEXT
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS config_snapshots (
|
|
82
|
+
id TEXT PRIMARY KEY,
|
|
83
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
84
|
+
content TEXT NOT NULL,
|
|
85
|
+
version INTEGER NOT NULL,
|
|
86
|
+
created_at TEXT NOT NULL
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE TABLE IF NOT EXISTS profiles (
|
|
90
|
+
id TEXT PRIMARY KEY,
|
|
91
|
+
name TEXT NOT NULL,
|
|
92
|
+
slug TEXT NOT NULL UNIQUE,
|
|
93
|
+
description TEXT,
|
|
94
|
+
created_at TEXT NOT NULL,
|
|
95
|
+
updated_at TEXT NOT NULL
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CREATE TABLE IF NOT EXISTS profile_configs (
|
|
99
|
+
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
100
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
101
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
102
|
+
PRIMARY KEY (profile_id, config_id)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
CREATE TABLE IF NOT EXISTS machines (
|
|
106
|
+
id TEXT PRIMARY KEY,
|
|
107
|
+
hostname TEXT NOT NULL UNIQUE,
|
|
108
|
+
os TEXT,
|
|
109
|
+
last_applied_at TEXT,
|
|
110
|
+
created_at TEXT NOT NULL
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
114
|
+
version INTEGER PRIMARY KEY
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
INSERT OR IGNORE INTO schema_version (version) VALUES (1);
|
|
118
|
+
`
|
|
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
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/db/configs.ts
|
|
148
|
+
function rowToConfig(row) {
|
|
149
|
+
return {
|
|
150
|
+
...row,
|
|
151
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
152
|
+
is_template: !!row.is_template,
|
|
153
|
+
kind: row.kind,
|
|
154
|
+
category: row.category,
|
|
155
|
+
agent: row.agent,
|
|
156
|
+
format: row.format
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function uniqueSlug(name, db, excludeId) {
|
|
160
|
+
const base = slugify(name);
|
|
161
|
+
let slug = base;
|
|
162
|
+
let i = 1;
|
|
163
|
+
while (true) {
|
|
164
|
+
const existing = db.query("SELECT id FROM configs WHERE slug = ?").get(slug);
|
|
165
|
+
if (!existing || existing.id === excludeId)
|
|
166
|
+
return slug;
|
|
167
|
+
slug = `${base}-${i++}`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function createConfig(input, db) {
|
|
171
|
+
const d = db || getDatabase();
|
|
172
|
+
const id = uuid();
|
|
173
|
+
const ts = now();
|
|
174
|
+
const slug = uniqueSlug(input.name, d);
|
|
175
|
+
const tags = JSON.stringify(input.tags || []);
|
|
176
|
+
d.run(`INSERT INTO configs (id, name, slug, kind, category, agent, target_path, format, content, description, tags, is_template, version, created_at, updated_at, synced_at)
|
|
177
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, NULL)`, [
|
|
178
|
+
id,
|
|
179
|
+
input.name,
|
|
180
|
+
slug,
|
|
181
|
+
input.kind ?? "file",
|
|
182
|
+
input.category,
|
|
183
|
+
input.agent ?? "global",
|
|
184
|
+
input.target_path ?? null,
|
|
185
|
+
input.format ?? "text",
|
|
186
|
+
input.content,
|
|
187
|
+
input.description ?? null,
|
|
188
|
+
tags,
|
|
189
|
+
input.is_template ? 1 : 0,
|
|
190
|
+
ts,
|
|
191
|
+
ts
|
|
192
|
+
]);
|
|
193
|
+
return getConfig(id, d);
|
|
194
|
+
}
|
|
195
|
+
function getConfig(idOrSlug, db) {
|
|
196
|
+
const d = db || getDatabase();
|
|
197
|
+
const row = d.query("SELECT * FROM configs WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
|
|
198
|
+
if (!row)
|
|
199
|
+
throw new ConfigNotFoundError(idOrSlug);
|
|
200
|
+
return rowToConfig(row);
|
|
201
|
+
}
|
|
202
|
+
function getConfigById(id, db) {
|
|
203
|
+
const d = db || getDatabase();
|
|
204
|
+
const row = d.query("SELECT * FROM configs WHERE id = ?").get(id);
|
|
205
|
+
if (!row)
|
|
206
|
+
throw new ConfigNotFoundError(id);
|
|
207
|
+
return rowToConfig(row);
|
|
208
|
+
}
|
|
209
|
+
function listConfigs(filter, db) {
|
|
210
|
+
const d = db || getDatabase();
|
|
211
|
+
const conditions = [];
|
|
212
|
+
const params = [];
|
|
213
|
+
if (filter?.category) {
|
|
214
|
+
conditions.push("category = ?");
|
|
215
|
+
params.push(filter.category);
|
|
216
|
+
}
|
|
217
|
+
if (filter?.agent) {
|
|
218
|
+
conditions.push("agent = ?");
|
|
219
|
+
params.push(filter.agent);
|
|
220
|
+
}
|
|
221
|
+
if (filter?.kind) {
|
|
222
|
+
conditions.push("kind = ?");
|
|
223
|
+
params.push(filter.kind);
|
|
224
|
+
}
|
|
225
|
+
if (filter?.is_template !== undefined) {
|
|
226
|
+
conditions.push("is_template = ?");
|
|
227
|
+
params.push(filter.is_template ? 1 : 0);
|
|
228
|
+
}
|
|
229
|
+
if (filter?.search) {
|
|
230
|
+
conditions.push("(name LIKE ? OR description LIKE ? OR content LIKE ?)");
|
|
231
|
+
const q = `%${filter.search}%`;
|
|
232
|
+
params.push(q, q, q);
|
|
233
|
+
}
|
|
234
|
+
if (filter?.tags && filter.tags.length > 0) {
|
|
235
|
+
const tagConditions = filter.tags.map(() => "tags LIKE ?").join(" OR ");
|
|
236
|
+
conditions.push(`(${tagConditions})`);
|
|
237
|
+
for (const tag of filter.tags)
|
|
238
|
+
params.push(`%"${tag}"%`);
|
|
239
|
+
}
|
|
240
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
241
|
+
const rows = d.query(`SELECT * FROM configs ${where} ORDER BY category, name`).all(...params);
|
|
242
|
+
return rows.map(rowToConfig);
|
|
243
|
+
}
|
|
244
|
+
function updateConfig(idOrSlug, input, db) {
|
|
245
|
+
const d = db || getDatabase();
|
|
246
|
+
const existing = getConfig(idOrSlug, d);
|
|
247
|
+
const ts = now();
|
|
248
|
+
const updates = ["updated_at = ?", "version = version + 1"];
|
|
249
|
+
const params = [ts];
|
|
250
|
+
if (input.name !== undefined) {
|
|
251
|
+
updates.push("name = ?", "slug = ?");
|
|
252
|
+
params.push(input.name, uniqueSlug(input.name, d, existing.id));
|
|
253
|
+
}
|
|
254
|
+
if (input.kind !== undefined) {
|
|
255
|
+
updates.push("kind = ?");
|
|
256
|
+
params.push(input.kind);
|
|
257
|
+
}
|
|
258
|
+
if (input.category !== undefined) {
|
|
259
|
+
updates.push("category = ?");
|
|
260
|
+
params.push(input.category);
|
|
261
|
+
}
|
|
262
|
+
if (input.agent !== undefined) {
|
|
263
|
+
updates.push("agent = ?");
|
|
264
|
+
params.push(input.agent);
|
|
265
|
+
}
|
|
266
|
+
if (input.target_path !== undefined) {
|
|
267
|
+
updates.push("target_path = ?");
|
|
268
|
+
params.push(input.target_path);
|
|
269
|
+
}
|
|
270
|
+
if (input.format !== undefined) {
|
|
271
|
+
updates.push("format = ?");
|
|
272
|
+
params.push(input.format);
|
|
273
|
+
}
|
|
274
|
+
if (input.content !== undefined) {
|
|
275
|
+
updates.push("content = ?");
|
|
276
|
+
params.push(input.content);
|
|
277
|
+
}
|
|
278
|
+
if (input.description !== undefined) {
|
|
279
|
+
updates.push("description = ?");
|
|
280
|
+
params.push(input.description);
|
|
281
|
+
}
|
|
282
|
+
if (input.tags !== undefined) {
|
|
283
|
+
updates.push("tags = ?");
|
|
284
|
+
params.push(JSON.stringify(input.tags));
|
|
285
|
+
}
|
|
286
|
+
if (input.is_template !== undefined) {
|
|
287
|
+
updates.push("is_template = ?");
|
|
288
|
+
params.push(input.is_template ? 1 : 0);
|
|
289
|
+
}
|
|
290
|
+
if (input.synced_at !== undefined) {
|
|
291
|
+
updates.push("synced_at = ?");
|
|
292
|
+
params.push(input.synced_at);
|
|
293
|
+
}
|
|
294
|
+
params.push(existing.id);
|
|
295
|
+
d.run(`UPDATE configs SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
296
|
+
return getConfigById(existing.id, d);
|
|
297
|
+
}
|
|
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";
|
|
303
|
+
|
|
304
|
+
// src/db/snapshots.ts
|
|
305
|
+
function createSnapshot(configId, content, version, db) {
|
|
306
|
+
const d = db || getDatabase();
|
|
307
|
+
const id = uuid();
|
|
308
|
+
const ts = now();
|
|
309
|
+
d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
|
|
310
|
+
return { id, config_id: configId, content, version, created_at: ts };
|
|
311
|
+
}
|
|
312
|
+
function listSnapshots(configId, db) {
|
|
313
|
+
const d = db || getDatabase();
|
|
314
|
+
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
|
|
315
|
+
}
|
|
316
|
+
function getSnapshotByVersion(configId, version, db) {
|
|
317
|
+
const d = db || getDatabase();
|
|
318
|
+
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/lib/apply.ts
|
|
322
|
+
function expandPath(p) {
|
|
323
|
+
if (p.startsWith("~/")) {
|
|
324
|
+
return resolve2(homedir(), p.slice(2));
|
|
325
|
+
}
|
|
326
|
+
return resolve2(p);
|
|
327
|
+
}
|
|
328
|
+
async function applyConfig(config, opts = {}) {
|
|
329
|
+
if (!config.target_path) {
|
|
330
|
+
throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
|
|
331
|
+
}
|
|
332
|
+
const path = expandPath(config.target_path);
|
|
333
|
+
const previousContent = existsSync2(path) ? readFileSync(path, "utf-8") : null;
|
|
334
|
+
const changed = previousContent !== config.content;
|
|
335
|
+
if (!opts.dryRun) {
|
|
336
|
+
const dir = dirname2(path);
|
|
337
|
+
if (!existsSync2(dir)) {
|
|
338
|
+
mkdirSync2(dir, { recursive: true });
|
|
339
|
+
}
|
|
340
|
+
if (previousContent !== null && changed) {
|
|
341
|
+
const db2 = opts.db || getDatabase();
|
|
342
|
+
createSnapshot(config.id, previousContent, config.version, db2);
|
|
343
|
+
}
|
|
344
|
+
writeFileSync(path, config.content, "utf-8");
|
|
345
|
+
const db = opts.db || getDatabase();
|
|
346
|
+
updateConfig(config.id, { synced_at: now() }, db);
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
config_id: config.id,
|
|
350
|
+
path,
|
|
351
|
+
previous_content: previousContent,
|
|
352
|
+
new_content: config.content,
|
|
353
|
+
dry_run: opts.dryRun ?? false,
|
|
354
|
+
changed
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
async function applyConfigs(configs, opts = {}) {
|
|
358
|
+
const results = [];
|
|
359
|
+
for (const config of configs) {
|
|
360
|
+
if (config.kind === "reference")
|
|
361
|
+
continue;
|
|
362
|
+
results.push(await applyConfig(config, opts));
|
|
363
|
+
}
|
|
364
|
+
return results;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/lib/sync.ts
|
|
368
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
369
|
+
import { extname, join as join2, relative } from "path";
|
|
370
|
+
import { homedir as homedir2 } from "os";
|
|
371
|
+
function detectCategory(filePath) {
|
|
372
|
+
const p = filePath.toLowerCase().replace(homedir2(), "~");
|
|
373
|
+
if (p.includes("/.claude/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("gemini.md"))
|
|
374
|
+
return "rules";
|
|
375
|
+
if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.gemini/") || p.includes("/.cursor/"))
|
|
376
|
+
return "agent";
|
|
377
|
+
if (p.includes(".mcp.json") || p.includes("mcp"))
|
|
378
|
+
return "mcp";
|
|
379
|
+
if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc") || p.includes(".bash_profile"))
|
|
380
|
+
return "shell";
|
|
381
|
+
if (p.includes(".gitconfig") || p.includes(".gitignore"))
|
|
382
|
+
return "git";
|
|
383
|
+
if (p.includes(".npmrc") || p.includes("tsconfig") || p.includes("bunfig"))
|
|
384
|
+
return "tools";
|
|
385
|
+
if (p.includes(".secrets"))
|
|
386
|
+
return "secrets_schema";
|
|
387
|
+
return "tools";
|
|
388
|
+
}
|
|
389
|
+
function detectAgent(filePath) {
|
|
390
|
+
const p = filePath.toLowerCase().replace(homedir2(), "~");
|
|
391
|
+
if (p.includes("/.claude/") || p.endsWith("claude.md"))
|
|
392
|
+
return "claude";
|
|
393
|
+
if (p.includes("/.codex/") || p.endsWith("agents.md"))
|
|
394
|
+
return "codex";
|
|
395
|
+
if (p.includes("/.gemini/") || p.endsWith("gemini.md"))
|
|
396
|
+
return "gemini";
|
|
397
|
+
if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc"))
|
|
398
|
+
return "zsh";
|
|
399
|
+
if (p.includes(".gitconfig") || p.includes(".gitignore"))
|
|
400
|
+
return "git";
|
|
401
|
+
if (p.includes(".npmrc"))
|
|
402
|
+
return "npm";
|
|
403
|
+
return "global";
|
|
404
|
+
}
|
|
405
|
+
function detectFormat(filePath) {
|
|
406
|
+
const ext = extname(filePath).toLowerCase();
|
|
407
|
+
if (ext === ".json")
|
|
408
|
+
return "json";
|
|
409
|
+
if (ext === ".toml")
|
|
410
|
+
return "toml";
|
|
411
|
+
if (ext === ".yaml" || ext === ".yml")
|
|
412
|
+
return "yaml";
|
|
413
|
+
if (ext === ".md" || ext === ".markdown")
|
|
414
|
+
return "markdown";
|
|
415
|
+
if (ext === ".ini" || ext === ".cfg")
|
|
416
|
+
return "ini";
|
|
417
|
+
return "text";
|
|
418
|
+
}
|
|
419
|
+
var SKIP_PATTERNS = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
|
|
420
|
+
function shouldSkip(p) {
|
|
421
|
+
return SKIP_PATTERNS.some((pat) => p.includes(pat));
|
|
422
|
+
}
|
|
423
|
+
function walkDir(dir, files = []) {
|
|
424
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
425
|
+
for (const entry of entries) {
|
|
426
|
+
const full = join2(dir, entry.name);
|
|
427
|
+
if (shouldSkip(full))
|
|
428
|
+
continue;
|
|
429
|
+
if (entry.isDirectory()) {
|
|
430
|
+
walkDir(full, files);
|
|
431
|
+
} else if (entry.isFile()) {
|
|
432
|
+
files.push(full);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return files;
|
|
436
|
+
}
|
|
437
|
+
async function syncFromDir(dir, opts = {}) {
|
|
438
|
+
const d = opts.db || getDatabase();
|
|
439
|
+
const absDir = expandPath(dir);
|
|
440
|
+
if (!existsSync3(absDir)) {
|
|
441
|
+
return { added: 0, updated: 0, unchanged: 0, skipped: [`Directory not found: ${absDir}`] };
|
|
442
|
+
}
|
|
443
|
+
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join2(absDir, f)).filter((f) => statSync(f).isFile());
|
|
444
|
+
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
445
|
+
const allConfigs = listConfigs(undefined, d);
|
|
446
|
+
for (const file of files) {
|
|
447
|
+
if (shouldSkip(file)) {
|
|
448
|
+
result.skipped.push(file);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const content = readFileSync2(file, "utf-8");
|
|
453
|
+
const targetPath = file.startsWith(homedir2()) ? file.replace(homedir2(), "~") : file;
|
|
454
|
+
const existing = allConfigs.find((c) => c.target_path === targetPath);
|
|
455
|
+
if (!existing) {
|
|
456
|
+
if (!opts.dryRun) {
|
|
457
|
+
const name = relative(absDir, file);
|
|
458
|
+
createConfig({
|
|
459
|
+
name,
|
|
460
|
+
category: detectCategory(file),
|
|
461
|
+
agent: detectAgent(file),
|
|
462
|
+
target_path: targetPath,
|
|
463
|
+
format: detectFormat(file),
|
|
464
|
+
content
|
|
465
|
+
}, d);
|
|
466
|
+
}
|
|
467
|
+
result.added++;
|
|
468
|
+
} else if (existing.content !== content) {
|
|
469
|
+
if (!opts.dryRun) {
|
|
470
|
+
updateConfig(existing.id, { content }, d);
|
|
471
|
+
}
|
|
472
|
+
result.updated++;
|
|
473
|
+
} else {
|
|
474
|
+
result.unchanged++;
|
|
475
|
+
}
|
|
476
|
+
} catch {
|
|
477
|
+
result.skipped.push(file);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
async function syncToDir(dir, opts = {}) {
|
|
483
|
+
const d = opts.db || getDatabase();
|
|
484
|
+
const absDir = expandPath(dir);
|
|
485
|
+
const normalizedDir = dir.startsWith("~/") ? dir : absDir.replace(homedir2(), "~");
|
|
486
|
+
const configs = listConfigs(undefined, d).filter((c) => c.target_path && (c.target_path.startsWith(normalizedDir) || c.target_path.startsWith(absDir)));
|
|
487
|
+
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
488
|
+
for (const config of configs) {
|
|
489
|
+
if (config.kind === "reference")
|
|
490
|
+
continue;
|
|
491
|
+
try {
|
|
492
|
+
const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
|
|
493
|
+
if (r.changed) {
|
|
494
|
+
existsSync3(expandPath(config.target_path)) ? result.updated++ : result.added++;
|
|
495
|
+
} else {
|
|
496
|
+
result.unchanged++;
|
|
497
|
+
}
|
|
498
|
+
} catch {
|
|
499
|
+
result.skipped.push(config.target_path || config.id);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return result;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/db/profiles.ts
|
|
506
|
+
function rowToProfile(row) {
|
|
507
|
+
return { ...row };
|
|
508
|
+
}
|
|
509
|
+
function getProfile(idOrSlug, db) {
|
|
510
|
+
const d = db || getDatabase();
|
|
511
|
+
const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
|
|
512
|
+
if (!row)
|
|
513
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
514
|
+
return rowToProfile(row);
|
|
515
|
+
}
|
|
516
|
+
function listProfiles(db) {
|
|
517
|
+
const d = db || getDatabase();
|
|
518
|
+
return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
|
|
519
|
+
}
|
|
520
|
+
function getProfileConfigs(profileIdOrSlug, db) {
|
|
521
|
+
const d = db || getDatabase();
|
|
522
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
523
|
+
const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
|
|
524
|
+
if (rows.length === 0)
|
|
525
|
+
return [];
|
|
526
|
+
const ids = rows.map((r) => r.config_id);
|
|
527
|
+
return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/mcp/index.ts
|
|
531
|
+
var TOOL_DOCS = {
|
|
532
|
+
list_configs: "List configs. Params: category?, agent?, kind?, search?. Returns array of config objects.",
|
|
533
|
+
get_config: "Get a config by id or slug. Returns full config including content.",
|
|
534
|
+
create_config: "Create a new config. Required: name, content, category. Optional: agent, target_path, kind, format, tags, description, is_template.",
|
|
535
|
+
update_config: "Update a config by id or slug. Optional: content, name, tags, description, category, agent, target_path.",
|
|
536
|
+
apply_config: "Apply a config to its target_path on disk. Params: id_or_slug, dry_run?. Returns apply result.",
|
|
537
|
+
sync_directory: "Sync a directory with the DB. Params: dir, direction ('from_disk'|'to_disk'). Returns sync result.",
|
|
538
|
+
list_profiles: "List all profiles. Returns array of profile objects.",
|
|
539
|
+
apply_profile: "Apply all configs in a profile to disk. Params: id_or_slug, dry_run?. Returns array of apply results.",
|
|
540
|
+
get_snapshot: "Get snapshot(s) for a config. Params: config_id_or_slug, version?. Returns latest snapshot or specific version.",
|
|
541
|
+
search_tools: "Search tool descriptions. Params: query. Returns matching tool names and descriptions.",
|
|
542
|
+
describe_tools: "Get full descriptions for tools. Params: names? (array). Returns tool docs."
|
|
543
|
+
};
|
|
544
|
+
var LEAN_TOOLS = [
|
|
545
|
+
{ name: "list_configs", inputSchema: { type: "object", properties: { category: { type: "string" }, agent: { type: "string" }, kind: { type: "string" }, search: { type: "string" } } } },
|
|
546
|
+
{ name: "get_config", inputSchema: { type: "object", properties: { id_or_slug: { type: "string" } }, required: ["id_or_slug"] } },
|
|
547
|
+
{ name: "create_config", inputSchema: { type: "object", properties: { name: { type: "string" }, content: { type: "string" }, category: { type: "string" }, agent: { type: "string" }, target_path: { type: "string" }, kind: { type: "string" }, format: { type: "string" }, tags: { type: "array", items: { type: "string" } }, description: { type: "string" }, is_template: { type: "boolean" } }, required: ["name", "content", "category"] } },
|
|
548
|
+
{ name: "update_config", inputSchema: { type: "object", properties: { id_or_slug: { type: "string" }, content: { type: "string" }, name: { type: "string" }, tags: { type: "array", items: { type: "string" } }, description: { type: "string" }, category: { type: "string" }, agent: { type: "string" }, target_path: { type: "string" } }, required: ["id_or_slug"] } },
|
|
549
|
+
{ name: "apply_config", inputSchema: { type: "object", properties: { id_or_slug: { type: "string" }, dry_run: { type: "boolean" } }, required: ["id_or_slug"] } },
|
|
550
|
+
{ name: "sync_directory", inputSchema: { type: "object", properties: { dir: { type: "string" }, direction: { type: "string" } }, required: ["dir"] } },
|
|
551
|
+
{ name: "list_profiles", inputSchema: { type: "object", properties: {} } },
|
|
552
|
+
{ name: "apply_profile", inputSchema: { type: "object", properties: { id_or_slug: { type: "string" }, dry_run: { type: "boolean" } }, required: ["id_or_slug"] } },
|
|
553
|
+
{ name: "get_snapshot", inputSchema: { type: "object", properties: { config_id_or_slug: { type: "string" }, version: { type: "number" } }, required: ["config_id_or_slug"] } },
|
|
554
|
+
{ name: "search_tools", inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] } },
|
|
555
|
+
{ name: "describe_tools", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" } } } } }
|
|
556
|
+
];
|
|
557
|
+
function ok(data) {
|
|
558
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
559
|
+
}
|
|
560
|
+
function err(msg) {
|
|
561
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: msg }) }], isError: true };
|
|
562
|
+
}
|
|
563
|
+
var server = new Server({ name: "configs", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
564
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: LEAN_TOOLS }));
|
|
565
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
566
|
+
const { name, arguments: args = {} } = req.params;
|
|
567
|
+
try {
|
|
568
|
+
switch (name) {
|
|
569
|
+
case "list_configs": {
|
|
570
|
+
const configs = listConfigs({
|
|
571
|
+
category: args["category"] || undefined,
|
|
572
|
+
agent: args["agent"] || undefined,
|
|
573
|
+
kind: args["kind"] || undefined,
|
|
574
|
+
search: args["search"] || undefined
|
|
575
|
+
});
|
|
576
|
+
return ok(configs.map((c) => ({ id: c.id, slug: c.slug, name: c.name, category: c.category, agent: c.agent, kind: c.kind, target_path: c.target_path, version: c.version })));
|
|
577
|
+
}
|
|
578
|
+
case "get_config": {
|
|
579
|
+
const c = getConfig(args["id_or_slug"]);
|
|
580
|
+
return ok(c);
|
|
581
|
+
}
|
|
582
|
+
case "create_config": {
|
|
583
|
+
const c = createConfig({
|
|
584
|
+
name: args["name"],
|
|
585
|
+
content: args["content"],
|
|
586
|
+
category: args["category"],
|
|
587
|
+
agent: args["agent"] || undefined,
|
|
588
|
+
target_path: args["target_path"] || undefined,
|
|
589
|
+
kind: args["kind"] || undefined,
|
|
590
|
+
format: args["format"] || undefined,
|
|
591
|
+
tags: args["tags"] || undefined,
|
|
592
|
+
description: args["description"] || undefined,
|
|
593
|
+
is_template: args["is_template"] || undefined
|
|
594
|
+
});
|
|
595
|
+
return ok({ id: c.id, slug: c.slug, name: c.name });
|
|
596
|
+
}
|
|
597
|
+
case "update_config": {
|
|
598
|
+
const c = updateConfig(args["id_or_slug"], {
|
|
599
|
+
content: args["content"],
|
|
600
|
+
name: args["name"],
|
|
601
|
+
tags: args["tags"],
|
|
602
|
+
description: args["description"],
|
|
603
|
+
category: args["category"],
|
|
604
|
+
agent: args["agent"],
|
|
605
|
+
target_path: args["target_path"]
|
|
606
|
+
});
|
|
607
|
+
return ok({ id: c.id, slug: c.slug, version: c.version });
|
|
608
|
+
}
|
|
609
|
+
case "apply_config": {
|
|
610
|
+
const config = getConfig(args["id_or_slug"]);
|
|
611
|
+
const result = await applyConfig(config, { dryRun: args["dry_run"] });
|
|
612
|
+
return ok(result);
|
|
613
|
+
}
|
|
614
|
+
case "sync_directory": {
|
|
615
|
+
const dir = args["dir"];
|
|
616
|
+
const direction = args["direction"] || "from_disk";
|
|
617
|
+
const result = direction === "to_disk" ? await syncToDir(dir) : await syncFromDir(dir);
|
|
618
|
+
return ok(result);
|
|
619
|
+
}
|
|
620
|
+
case "list_profiles": {
|
|
621
|
+
return ok(listProfiles());
|
|
622
|
+
}
|
|
623
|
+
case "apply_profile": {
|
|
624
|
+
const configs = getProfileConfigs(args["id_or_slug"]);
|
|
625
|
+
const results = await applyConfigs(configs, { dryRun: args["dry_run"] });
|
|
626
|
+
return ok(results);
|
|
627
|
+
}
|
|
628
|
+
case "get_snapshot": {
|
|
629
|
+
const config = getConfig(args["config_id_or_slug"]);
|
|
630
|
+
if (args["version"]) {
|
|
631
|
+
const snap = getSnapshotByVersion(config.id, args["version"]);
|
|
632
|
+
return snap ? ok(snap) : err("Snapshot not found");
|
|
633
|
+
}
|
|
634
|
+
const snaps = listSnapshots(config.id);
|
|
635
|
+
return ok(snaps[0] ?? null);
|
|
636
|
+
}
|
|
637
|
+
case "search_tools": {
|
|
638
|
+
const query = (args["query"] || "").toLowerCase();
|
|
639
|
+
const matches = Object.entries(TOOL_DOCS).filter(([k, v]) => k.includes(query) || v.toLowerCase().includes(query)).map(([name2, description]) => ({ name: name2, description }));
|
|
640
|
+
return ok(matches);
|
|
641
|
+
}
|
|
642
|
+
case "describe_tools": {
|
|
643
|
+
const names = args["names"];
|
|
644
|
+
if (names) {
|
|
645
|
+
return ok(Object.fromEntries(names.map((n) => [n, TOOL_DOCS[n] ?? "Unknown tool"])));
|
|
646
|
+
}
|
|
647
|
+
return ok(TOOL_DOCS);
|
|
648
|
+
}
|
|
649
|
+
default:
|
|
650
|
+
return err(`Unknown tool: ${name}`);
|
|
651
|
+
}
|
|
652
|
+
} catch (e) {
|
|
653
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
if (process.argv.includes("--claude")) {
|
|
657
|
+
const proc = Bun.spawn(["claude", "mcp", "add", "--transport", "stdio", "--scope", "user", "configs", "--", "configs-mcp"], { stdout: "inherit", stderr: "inherit" });
|
|
658
|
+
await proc.exited;
|
|
659
|
+
process.exit(0);
|
|
660
|
+
}
|
|
661
|
+
var transport = new StdioServerTransport;
|
|
662
|
+
await server.connect(transport);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.test.d.ts","sourceRoot":"","sources":["../../src/mcp/mcp.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":";;;;;AA2MA,wBAAgD"}
|