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