@hasna/configs 0.2.6 → 0.2.8
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 +46 -18
- package/dist/cli/index.js +25 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +361 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,9 +6,10 @@ AI coding agent configuration manager — store, version, apply, and share all y
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
bun install -g @hasna/configs
|
|
9
|
-
configs
|
|
10
|
-
configs
|
|
11
|
-
configs
|
|
9
|
+
configs init # first-time setup: sync all known configs + create profile
|
|
10
|
+
configs status # health check: drifted, secrets, templates
|
|
11
|
+
configs pull # re-sync from disk → DB
|
|
12
|
+
configs push # apply DB → disk
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
## What It Stores
|
|
@@ -84,22 +85,35 @@ configs add <path> ingest a file into the DB
|
|
|
84
85
|
configs apply <id> write config to its target_path
|
|
85
86
|
--dry-run preview without writing
|
|
86
87
|
|
|
87
|
-
configs diff
|
|
88
|
+
configs diff [id] show diff: stored vs disk (omit id for all)
|
|
89
|
+
configs compare <a> <b> diff two stored configs against each other
|
|
88
90
|
|
|
89
|
-
configs sync
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
--to-disk apply DB
|
|
91
|
+
configs sync sync known AI coding configs from disk
|
|
92
|
+
-a, --agent <agent> only sync this agent
|
|
93
|
+
-p, --project [dir] sync project-scoped configs (CLAUDE.md, .mcp.json)
|
|
94
|
+
--to-disk apply DB → disk instead
|
|
93
95
|
--dry-run preview
|
|
96
|
+
--list show which files would be synced
|
|
94
97
|
|
|
95
|
-
configs
|
|
96
|
-
|
|
97
|
-
-c, --category <cat> filter by category
|
|
98
|
-
|
|
99
|
-
configs import <file> import from tar.gz bundle
|
|
100
|
-
--overwrite overwrite existing configs
|
|
98
|
+
configs pull alias for sync (disk → DB)
|
|
99
|
+
configs push alias for sync --to-disk (DB → disk)
|
|
101
100
|
|
|
102
|
-
configs
|
|
101
|
+
configs export export as tar.gz bundle
|
|
102
|
+
configs import <file> import from tar.gz bundle (--overwrite)
|
|
103
|
+
configs backup timestamped export to ~/.configs/backups/
|
|
104
|
+
configs restore <file> import from backup (--overwrite)
|
|
105
|
+
|
|
106
|
+
configs init first-time setup: sync + seed + create profile
|
|
107
|
+
configs status health check: drifted, secrets, templates
|
|
108
|
+
configs whoami setup summary: DB path, counts by category
|
|
109
|
+
configs doctor validate syntax, permissions, missing files, secrets
|
|
110
|
+
configs scan [id] scan for unredacted secrets (--fix to redact)
|
|
111
|
+
configs watch auto-sync on file changes (polls every 3s)
|
|
112
|
+
configs update check for + install latest version
|
|
113
|
+
configs completions output zsh/bash completion script
|
|
114
|
+
|
|
115
|
+
configs mcp install install MCP server (--claude, --codex, --gemini, --all)
|
|
116
|
+
configs mcp uninstall remove MCP server
|
|
103
117
|
```
|
|
104
118
|
|
|
105
119
|
### Profiles
|
|
@@ -127,12 +141,26 @@ configs snapshot show <snapshot-id> # view content
|
|
|
127
141
|
configs snapshot restore <config> <id> # restore to that version
|
|
128
142
|
```
|
|
129
143
|
|
|
130
|
-
### Templates
|
|
144
|
+
### Templates & Secret Redaction
|
|
145
|
+
|
|
146
|
+
Secrets are automatically redacted when ingesting configs. Values matching API keys, tokens, passwords etc. are replaced with `{{VAR_NAME}}` placeholders.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
configs template vars npmrc # show: {{NPM_AUTH_TOKEN}}
|
|
150
|
+
configs template render npmrc --env --apply # fill from env vars, write to disk
|
|
151
|
+
configs template render npmrc --var NPM_AUTH_TOKEN=xxx --dry-run # preview
|
|
152
|
+
configs scan # check for unredacted secrets
|
|
153
|
+
configs scan --fix # redact any that slipped through
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Agent Profiles (Token Optimization)
|
|
131
157
|
|
|
132
|
-
|
|
158
|
+
Control which MCP tools are exposed via `CONFIGS_PROFILE` env var:
|
|
133
159
|
|
|
134
160
|
```bash
|
|
135
|
-
configs
|
|
161
|
+
CONFIGS_PROFILE=minimal configs-mcp # 3 tools: get_status, get_config, sync_known
|
|
162
|
+
CONFIGS_PROFILE=standard configs-mcp # 11 tools: CRUD + sync + profiles
|
|
163
|
+
CONFIGS_PROFILE=full configs-mcp # 13 tools (default)
|
|
136
164
|
```
|
|
137
165
|
|
|
138
166
|
## MCP Server
|
package/dist/cli/index.js
CHANGED
|
@@ -3411,7 +3411,7 @@ program.command("diff [id]").description("Show diff between stored config and di
|
|
|
3411
3411
|
process.exit(1);
|
|
3412
3412
|
}
|
|
3413
3413
|
});
|
|
3414
|
-
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) => {
|
|
3414
|
+
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("--all", "with --project: scan all subdirs for projects to sync").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) => {
|
|
3415
3415
|
if (opts.list) {
|
|
3416
3416
|
const targets = KNOWN_CONFIGS.filter((k) => {
|
|
3417
3417
|
if (opts.agent && k.agent !== opts.agent)
|
|
@@ -3428,6 +3428,30 @@ program.command("sync").description("Sync known AI coding configs from disk into
|
|
|
3428
3428
|
}
|
|
3429
3429
|
if (opts.project) {
|
|
3430
3430
|
const dir = typeof opts.project === "string" ? opts.project : process.cwd();
|
|
3431
|
+
if (opts.all) {
|
|
3432
|
+
const { readdirSync: readdirSync3, statSync: st } = await import("fs");
|
|
3433
|
+
const absDir = expandPath(dir);
|
|
3434
|
+
const entries = readdirSync3(absDir, { withFileTypes: true });
|
|
3435
|
+
let totalAdded = 0, totalUpdated = 0, totalUnchanged = 0, projects = 0;
|
|
3436
|
+
for (const entry of entries) {
|
|
3437
|
+
if (!entry.isDirectory())
|
|
3438
|
+
continue;
|
|
3439
|
+
const projDir = join6(absDir, entry.name);
|
|
3440
|
+
const hasClaude = existsSync7(join6(projDir, "CLAUDE.md")) || existsSync7(join6(projDir, ".mcp.json")) || existsSync7(join6(projDir, ".claude"));
|
|
3441
|
+
if (!hasClaude)
|
|
3442
|
+
continue;
|
|
3443
|
+
const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun });
|
|
3444
|
+
if (result2.added + result2.updated > 0) {
|
|
3445
|
+
console.log(` ${chalk.green("\u2713")} ${entry.name}: +${result2.added} updated:${result2.updated}`);
|
|
3446
|
+
}
|
|
3447
|
+
totalAdded += result2.added;
|
|
3448
|
+
totalUpdated += result2.updated;
|
|
3449
|
+
totalUnchanged += result2.unchanged;
|
|
3450
|
+
projects++;
|
|
3451
|
+
}
|
|
3452
|
+
console.log(chalk.green("\u2713") + ` Synced ${projects} projects: +${totalAdded} updated:${totalUpdated} unchanged:${totalUnchanged}`);
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3431
3455
|
const result = await syncProject({ projectDir: dir, dryRun: opts.dryRun });
|
|
3432
3456
|
console.log(chalk.green("\u2713") + ` Project sync: +${result.added} updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
|
|
3433
3457
|
return;
|
|
@@ -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":";;;;;AA0QA,wBAAgD"}
|
package/dist/server/index.js
CHANGED
|
@@ -2083,64 +2083,188 @@ async function applyConfigs(configs, opts = {}) {
|
|
|
2083
2083
|
return results;
|
|
2084
2084
|
}
|
|
2085
2085
|
|
|
2086
|
-
// src/lib/sync
|
|
2087
|
-
import { existsSync as
|
|
2088
|
-
import { join as join3
|
|
2086
|
+
// src/lib/sync.ts
|
|
2087
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
2088
|
+
import { extname, join as join3 } from "path";
|
|
2089
2089
|
import { homedir as homedir3 } from "os";
|
|
2090
2090
|
|
|
2091
|
-
// src/lib/
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2091
|
+
// src/lib/redact.ts
|
|
2092
|
+
var SECRET_KEY_PATTERN = /^(.*_?API_?KEY|.*_?TOKEN|.*_?SECRET|.*_?PASSWORD|.*_?PASSWD|.*_?CREDENTIAL|.*_?AUTH(?:_TOKEN|_KEY|ORIZATION)?|.*_?PRIVATE_?KEY|.*_?ACCESS_?KEY|.*_?CLIENT_?SECRET|.*_?SIGNING_?KEY|.*_?ENCRYPTION_?KEY|.*_AUTH_TOKEN)$/i;
|
|
2093
|
+
var VALUE_PATTERNS = [
|
|
2094
|
+
{ re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
|
|
2095
|
+
{ re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
|
|
2096
|
+
{ re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
|
|
2097
|
+
{ re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
|
|
2098
|
+
{ re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
|
|
2099
|
+
{ re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
|
|
2100
|
+
{ re: /ey[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\./, reason: "JWT token" },
|
|
2101
|
+
{ re: /AKIA[0-9A-Z]{16}/, reason: "AWS access key" }
|
|
2102
|
+
];
|
|
2103
|
+
var MIN_SECRET_VALUE_LEN = 8;
|
|
2104
|
+
function redactShell(content) {
|
|
2105
|
+
const redacted = [];
|
|
2106
|
+
const lines = content.split(`
|
|
2107
|
+
`);
|
|
2108
|
+
const out = [];
|
|
2109
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2110
|
+
const line = lines[i];
|
|
2111
|
+
const m = line.match(/^(\s*(?:export\s+)?)([A-Z][A-Z0-9_]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
|
|
2112
|
+
if (m) {
|
|
2113
|
+
const [, prefix, key, eq, quote, value] = m;
|
|
2114
|
+
if (shouldRedactKeyValue(key, value)) {
|
|
2115
|
+
const reason = reasonFor(key, value);
|
|
2116
|
+
redacted.push({ varName: key, line: i + 1, reason });
|
|
2117
|
+
out.push(`${prefix}${key}${eq}${quote}{{${key}}}${quote}`);
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
out.push(line);
|
|
2122
|
+
}
|
|
2123
|
+
return { content: out.join(`
|
|
2124
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
2111
2125
|
}
|
|
2112
|
-
function
|
|
2113
|
-
const
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2126
|
+
function redactJson(content) {
|
|
2127
|
+
const redacted = [];
|
|
2128
|
+
const lines = content.split(`
|
|
2129
|
+
`);
|
|
2130
|
+
const out = [];
|
|
2131
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2132
|
+
const line = lines[i];
|
|
2133
|
+
const m = line.match(/^(\s*"([^"]+)"\s*:\s*)"([^"]+)"(,?)(\s*)$/);
|
|
2134
|
+
if (m) {
|
|
2135
|
+
const [, prefix, key, value, comma, trail] = m;
|
|
2136
|
+
if (shouldRedactKeyValue(key, value)) {
|
|
2137
|
+
const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
2138
|
+
redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
|
|
2139
|
+
out.push(`${prefix}"{{${varName}}}"${comma}${trail}`);
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
let newLine = line;
|
|
2144
|
+
for (const { re, reason } of VALUE_PATTERNS) {
|
|
2145
|
+
newLine = newLine.replace(re, (match2) => {
|
|
2146
|
+
const varName = `REDACTED_${reason.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
2147
|
+
redacted.push({ varName, line: i + 1, reason });
|
|
2148
|
+
return `{{${varName}}}`;
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
out.push(newLine);
|
|
2152
|
+
}
|
|
2153
|
+
return { content: out.join(`
|
|
2154
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
2127
2155
|
}
|
|
2128
|
-
function
|
|
2129
|
-
const
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2156
|
+
function redactToml(content) {
|
|
2157
|
+
const redacted = [];
|
|
2158
|
+
const lines = content.split(`
|
|
2159
|
+
`);
|
|
2160
|
+
const out = [];
|
|
2161
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2162
|
+
const line = lines[i];
|
|
2163
|
+
const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
|
|
2164
|
+
if (m) {
|
|
2165
|
+
const [, indent, key, eq, quote, value] = m;
|
|
2166
|
+
if (shouldRedactKeyValue(key, value)) {
|
|
2167
|
+
const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
2168
|
+
redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
|
|
2169
|
+
out.push(`${indent}${key}${eq}${quote}{{${varName}}}${quote}`);
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
out.push(line);
|
|
2174
|
+
}
|
|
2175
|
+
return { content: out.join(`
|
|
2176
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
2177
|
+
}
|
|
2178
|
+
function redactIni(content) {
|
|
2179
|
+
const redacted = [];
|
|
2180
|
+
const lines = content.split(`
|
|
2181
|
+
`);
|
|
2182
|
+
const out = [];
|
|
2183
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2184
|
+
const line = lines[i];
|
|
2185
|
+
const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
|
|
2186
|
+
if (authM && !authM[2].startsWith("{{")) {
|
|
2187
|
+
redacted.push({ varName: "NPM_AUTH_TOKEN", line: i + 1, reason: "npm auth token" });
|
|
2188
|
+
out.push(`${authM[1]}{{NPM_AUTH_TOKEN}}`);
|
|
2189
|
+
continue;
|
|
2190
|
+
}
|
|
2191
|
+
const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
|
|
2192
|
+
if (m) {
|
|
2193
|
+
const [, indent, key, eq, value] = m;
|
|
2194
|
+
if (shouldRedactKeyValue(key, value)) {
|
|
2195
|
+
const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
2196
|
+
redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
|
|
2197
|
+
out.push(`${indent}${key}${eq}{{${varName}}}`);
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
out.push(line);
|
|
2202
|
+
}
|
|
2203
|
+
return { content: out.join(`
|
|
2204
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
2205
|
+
}
|
|
2206
|
+
function redactGeneric(content) {
|
|
2207
|
+
const redacted = [];
|
|
2208
|
+
const lines = content.split(`
|
|
2209
|
+
`);
|
|
2210
|
+
const out = [];
|
|
2211
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2212
|
+
let line = lines[i];
|
|
2213
|
+
for (const { re, reason } of VALUE_PATTERNS) {
|
|
2214
|
+
line = line.replace(re, (match2) => {
|
|
2215
|
+
const varName = reason.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
2216
|
+
redacted.push({ varName, line: i + 1, reason });
|
|
2217
|
+
return `{{${varName}}}`;
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
out.push(line);
|
|
2221
|
+
}
|
|
2222
|
+
return { content: out.join(`
|
|
2223
|
+
`), redacted, isTemplate: redacted.length > 0 };
|
|
2224
|
+
}
|
|
2225
|
+
function shouldRedactKeyValue(key, value) {
|
|
2226
|
+
if (!value || value.startsWith("{{"))
|
|
2227
|
+
return false;
|
|
2228
|
+
if (value.length < MIN_SECRET_VALUE_LEN)
|
|
2229
|
+
return false;
|
|
2230
|
+
if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
|
|
2231
|
+
return false;
|
|
2232
|
+
if (SECRET_KEY_PATTERN.test(key))
|
|
2233
|
+
return true;
|
|
2234
|
+
for (const { re } of VALUE_PATTERNS) {
|
|
2235
|
+
if (re.test(value))
|
|
2236
|
+
return true;
|
|
2237
|
+
}
|
|
2238
|
+
return false;
|
|
2239
|
+
}
|
|
2240
|
+
function reasonFor(key, value) {
|
|
2241
|
+
if (SECRET_KEY_PATTERN.test(key))
|
|
2242
|
+
return `secret key name: ${key}`;
|
|
2243
|
+
for (const { re, reason } of VALUE_PATTERNS) {
|
|
2244
|
+
if (re.test(value))
|
|
2245
|
+
return reason;
|
|
2246
|
+
}
|
|
2247
|
+
return "secret value pattern";
|
|
2248
|
+
}
|
|
2249
|
+
function redactContent(content, format) {
|
|
2250
|
+
switch (format) {
|
|
2251
|
+
case "shell":
|
|
2252
|
+
return redactShell(content);
|
|
2253
|
+
case "json":
|
|
2254
|
+
return redactJson(content);
|
|
2255
|
+
case "toml":
|
|
2256
|
+
return redactToml(content);
|
|
2257
|
+
case "ini":
|
|
2258
|
+
return redactIni(content);
|
|
2259
|
+
default:
|
|
2260
|
+
return redactGeneric(content);
|
|
2261
|
+
}
|
|
2141
2262
|
}
|
|
2142
2263
|
|
|
2143
2264
|
// src/lib/sync-dir.ts
|
|
2265
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
2266
|
+
import { join as join2, relative } from "path";
|
|
2267
|
+
import { homedir as homedir2 } from "os";
|
|
2144
2268
|
var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
|
|
2145
2269
|
function shouldSkip(p) {
|
|
2146
2270
|
return SKIP.some((s) => p.includes(s));
|
|
@@ -2150,9 +2274,9 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
2150
2274
|
const absDir = expandPath(dir);
|
|
2151
2275
|
if (!existsSync3(absDir))
|
|
2152
2276
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
2153
|
-
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) =>
|
|
2277
|
+
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join2(absDir, f)).filter((f) => statSync(f).isFile());
|
|
2154
2278
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
2155
|
-
const home =
|
|
2279
|
+
const home = homedir2();
|
|
2156
2280
|
const allConfigs = listConfigs(undefined, d);
|
|
2157
2281
|
for (const file of files) {
|
|
2158
2282
|
if (shouldSkip(file)) {
|
|
@@ -2186,7 +2310,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
2186
2310
|
}
|
|
2187
2311
|
async function syncToDir(dir, opts = {}) {
|
|
2188
2312
|
const d = opts.db || getDatabase();
|
|
2189
|
-
const home =
|
|
2313
|
+
const home = homedir2();
|
|
2190
2314
|
const absDir = expandPath(dir);
|
|
2191
2315
|
const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
|
|
2192
2316
|
const configs = listConfigs(undefined, d).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
|
|
@@ -2205,7 +2329,7 @@ async function syncToDir(dir, opts = {}) {
|
|
|
2205
2329
|
}
|
|
2206
2330
|
function walkDir(dir, files = []) {
|
|
2207
2331
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2208
|
-
const full =
|
|
2332
|
+
const full = join2(dir, entry.name);
|
|
2209
2333
|
if (shouldSkip(full))
|
|
2210
2334
|
continue;
|
|
2211
2335
|
if (entry.isDirectory())
|
|
@@ -2216,8 +2340,161 @@ function walkDir(dir, files = []) {
|
|
|
2216
2340
|
return files;
|
|
2217
2341
|
}
|
|
2218
2342
|
|
|
2343
|
+
// src/lib/sync.ts
|
|
2344
|
+
var KNOWN_CONFIGS = [
|
|
2345
|
+
{ path: "~/.claude/CLAUDE.md", name: "claude-claude-md", category: "rules", agent: "claude", format: "markdown" },
|
|
2346
|
+
{ path: "~/.claude/settings.json", name: "claude-settings", category: "agent", agent: "claude", format: "json" },
|
|
2347
|
+
{ path: "~/.claude/settings.local.json", name: "claude-settings-local", category: "agent", agent: "claude", format: "json" },
|
|
2348
|
+
{ path: "~/.claude/keybindings.json", name: "claude-keybindings", category: "agent", agent: "claude", format: "json" },
|
|
2349
|
+
{ path: "~/.claude/rules", name: "claude-rules", category: "rules", agent: "claude", rulesDir: "~/.claude/rules" },
|
|
2350
|
+
{ path: "~/.codex/config.toml", name: "codex-config", category: "agent", agent: "codex", format: "toml" },
|
|
2351
|
+
{ path: "~/.codex/AGENTS.md", name: "codex-agents-md", category: "rules", agent: "codex", format: "markdown" },
|
|
2352
|
+
{ path: "~/.gemini/settings.json", name: "gemini-settings", category: "agent", agent: "gemini", format: "json" },
|
|
2353
|
+
{ path: "~/.gemini/GEMINI.md", name: "gemini-gemini-md", category: "rules", agent: "gemini", format: "markdown" },
|
|
2354
|
+
{ path: "~/.claude.json", name: "claude-json", category: "mcp", agent: "claude", format: "json", description: "Claude Code global config (includes MCP server entries)" },
|
|
2355
|
+
{ path: "~/.zshrc", name: "zshrc", category: "shell", agent: "zsh" },
|
|
2356
|
+
{ path: "~/.zprofile", name: "zprofile", category: "shell", agent: "zsh" },
|
|
2357
|
+
{ path: "~/.bashrc", name: "bashrc", category: "shell", agent: "zsh" },
|
|
2358
|
+
{ path: "~/.bash_profile", name: "bash-profile", category: "shell", agent: "zsh" },
|
|
2359
|
+
{ path: "~/.gitconfig", name: "gitconfig", category: "git", agent: "git", format: "ini" },
|
|
2360
|
+
{ path: "~/.gitignore_global", name: "gitignore-global", category: "git", agent: "git" },
|
|
2361
|
+
{ path: "~/.npmrc", name: "npmrc", category: "tools", agent: "npm", format: "ini" },
|
|
2362
|
+
{ path: "~/.bunfig.toml", name: "bunfig", category: "tools", agent: "global", format: "toml" }
|
|
2363
|
+
];
|
|
2364
|
+
async function syncKnown(opts = {}) {
|
|
2365
|
+
const d = opts.db || getDatabase();
|
|
2366
|
+
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
2367
|
+
const home = homedir3();
|
|
2368
|
+
let targets = KNOWN_CONFIGS;
|
|
2369
|
+
if (opts.agent)
|
|
2370
|
+
targets = targets.filter((k) => k.agent === opts.agent);
|
|
2371
|
+
if (opts.category)
|
|
2372
|
+
targets = targets.filter((k) => k.category === opts.category);
|
|
2373
|
+
const allConfigs = listConfigs(undefined, d);
|
|
2374
|
+
for (const known of targets) {
|
|
2375
|
+
if (known.rulesDir) {
|
|
2376
|
+
const absDir = expandPath(known.rulesDir);
|
|
2377
|
+
if (!existsSync4(absDir)) {
|
|
2378
|
+
result.skipped.push(known.rulesDir);
|
|
2379
|
+
continue;
|
|
2380
|
+
}
|
|
2381
|
+
const mdFiles = readdirSync2(absDir).filter((f) => f.endsWith(".md"));
|
|
2382
|
+
for (const f of mdFiles) {
|
|
2383
|
+
const abs2 = join3(absDir, f);
|
|
2384
|
+
const targetPath = abs2.replace(home, "~");
|
|
2385
|
+
const raw2 = readFileSync3(abs2, "utf-8");
|
|
2386
|
+
const { content, isTemplate } = redactContent(raw2, "markdown");
|
|
2387
|
+
const name = `claude-rules-${f}`;
|
|
2388
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
2389
|
+
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
|
|
2390
|
+
if (!existing) {
|
|
2391
|
+
if (!opts.dryRun)
|
|
2392
|
+
createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate }, d);
|
|
2393
|
+
result.added++;
|
|
2394
|
+
} else if (existing.content !== content) {
|
|
2395
|
+
if (!opts.dryRun)
|
|
2396
|
+
updateConfig(existing.id, { content, is_template: isTemplate }, d);
|
|
2397
|
+
result.updated++;
|
|
2398
|
+
} else {
|
|
2399
|
+
result.unchanged++;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
continue;
|
|
2403
|
+
}
|
|
2404
|
+
const abs = expandPath(known.path);
|
|
2405
|
+
if (!existsSync4(abs)) {
|
|
2406
|
+
result.skipped.push(known.path);
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
try {
|
|
2410
|
+
const rawContent = readFileSync3(abs, "utf-8");
|
|
2411
|
+
if (rawContent.length > 500000) {
|
|
2412
|
+
result.skipped.push(known.path + " (too large)");
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
2415
|
+
const fmt = known.format ?? detectFormat(abs);
|
|
2416
|
+
const { content, isTemplate } = redactContent(rawContent, fmt);
|
|
2417
|
+
const targetPath = abs.replace(home, "~");
|
|
2418
|
+
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
|
|
2419
|
+
if (!existing) {
|
|
2420
|
+
if (!opts.dryRun) {
|
|
2421
|
+
createConfig({
|
|
2422
|
+
name: known.name,
|
|
2423
|
+
category: known.category,
|
|
2424
|
+
agent: known.agent,
|
|
2425
|
+
format: fmt,
|
|
2426
|
+
content,
|
|
2427
|
+
target_path: known.kind === "reference" ? null : targetPath,
|
|
2428
|
+
kind: known.kind ?? "file",
|
|
2429
|
+
description: known.description,
|
|
2430
|
+
is_template: isTemplate
|
|
2431
|
+
}, d);
|
|
2432
|
+
}
|
|
2433
|
+
result.added++;
|
|
2434
|
+
} else if (existing.content !== content) {
|
|
2435
|
+
if (!opts.dryRun)
|
|
2436
|
+
updateConfig(existing.id, { content, is_template: isTemplate }, d);
|
|
2437
|
+
result.updated++;
|
|
2438
|
+
} else {
|
|
2439
|
+
result.unchanged++;
|
|
2440
|
+
}
|
|
2441
|
+
} catch {
|
|
2442
|
+
result.skipped.push(known.path);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
return result;
|
|
2446
|
+
}
|
|
2447
|
+
function detectCategory(filePath) {
|
|
2448
|
+
const p = filePath.toLowerCase().replace(homedir3(), "~");
|
|
2449
|
+
if (p.includes("/.claude/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("gemini.md"))
|
|
2450
|
+
return "rules";
|
|
2451
|
+
if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.gemini/") || p.includes("/.cursor/"))
|
|
2452
|
+
return "agent";
|
|
2453
|
+
if (p.includes(".mcp.json") || p.includes("mcp"))
|
|
2454
|
+
return "mcp";
|
|
2455
|
+
if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc") || p.includes(".bash_profile"))
|
|
2456
|
+
return "shell";
|
|
2457
|
+
if (p.includes(".gitconfig") || p.includes(".gitignore"))
|
|
2458
|
+
return "git";
|
|
2459
|
+
if (p.includes(".npmrc") || p.includes("tsconfig") || p.includes("bunfig"))
|
|
2460
|
+
return "tools";
|
|
2461
|
+
if (p.includes(".secrets"))
|
|
2462
|
+
return "secrets_schema";
|
|
2463
|
+
return "tools";
|
|
2464
|
+
}
|
|
2465
|
+
function detectAgent(filePath) {
|
|
2466
|
+
const p = filePath.toLowerCase().replace(homedir3(), "~");
|
|
2467
|
+
if (p.includes("/.claude/") || p.endsWith("claude.md"))
|
|
2468
|
+
return "claude";
|
|
2469
|
+
if (p.includes("/.codex/") || p.endsWith("agents.md"))
|
|
2470
|
+
return "codex";
|
|
2471
|
+
if (p.includes("/.gemini/") || p.endsWith("gemini.md"))
|
|
2472
|
+
return "gemini";
|
|
2473
|
+
if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc"))
|
|
2474
|
+
return "zsh";
|
|
2475
|
+
if (p.includes(".gitconfig") || p.includes(".gitignore"))
|
|
2476
|
+
return "git";
|
|
2477
|
+
if (p.includes(".npmrc"))
|
|
2478
|
+
return "npm";
|
|
2479
|
+
return "global";
|
|
2480
|
+
}
|
|
2481
|
+
function detectFormat(filePath) {
|
|
2482
|
+
const ext = extname(filePath).toLowerCase();
|
|
2483
|
+
if (ext === ".json")
|
|
2484
|
+
return "json";
|
|
2485
|
+
if (ext === ".toml")
|
|
2486
|
+
return "toml";
|
|
2487
|
+
if (ext === ".yaml" || ext === ".yml")
|
|
2488
|
+
return "yaml";
|
|
2489
|
+
if (ext === ".md" || ext === ".markdown")
|
|
2490
|
+
return "markdown";
|
|
2491
|
+
if (ext === ".ini" || ext === ".cfg")
|
|
2492
|
+
return "ini";
|
|
2493
|
+
return "text";
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2219
2496
|
// src/server/index.ts
|
|
2220
|
-
import { existsSync as
|
|
2497
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
2221
2498
|
import { join as join4, extname as extname2 } from "path";
|
|
2222
2499
|
var PORT = Number(process.env["CONFIGS_PORT"] ?? 3457);
|
|
2223
2500
|
function pickFields(obj, fields) {
|
|
@@ -2229,6 +2506,30 @@ function pickFields(obj, fields) {
|
|
|
2229
2506
|
var app = new Hono2;
|
|
2230
2507
|
app.use("*", cors());
|
|
2231
2508
|
app.get("/api/stats", (c) => c.json(getConfigStats()));
|
|
2509
|
+
app.get("/api/status", (c) => {
|
|
2510
|
+
const stats = getConfigStats();
|
|
2511
|
+
const allConfigs = listConfigs({ kind: "file" });
|
|
2512
|
+
let templates = 0;
|
|
2513
|
+
for (const cfg of allConfigs) {
|
|
2514
|
+
if (cfg.is_template)
|
|
2515
|
+
templates++;
|
|
2516
|
+
}
|
|
2517
|
+
return c.json({
|
|
2518
|
+
total: stats["total"] || 0,
|
|
2519
|
+
by_category: Object.fromEntries(Object.entries(stats).filter(([k]) => k !== "total")),
|
|
2520
|
+
templates,
|
|
2521
|
+
db_path: process.env["CONFIGS_DB_PATH"] || "~/.configs/configs.db"
|
|
2522
|
+
});
|
|
2523
|
+
});
|
|
2524
|
+
app.post("/api/sync-known", async (c) => {
|
|
2525
|
+
try {
|
|
2526
|
+
const body = await c.req.json().catch(() => ({}));
|
|
2527
|
+
const result = await syncKnown({ agent: body.agent, category: body.category, dryRun: body.dry_run });
|
|
2528
|
+
return c.json(result);
|
|
2529
|
+
} catch (e) {
|
|
2530
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 422);
|
|
2531
|
+
}
|
|
2532
|
+
});
|
|
2232
2533
|
app.get("/api/configs", (c) => {
|
|
2233
2534
|
const { category, agent, kind, search, fields } = c.req.query();
|
|
2234
2535
|
const configs = listConfigs({
|
|
@@ -2388,7 +2689,7 @@ function findDashboardDir() {
|
|
|
2388
2689
|
join4(import.meta.dir, "../../../dashboard/dist")
|
|
2389
2690
|
];
|
|
2390
2691
|
for (const dir of candidates) {
|
|
2391
|
-
if (
|
|
2692
|
+
if (existsSync5(join4(dir, "index.html")))
|
|
2392
2693
|
return dir;
|
|
2393
2694
|
}
|
|
2394
2695
|
return null;
|
|
@@ -2399,11 +2700,11 @@ if (dashDir) {
|
|
|
2399
2700
|
const url = new URL(c.req.url);
|
|
2400
2701
|
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
2401
2702
|
let absPath = join4(dashDir, filePath);
|
|
2402
|
-
if (!
|
|
2703
|
+
if (!existsSync5(absPath))
|
|
2403
2704
|
absPath = join4(dashDir, "index.html");
|
|
2404
|
-
if (!
|
|
2705
|
+
if (!existsSync5(absPath))
|
|
2405
2706
|
return c.json({ error: "Not found" }, 404);
|
|
2406
|
-
const content =
|
|
2707
|
+
const content = readFileSync4(absPath);
|
|
2407
2708
|
const ext = extname2(absPath);
|
|
2408
2709
|
return new Response(content, {
|
|
2409
2710
|
headers: { "Content-Type": MIME[ext] || "application/octet-stream" }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/configs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
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",
|