@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.
Files changed (58) hide show
  1. package/README.md +264 -0
  2. package/dashboard/dist/assets/index-DQ3P1g1z.css +1 -0
  3. package/dashboard/dist/assets/index-DbXmAL_d.js +11 -0
  4. package/dashboard/dist/index.html +14 -0
  5. package/dashboard/dist/vite.svg +1 -0
  6. package/dist/cli/index.d.ts +3 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +3087 -0
  9. package/dist/db/configs.d.ts +10 -0
  10. package/dist/db/configs.d.ts.map +1 -0
  11. package/dist/db/configs.test.d.ts +2 -0
  12. package/dist/db/configs.test.d.ts.map +1 -0
  13. package/dist/db/database.d.ts +7 -0
  14. package/dist/db/database.d.ts.map +1 -0
  15. package/dist/db/machines.d.ts +8 -0
  16. package/dist/db/machines.d.ts.map +1 -0
  17. package/dist/db/machines.test.d.ts +2 -0
  18. package/dist/db/machines.test.d.ts.map +1 -0
  19. package/dist/db/profiles.d.ts +11 -0
  20. package/dist/db/profiles.d.ts.map +1 -0
  21. package/dist/db/profiles.test.d.ts +2 -0
  22. package/dist/db/profiles.test.d.ts.map +1 -0
  23. package/dist/db/snapshots.d.ts +8 -0
  24. package/dist/db/snapshots.d.ts.map +1 -0
  25. package/dist/db/snapshots.test.d.ts +2 -0
  26. package/dist/db/snapshots.test.d.ts.map +1 -0
  27. package/dist/index.d.ts +17 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +896 -0
  30. package/dist/lib/apply.d.ts +11 -0
  31. package/dist/lib/apply.d.ts.map +1 -0
  32. package/dist/lib/apply.test.d.ts +2 -0
  33. package/dist/lib/apply.test.d.ts.map +1 -0
  34. package/dist/lib/export.d.ts +12 -0
  35. package/dist/lib/export.d.ts.map +1 -0
  36. package/dist/lib/import.d.ts +14 -0
  37. package/dist/lib/import.d.ts.map +1 -0
  38. package/dist/lib/sync.d.ts +19 -0
  39. package/dist/lib/sync.d.ts.map +1 -0
  40. package/dist/lib/sync.test.d.ts +2 -0
  41. package/dist/lib/sync.test.d.ts.map +1 -0
  42. package/dist/lib/template.d.ts +10 -0
  43. package/dist/lib/template.d.ts.map +1 -0
  44. package/dist/lib/template.test.d.ts +2 -0
  45. package/dist/lib/template.test.d.ts.map +1 -0
  46. package/dist/mcp/index.d.ts +3 -0
  47. package/dist/mcp/index.d.ts.map +1 -0
  48. package/dist/mcp/index.js +662 -0
  49. package/dist/mcp/mcp.test.d.ts +2 -0
  50. package/dist/mcp/mcp.test.d.ts.map +1 -0
  51. package/dist/server/index.d.ts +7 -0
  52. package/dist/server/index.d.ts.map +1 -0
  53. package/dist/server/index.js +2390 -0
  54. package/dist/server/server.test.d.ts +2 -0
  55. package/dist/server/server.test.d.ts.map +1 -0
  56. package/dist/types/index.d.ts +152 -0
  57. package/dist/types/index.d.ts.map +1 -0
  58. package/package.json +78 -0
package/dist/index.js ADDED
@@ -0,0 +1,896 @@
1
+ // @bun
2
+ // src/types/index.ts
3
+ var CONFIG_KINDS = ["file", "reference"];
4
+ var CONFIG_CATEGORIES = [
5
+ "agent",
6
+ "rules",
7
+ "mcp",
8
+ "shell",
9
+ "secrets_schema",
10
+ "workspace",
11
+ "git",
12
+ "tools"
13
+ ];
14
+ var CONFIG_AGENTS = [
15
+ "claude",
16
+ "codex",
17
+ "gemini",
18
+ "zsh",
19
+ "git",
20
+ "npm",
21
+ "global"
22
+ ];
23
+ var CONFIG_FORMATS = [
24
+ "text",
25
+ "json",
26
+ "toml",
27
+ "yaml",
28
+ "markdown",
29
+ "ini"
30
+ ];
31
+
32
+ class ConfigNotFoundError extends Error {
33
+ constructor(id) {
34
+ super(`Config not found: ${id}`);
35
+ this.name = "ConfigNotFoundError";
36
+ }
37
+ }
38
+
39
+ class ProfileNotFoundError extends Error {
40
+ constructor(id) {
41
+ super(`Profile not found: ${id}`);
42
+ this.name = "ProfileNotFoundError";
43
+ }
44
+ }
45
+
46
+ class ConfigApplyError extends Error {
47
+ constructor(message) {
48
+ super(message);
49
+ this.name = "ConfigApplyError";
50
+ }
51
+ }
52
+
53
+ class TemplateRenderError extends Error {
54
+ constructor(message) {
55
+ super(message);
56
+ this.name = "TemplateRenderError";
57
+ }
58
+ }
59
+ // src/db/database.ts
60
+ import { Database } from "bun:sqlite";
61
+ import { existsSync, mkdirSync } from "fs";
62
+ import { dirname, join, resolve } from "path";
63
+ import { randomUUID } from "crypto";
64
+ function getDbPath() {
65
+ if (process.env["CONFIGS_DB_PATH"]) {
66
+ return process.env["CONFIGS_DB_PATH"];
67
+ }
68
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
69
+ return join(home, ".configs", "configs.db");
70
+ }
71
+ function ensureDir(filePath) {
72
+ if (filePath === ":memory:" || filePath.startsWith("file::memory:"))
73
+ return;
74
+ const dir = dirname(resolve(filePath));
75
+ if (!existsSync(dir)) {
76
+ mkdirSync(dir, { recursive: true });
77
+ }
78
+ }
79
+ function uuid() {
80
+ return randomUUID();
81
+ }
82
+ function now() {
83
+ return new Date().toISOString();
84
+ }
85
+ function slugify(name) {
86
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
87
+ }
88
+ var MIGRATIONS = [
89
+ `
90
+ CREATE TABLE IF NOT EXISTS configs (
91
+ id TEXT PRIMARY KEY,
92
+ name TEXT NOT NULL,
93
+ slug TEXT NOT NULL UNIQUE,
94
+ kind TEXT NOT NULL DEFAULT 'file',
95
+ category TEXT NOT NULL,
96
+ agent TEXT NOT NULL DEFAULT 'global',
97
+ target_path TEXT,
98
+ format TEXT NOT NULL DEFAULT 'text',
99
+ content TEXT NOT NULL DEFAULT '',
100
+ description TEXT,
101
+ tags TEXT NOT NULL DEFAULT '[]',
102
+ is_template INTEGER NOT NULL DEFAULT 0,
103
+ version INTEGER NOT NULL DEFAULT 1,
104
+ created_at TEXT NOT NULL,
105
+ updated_at TEXT NOT NULL,
106
+ synced_at TEXT
107
+ );
108
+
109
+ CREATE TABLE IF NOT EXISTS config_snapshots (
110
+ id TEXT PRIMARY KEY,
111
+ config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
112
+ content TEXT NOT NULL,
113
+ version INTEGER NOT NULL,
114
+ created_at TEXT NOT NULL
115
+ );
116
+
117
+ CREATE TABLE IF NOT EXISTS profiles (
118
+ id TEXT PRIMARY KEY,
119
+ name TEXT NOT NULL,
120
+ slug TEXT NOT NULL UNIQUE,
121
+ description TEXT,
122
+ created_at TEXT NOT NULL,
123
+ updated_at TEXT NOT NULL
124
+ );
125
+
126
+ CREATE TABLE IF NOT EXISTS profile_configs (
127
+ profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
128
+ config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
129
+ sort_order INTEGER NOT NULL DEFAULT 0,
130
+ PRIMARY KEY (profile_id, config_id)
131
+ );
132
+
133
+ CREATE TABLE IF NOT EXISTS machines (
134
+ id TEXT PRIMARY KEY,
135
+ hostname TEXT NOT NULL UNIQUE,
136
+ os TEXT,
137
+ last_applied_at TEXT,
138
+ created_at TEXT NOT NULL
139
+ );
140
+
141
+ CREATE TABLE IF NOT EXISTS schema_version (
142
+ version INTEGER PRIMARY KEY
143
+ );
144
+
145
+ INSERT OR IGNORE INTO schema_version (version) VALUES (1);
146
+ `
147
+ ];
148
+ var _db = null;
149
+ function getDatabase(path) {
150
+ if (_db)
151
+ return _db;
152
+ const dbPath = path || getDbPath();
153
+ ensureDir(dbPath);
154
+ const db = new Database(dbPath);
155
+ db.run("PRAGMA journal_mode = WAL");
156
+ db.run("PRAGMA foreign_keys = ON");
157
+ applyMigrations(db);
158
+ _db = db;
159
+ return db;
160
+ }
161
+ function resetDatabase() {
162
+ _db = null;
163
+ }
164
+ function applyMigrations(db) {
165
+ let currentVersion = 0;
166
+ try {
167
+ const row = db.query("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get();
168
+ currentVersion = row?.version ?? 0;
169
+ } catch {
170
+ currentVersion = 0;
171
+ }
172
+ for (let i = currentVersion;i < MIGRATIONS.length; i++) {
173
+ db.run(MIGRATIONS[i]);
174
+ db.run(`INSERT OR REPLACE INTO schema_version (version) VALUES (${i + 1})`);
175
+ }
176
+ }
177
+
178
+ // src/db/configs.ts
179
+ function rowToConfig(row) {
180
+ return {
181
+ ...row,
182
+ tags: JSON.parse(row.tags || "[]"),
183
+ is_template: !!row.is_template,
184
+ kind: row.kind,
185
+ category: row.category,
186
+ agent: row.agent,
187
+ format: row.format
188
+ };
189
+ }
190
+ function uniqueSlug(name, db, excludeId) {
191
+ const base = slugify(name);
192
+ let slug = base;
193
+ let i = 1;
194
+ while (true) {
195
+ const existing = db.query("SELECT id FROM configs WHERE slug = ?").get(slug);
196
+ if (!existing || existing.id === excludeId)
197
+ return slug;
198
+ slug = `${base}-${i++}`;
199
+ }
200
+ }
201
+ function createConfig(input, db) {
202
+ const d = db || getDatabase();
203
+ const id = uuid();
204
+ const ts = now();
205
+ const slug = uniqueSlug(input.name, d);
206
+ const tags = JSON.stringify(input.tags || []);
207
+ 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)
208
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, NULL)`, [
209
+ id,
210
+ input.name,
211
+ slug,
212
+ input.kind ?? "file",
213
+ input.category,
214
+ input.agent ?? "global",
215
+ input.target_path ?? null,
216
+ input.format ?? "text",
217
+ input.content,
218
+ input.description ?? null,
219
+ tags,
220
+ input.is_template ? 1 : 0,
221
+ ts,
222
+ ts
223
+ ]);
224
+ return getConfig(id, d);
225
+ }
226
+ function getConfig(idOrSlug, db) {
227
+ const d = db || getDatabase();
228
+ const row = d.query("SELECT * FROM configs WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
229
+ if (!row)
230
+ throw new ConfigNotFoundError(idOrSlug);
231
+ return rowToConfig(row);
232
+ }
233
+ function getConfigById(id, db) {
234
+ const d = db || getDatabase();
235
+ const row = d.query("SELECT * FROM configs WHERE id = ?").get(id);
236
+ if (!row)
237
+ throw new ConfigNotFoundError(id);
238
+ return rowToConfig(row);
239
+ }
240
+ function listConfigs(filter, db) {
241
+ const d = db || getDatabase();
242
+ const conditions = [];
243
+ const params = [];
244
+ if (filter?.category) {
245
+ conditions.push("category = ?");
246
+ params.push(filter.category);
247
+ }
248
+ if (filter?.agent) {
249
+ conditions.push("agent = ?");
250
+ params.push(filter.agent);
251
+ }
252
+ if (filter?.kind) {
253
+ conditions.push("kind = ?");
254
+ params.push(filter.kind);
255
+ }
256
+ if (filter?.is_template !== undefined) {
257
+ conditions.push("is_template = ?");
258
+ params.push(filter.is_template ? 1 : 0);
259
+ }
260
+ if (filter?.search) {
261
+ conditions.push("(name LIKE ? OR description LIKE ? OR content LIKE ?)");
262
+ const q = `%${filter.search}%`;
263
+ params.push(q, q, q);
264
+ }
265
+ if (filter?.tags && filter.tags.length > 0) {
266
+ const tagConditions = filter.tags.map(() => "tags LIKE ?").join(" OR ");
267
+ conditions.push(`(${tagConditions})`);
268
+ for (const tag of filter.tags)
269
+ params.push(`%"${tag}"%`);
270
+ }
271
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
272
+ const rows = d.query(`SELECT * FROM configs ${where} ORDER BY category, name`).all(...params);
273
+ return rows.map(rowToConfig);
274
+ }
275
+ function updateConfig(idOrSlug, input, db) {
276
+ const d = db || getDatabase();
277
+ const existing = getConfig(idOrSlug, d);
278
+ const ts = now();
279
+ const updates = ["updated_at = ?", "version = version + 1"];
280
+ const params = [ts];
281
+ if (input.name !== undefined) {
282
+ updates.push("name = ?", "slug = ?");
283
+ params.push(input.name, uniqueSlug(input.name, d, existing.id));
284
+ }
285
+ if (input.kind !== undefined) {
286
+ updates.push("kind = ?");
287
+ params.push(input.kind);
288
+ }
289
+ if (input.category !== undefined) {
290
+ updates.push("category = ?");
291
+ params.push(input.category);
292
+ }
293
+ if (input.agent !== undefined) {
294
+ updates.push("agent = ?");
295
+ params.push(input.agent);
296
+ }
297
+ if (input.target_path !== undefined) {
298
+ updates.push("target_path = ?");
299
+ params.push(input.target_path);
300
+ }
301
+ if (input.format !== undefined) {
302
+ updates.push("format = ?");
303
+ params.push(input.format);
304
+ }
305
+ if (input.content !== undefined) {
306
+ updates.push("content = ?");
307
+ params.push(input.content);
308
+ }
309
+ if (input.description !== undefined) {
310
+ updates.push("description = ?");
311
+ params.push(input.description);
312
+ }
313
+ if (input.tags !== undefined) {
314
+ updates.push("tags = ?");
315
+ params.push(JSON.stringify(input.tags));
316
+ }
317
+ if (input.is_template !== undefined) {
318
+ updates.push("is_template = ?");
319
+ params.push(input.is_template ? 1 : 0);
320
+ }
321
+ if (input.synced_at !== undefined) {
322
+ updates.push("synced_at = ?");
323
+ params.push(input.synced_at);
324
+ }
325
+ params.push(existing.id);
326
+ d.run(`UPDATE configs SET ${updates.join(", ")} WHERE id = ?`, params);
327
+ return getConfigById(existing.id, d);
328
+ }
329
+ function deleteConfig(idOrSlug, db) {
330
+ const d = db || getDatabase();
331
+ const existing = getConfig(idOrSlug, d);
332
+ d.run("DELETE FROM configs WHERE id = ?", [existing.id]);
333
+ }
334
+ function getConfigStats(db) {
335
+ const d = db || getDatabase();
336
+ const rows = d.query("SELECT category, COUNT(*) as count FROM configs GROUP BY category").all();
337
+ const stats = { total: 0 };
338
+ for (const row of rows) {
339
+ stats[row.category] = row.count;
340
+ stats["total"] = (stats["total"] || 0) + row.count;
341
+ }
342
+ return stats;
343
+ }
344
+ // src/db/snapshots.ts
345
+ function createSnapshot(configId, content, version, db) {
346
+ const d = db || getDatabase();
347
+ const id = uuid();
348
+ const ts = now();
349
+ d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
350
+ return { id, config_id: configId, content, version, created_at: ts };
351
+ }
352
+ function listSnapshots(configId, db) {
353
+ const d = db || getDatabase();
354
+ return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
355
+ }
356
+ function getSnapshot(id, db) {
357
+ const d = db || getDatabase();
358
+ return d.query("SELECT * FROM config_snapshots WHERE id = ?").get(id);
359
+ }
360
+ function getSnapshotByVersion(configId, version, db) {
361
+ const d = db || getDatabase();
362
+ return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
363
+ }
364
+ function pruneSnapshots(configId, keep = 10, db) {
365
+ const d = db || getDatabase();
366
+ const result = d.run(`DELETE FROM config_snapshots WHERE config_id = ? AND id NOT IN (
367
+ SELECT id FROM config_snapshots WHERE config_id = ? ORDER BY version DESC LIMIT ?
368
+ )`, [configId, configId, keep]);
369
+ return result.changes;
370
+ }
371
+ // src/db/profiles.ts
372
+ function rowToProfile(row) {
373
+ return { ...row };
374
+ }
375
+ function uniqueProfileSlug(name, db, excludeId) {
376
+ const base = slugify(name);
377
+ let slug = base;
378
+ let i = 1;
379
+ while (true) {
380
+ const existing = db.query("SELECT id FROM profiles WHERE slug = ?").get(slug);
381
+ if (!existing || existing.id === excludeId)
382
+ return slug;
383
+ slug = `${base}-${i++}`;
384
+ }
385
+ }
386
+ function createProfile(input, db) {
387
+ const d = db || getDatabase();
388
+ const id = uuid();
389
+ const ts = now();
390
+ const slug = uniqueProfileSlug(input.name, d);
391
+ d.run("INSERT INTO profiles (id, name, slug, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", [id, input.name, slug, input.description ?? null, ts, ts]);
392
+ return getProfile(id, d);
393
+ }
394
+ function getProfile(idOrSlug, db) {
395
+ const d = db || getDatabase();
396
+ const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
397
+ if (!row)
398
+ throw new ProfileNotFoundError(idOrSlug);
399
+ return rowToProfile(row);
400
+ }
401
+ function listProfiles(db) {
402
+ const d = db || getDatabase();
403
+ return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
404
+ }
405
+ function updateProfile(idOrSlug, input, db) {
406
+ const d = db || getDatabase();
407
+ const existing = getProfile(idOrSlug, d);
408
+ const ts = now();
409
+ const updates = ["updated_at = ?"];
410
+ const params = [ts];
411
+ if (input.name !== undefined) {
412
+ updates.push("name = ?", "slug = ?");
413
+ params.push(input.name, uniqueProfileSlug(input.name, d, existing.id));
414
+ }
415
+ if (input.description !== undefined) {
416
+ updates.push("description = ?");
417
+ params.push(input.description);
418
+ }
419
+ params.push(existing.id);
420
+ d.run(`UPDATE profiles SET ${updates.join(", ")} WHERE id = ?`, params);
421
+ return getProfile(existing.id, d);
422
+ }
423
+ function deleteProfile(idOrSlug, db) {
424
+ const d = db || getDatabase();
425
+ const existing = getProfile(idOrSlug, d);
426
+ d.run("DELETE FROM profiles WHERE id = ?", [existing.id]);
427
+ }
428
+ function addConfigToProfile(profileIdOrSlug, configId, db) {
429
+ const d = db || getDatabase();
430
+ const profile = getProfile(profileIdOrSlug, d);
431
+ const maxRow = d.query("SELECT MAX(sort_order) as max_order FROM profile_configs WHERE profile_id = ?").get(profile.id);
432
+ const order = (maxRow?.max_order ?? -1) + 1;
433
+ d.run("INSERT OR IGNORE INTO profile_configs (profile_id, config_id, sort_order) VALUES (?, ?, ?)", [profile.id, configId, order]);
434
+ }
435
+ function removeConfigFromProfile(profileIdOrSlug, configId, db) {
436
+ const d = db || getDatabase();
437
+ const profile = getProfile(profileIdOrSlug, d);
438
+ d.run("DELETE FROM profile_configs WHERE profile_id = ? AND config_id = ?", [profile.id, configId]);
439
+ }
440
+ function getProfileConfigs(profileIdOrSlug, db) {
441
+ const d = db || getDatabase();
442
+ const profile = getProfile(profileIdOrSlug, d);
443
+ const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
444
+ if (rows.length === 0)
445
+ return [];
446
+ const ids = rows.map((r) => r.config_id);
447
+ return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
448
+ }
449
+ // src/db/machines.ts
450
+ import { hostname, type } from "os";
451
+ function currentHostname() {
452
+ return hostname();
453
+ }
454
+ function currentOs() {
455
+ return type();
456
+ }
457
+ function registerMachine(hostnameStr, os, db) {
458
+ const d = db || getDatabase();
459
+ const h = hostnameStr ?? currentHostname();
460
+ const o = os ?? currentOs();
461
+ const existing = d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
462
+ if (existing)
463
+ return existing;
464
+ const id = uuid();
465
+ const ts = now();
466
+ d.run("INSERT INTO machines (id, hostname, os, last_applied_at, created_at) VALUES (?, ?, ?, NULL, ?)", [id, h, o, ts]);
467
+ return d.query("SELECT * FROM machines WHERE id = ?").get(id);
468
+ }
469
+ function updateMachineApplied(hostnameStr, db) {
470
+ const d = db || getDatabase();
471
+ const h = hostnameStr ?? currentHostname();
472
+ d.run("UPDATE machines SET last_applied_at = ? WHERE hostname = ?", [now(), h]);
473
+ }
474
+ function listMachines(db) {
475
+ const d = db || getDatabase();
476
+ return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
477
+ }
478
+ // src/lib/apply.ts
479
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
480
+ import { dirname as dirname2, resolve as resolve2 } from "path";
481
+ import { homedir } from "os";
482
+ function expandPath(p) {
483
+ if (p.startsWith("~/")) {
484
+ return resolve2(homedir(), p.slice(2));
485
+ }
486
+ return resolve2(p);
487
+ }
488
+ async function applyConfig(config, opts = {}) {
489
+ if (!config.target_path) {
490
+ throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
491
+ }
492
+ const path = expandPath(config.target_path);
493
+ const previousContent = existsSync2(path) ? readFileSync(path, "utf-8") : null;
494
+ const changed = previousContent !== config.content;
495
+ if (!opts.dryRun) {
496
+ const dir = dirname2(path);
497
+ if (!existsSync2(dir)) {
498
+ mkdirSync2(dir, { recursive: true });
499
+ }
500
+ if (previousContent !== null && changed) {
501
+ const db2 = opts.db || getDatabase();
502
+ createSnapshot(config.id, previousContent, config.version, db2);
503
+ }
504
+ writeFileSync(path, config.content, "utf-8");
505
+ const db = opts.db || getDatabase();
506
+ updateConfig(config.id, { synced_at: now() }, db);
507
+ }
508
+ return {
509
+ config_id: config.id,
510
+ path,
511
+ previous_content: previousContent,
512
+ new_content: config.content,
513
+ dry_run: opts.dryRun ?? false,
514
+ changed
515
+ };
516
+ }
517
+ async function applyConfigs(configs, opts = {}) {
518
+ const results = [];
519
+ for (const config of configs) {
520
+ if (config.kind === "reference")
521
+ continue;
522
+ results.push(await applyConfig(config, opts));
523
+ }
524
+ return results;
525
+ }
526
+ // src/lib/sync.ts
527
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
528
+ import { extname, join as join2, relative } from "path";
529
+ import { homedir as homedir2 } from "os";
530
+ function detectCategory(filePath) {
531
+ const p = filePath.toLowerCase().replace(homedir2(), "~");
532
+ if (p.includes("/.claude/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("gemini.md"))
533
+ return "rules";
534
+ if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.gemini/") || p.includes("/.cursor/"))
535
+ return "agent";
536
+ if (p.includes(".mcp.json") || p.includes("mcp"))
537
+ return "mcp";
538
+ if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc") || p.includes(".bash_profile"))
539
+ return "shell";
540
+ if (p.includes(".gitconfig") || p.includes(".gitignore"))
541
+ return "git";
542
+ if (p.includes(".npmrc") || p.includes("tsconfig") || p.includes("bunfig"))
543
+ return "tools";
544
+ if (p.includes(".secrets"))
545
+ return "secrets_schema";
546
+ return "tools";
547
+ }
548
+ function detectAgent(filePath) {
549
+ const p = filePath.toLowerCase().replace(homedir2(), "~");
550
+ if (p.includes("/.claude/") || p.endsWith("claude.md"))
551
+ return "claude";
552
+ if (p.includes("/.codex/") || p.endsWith("agents.md"))
553
+ return "codex";
554
+ if (p.includes("/.gemini/") || p.endsWith("gemini.md"))
555
+ return "gemini";
556
+ if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc"))
557
+ return "zsh";
558
+ if (p.includes(".gitconfig") || p.includes(".gitignore"))
559
+ return "git";
560
+ if (p.includes(".npmrc"))
561
+ return "npm";
562
+ return "global";
563
+ }
564
+ function detectFormat(filePath) {
565
+ const ext = extname(filePath).toLowerCase();
566
+ if (ext === ".json")
567
+ return "json";
568
+ if (ext === ".toml")
569
+ return "toml";
570
+ if (ext === ".yaml" || ext === ".yml")
571
+ return "yaml";
572
+ if (ext === ".md" || ext === ".markdown")
573
+ return "markdown";
574
+ if (ext === ".ini" || ext === ".cfg")
575
+ return "ini";
576
+ return "text";
577
+ }
578
+ var SKIP_PATTERNS = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
579
+ function shouldSkip(p) {
580
+ return SKIP_PATTERNS.some((pat) => p.includes(pat));
581
+ }
582
+ function walkDir(dir, files = []) {
583
+ const entries = readdirSync(dir, { withFileTypes: true });
584
+ for (const entry of entries) {
585
+ const full = join2(dir, entry.name);
586
+ if (shouldSkip(full))
587
+ continue;
588
+ if (entry.isDirectory()) {
589
+ walkDir(full, files);
590
+ } else if (entry.isFile()) {
591
+ files.push(full);
592
+ }
593
+ }
594
+ return files;
595
+ }
596
+ async function syncFromDir(dir, opts = {}) {
597
+ const d = opts.db || getDatabase();
598
+ const absDir = expandPath(dir);
599
+ if (!existsSync3(absDir)) {
600
+ return { added: 0, updated: 0, unchanged: 0, skipped: [`Directory not found: ${absDir}`] };
601
+ }
602
+ const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join2(absDir, f)).filter((f) => statSync(f).isFile());
603
+ const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
604
+ const allConfigs = listConfigs(undefined, d);
605
+ for (const file of files) {
606
+ if (shouldSkip(file)) {
607
+ result.skipped.push(file);
608
+ continue;
609
+ }
610
+ try {
611
+ const content = readFileSync2(file, "utf-8");
612
+ const targetPath = file.startsWith(homedir2()) ? file.replace(homedir2(), "~") : file;
613
+ const existing = allConfigs.find((c) => c.target_path === targetPath);
614
+ if (!existing) {
615
+ if (!opts.dryRun) {
616
+ const name = relative(absDir, file);
617
+ createConfig({
618
+ name,
619
+ category: detectCategory(file),
620
+ agent: detectAgent(file),
621
+ target_path: targetPath,
622
+ format: detectFormat(file),
623
+ content
624
+ }, d);
625
+ }
626
+ result.added++;
627
+ } else if (existing.content !== content) {
628
+ if (!opts.dryRun) {
629
+ updateConfig(existing.id, { content }, d);
630
+ }
631
+ result.updated++;
632
+ } else {
633
+ result.unchanged++;
634
+ }
635
+ } catch {
636
+ result.skipped.push(file);
637
+ }
638
+ }
639
+ return result;
640
+ }
641
+ async function syncToDir(dir, opts = {}) {
642
+ const d = opts.db || getDatabase();
643
+ const absDir = expandPath(dir);
644
+ const normalizedDir = dir.startsWith("~/") ? dir : absDir.replace(homedir2(), "~");
645
+ const configs = listConfigs(undefined, d).filter((c) => c.target_path && (c.target_path.startsWith(normalizedDir) || c.target_path.startsWith(absDir)));
646
+ const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
647
+ for (const config of configs) {
648
+ if (config.kind === "reference")
649
+ continue;
650
+ try {
651
+ const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
652
+ if (r.changed) {
653
+ existsSync3(expandPath(config.target_path)) ? result.updated++ : result.added++;
654
+ } else {
655
+ result.unchanged++;
656
+ }
657
+ } catch {
658
+ result.skipped.push(config.target_path || config.id);
659
+ }
660
+ }
661
+ return result;
662
+ }
663
+ function diffConfig(config) {
664
+ if (!config.target_path)
665
+ return "(reference \u2014 no target path)";
666
+ const path = expandPath(config.target_path);
667
+ if (!existsSync3(path))
668
+ return `(file not found on disk: ${path})`;
669
+ const diskContent = readFileSync2(path, "utf-8");
670
+ if (diskContent === config.content)
671
+ return "(no diff \u2014 identical)";
672
+ const stored = config.content.split(`
673
+ `);
674
+ const disk = diskContent.split(`
675
+ `);
676
+ const lines = [`--- stored (DB)`, `+++ disk (${path})`];
677
+ const maxLen = Math.max(stored.length, disk.length);
678
+ for (let i = 0;i < maxLen; i++) {
679
+ const s = stored[i];
680
+ const d = disk[i];
681
+ if (s === d) {
682
+ if (s !== undefined)
683
+ lines.push(` ${s}`);
684
+ } else {
685
+ if (s !== undefined)
686
+ lines.push(`-${s}`);
687
+ if (d !== undefined)
688
+ lines.push(`+${d}`);
689
+ }
690
+ }
691
+ return lines.join(`
692
+ `);
693
+ }
694
+ // src/lib/export.ts
695
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
696
+ import { join as join3, resolve as resolve4 } from "path";
697
+ import { tmpdir } from "os";
698
+ async function exportConfigs(outputPath, opts = {}) {
699
+ const d = opts.db || getDatabase();
700
+ const configs = listConfigs(opts.filter, d);
701
+ const absOutput = resolve4(outputPath);
702
+ const tmpDir = join3(tmpdir(), `configs-export-${Date.now()}`);
703
+ const contentsDir = join3(tmpDir, "contents");
704
+ try {
705
+ mkdirSync3(contentsDir, { recursive: true });
706
+ const manifest = {
707
+ version: "1.0.0",
708
+ exported_at: now(),
709
+ configs: configs.map(({ content: _content, ...meta }) => meta)
710
+ };
711
+ writeFileSync2(join3(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
712
+ for (const config of configs) {
713
+ const fileName = `${config.slug}.${config.format === "text" ? "txt" : config.format}`;
714
+ writeFileSync2(join3(contentsDir, fileName), config.content, "utf-8");
715
+ }
716
+ const proc = Bun.spawn(["tar", "czf", absOutput, "-C", tmpDir, "."], {
717
+ stdout: "pipe",
718
+ stderr: "pipe"
719
+ });
720
+ const exitCode = await proc.exited;
721
+ if (exitCode !== 0) {
722
+ const stderr = await new Response(proc.stderr).text();
723
+ throw new Error(`tar failed: ${stderr}`);
724
+ }
725
+ return { path: absOutput, count: configs.length };
726
+ } finally {
727
+ if (existsSync4(tmpDir)) {
728
+ rmSync(tmpDir, { recursive: true, force: true });
729
+ }
730
+ }
731
+ }
732
+ // src/lib/import.ts
733
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
734
+ import { join as join4, resolve as resolve5 } from "path";
735
+ import { tmpdir as tmpdir2 } from "os";
736
+ async function importConfigs(bundlePath, opts = {}) {
737
+ const d = opts.db || getDatabase();
738
+ const conflict = opts.conflict ?? "skip";
739
+ const absPath = resolve5(bundlePath);
740
+ const tmpDir = join4(tmpdir2(), `configs-import-${Date.now()}`);
741
+ const result = { created: 0, updated: 0, skipped: 0, errors: [] };
742
+ try {
743
+ mkdirSync4(tmpDir, { recursive: true });
744
+ const proc = Bun.spawn(["tar", "xzf", absPath, "-C", tmpDir], {
745
+ stdout: "pipe",
746
+ stderr: "pipe"
747
+ });
748
+ const exitCode = await proc.exited;
749
+ if (exitCode !== 0) {
750
+ const stderr = await new Response(proc.stderr).text();
751
+ throw new Error(`tar extraction failed: ${stderr}`);
752
+ }
753
+ const manifestPath = join4(tmpDir, "manifest.json");
754
+ if (!existsSync5(manifestPath))
755
+ throw new Error("Invalid bundle: missing manifest.json");
756
+ const manifest = JSON.parse(readFileSync3(manifestPath, "utf-8"));
757
+ for (const meta of manifest.configs) {
758
+ try {
759
+ const ext = meta.format === "text" ? "txt" : meta.format;
760
+ const contentFile = join4(tmpDir, "contents", `${meta.slug}.${ext}`);
761
+ const content = existsSync5(contentFile) ? readFileSync3(contentFile, "utf-8") : "";
762
+ let existing = null;
763
+ try {
764
+ existing = getConfig(meta.slug, d);
765
+ } catch {}
766
+ if (existing) {
767
+ if (conflict === "skip") {
768
+ result.skipped++;
769
+ } else if (conflict === "overwrite" || conflict === "version") {
770
+ updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags }, d);
771
+ result.updated++;
772
+ }
773
+ } else {
774
+ createConfig({
775
+ name: meta.name,
776
+ kind: meta.kind,
777
+ category: meta.category,
778
+ agent: meta.agent,
779
+ target_path: meta.target_path ?? undefined,
780
+ format: meta.format,
781
+ content,
782
+ description: meta.description ?? undefined,
783
+ tags: meta.tags,
784
+ is_template: meta.is_template
785
+ }, d);
786
+ result.created++;
787
+ }
788
+ } catch (err) {
789
+ result.errors.push(`${meta.slug}: ${err instanceof Error ? err.message : String(err)}`);
790
+ }
791
+ }
792
+ return result;
793
+ } finally {
794
+ if (existsSync5(tmpDir)) {
795
+ rmSync2(tmpDir, { recursive: true, force: true });
796
+ }
797
+ }
798
+ }
799
+ // src/lib/template.ts
800
+ var VAR_PATTERN = /\{\{([A-Z0-9_]+)(?::([^}]*))?\}\}/g;
801
+ function parseTemplateVars(content) {
802
+ const names = new Set;
803
+ let match;
804
+ VAR_PATTERN.lastIndex = 0;
805
+ while ((match = VAR_PATTERN.exec(content)) !== null) {
806
+ names.add(match[1]);
807
+ }
808
+ return Array.from(names);
809
+ }
810
+ function extractTemplateVars(content) {
811
+ const vars = new Map;
812
+ let match;
813
+ VAR_PATTERN.lastIndex = 0;
814
+ while ((match = VAR_PATTERN.exec(content)) !== null) {
815
+ const name = match[1];
816
+ const description = match[2] ?? null;
817
+ if (!vars.has(name)) {
818
+ vars.set(name, { name, description, required: true });
819
+ }
820
+ }
821
+ return Array.from(vars.values());
822
+ }
823
+ function renderTemplate(content, vars) {
824
+ const missing = [];
825
+ VAR_PATTERN.lastIndex = 0;
826
+ let match;
827
+ while ((match = VAR_PATTERN.exec(content)) !== null) {
828
+ const name = match[1];
829
+ if (!(name in vars))
830
+ missing.push(name);
831
+ }
832
+ if (missing.length > 0) {
833
+ throw new TemplateRenderError(`Missing required template variables: ${missing.join(", ")}`);
834
+ }
835
+ VAR_PATTERN.lastIndex = 0;
836
+ return content.replace(VAR_PATTERN, (_match, name) => vars[name] ?? "");
837
+ }
838
+ function isTemplate(content) {
839
+ VAR_PATTERN.lastIndex = 0;
840
+ return VAR_PATTERN.test(content);
841
+ }
842
+ export {
843
+ uuid,
844
+ updateProfile,
845
+ updateMachineApplied,
846
+ updateConfig,
847
+ syncToDir,
848
+ syncFromDir,
849
+ slugify,
850
+ resetDatabase,
851
+ renderTemplate,
852
+ removeConfigFromProfile,
853
+ registerMachine,
854
+ pruneSnapshots,
855
+ parseTemplateVars,
856
+ now,
857
+ listSnapshots,
858
+ listProfiles,
859
+ listMachines,
860
+ listConfigs,
861
+ isTemplate,
862
+ importConfigs,
863
+ getSnapshotByVersion,
864
+ getSnapshot,
865
+ getProfileConfigs,
866
+ getProfile,
867
+ getDatabase,
868
+ getConfigStats,
869
+ getConfigById,
870
+ getConfig,
871
+ extractTemplateVars,
872
+ exportConfigs,
873
+ expandPath,
874
+ diffConfig,
875
+ detectFormat,
876
+ detectCategory,
877
+ detectAgent,
878
+ deleteProfile,
879
+ deleteConfig,
880
+ currentOs,
881
+ currentHostname,
882
+ createSnapshot,
883
+ createProfile,
884
+ createConfig,
885
+ applyConfigs,
886
+ applyConfig,
887
+ addConfigToProfile,
888
+ TemplateRenderError,
889
+ ProfileNotFoundError,
890
+ ConfigNotFoundError,
891
+ ConfigApplyError,
892
+ CONFIG_KINDS,
893
+ CONFIG_FORMATS,
894
+ CONFIG_CATEGORIES,
895
+ CONFIG_AGENTS
896
+ };