@hasna/configs 0.2.33 → 0.2.35
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 +781 -98
- package/dist/index.js +60 -62
- package/dist/mcp/index.js +58 -66
- package/dist/server/index.js +225 -132
- package/package.json +3 -2
package/dist/cli/index.js
CHANGED
|
@@ -993,7 +993,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
993
993
|
this._exitCallback = (err) => {
|
|
994
994
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
995
995
|
throw err;
|
|
996
|
-
}
|
|
996
|
+
}
|
|
997
997
|
};
|
|
998
998
|
}
|
|
999
999
|
return this;
|
|
@@ -2114,23 +2114,23 @@ var init_types = __esm(() => {
|
|
|
2114
2114
|
import { createRequire } from "module";
|
|
2115
2115
|
import { Database } from "bun:sqlite";
|
|
2116
2116
|
import {
|
|
2117
|
-
existsSync,
|
|
2117
|
+
existsSync as existsSync2,
|
|
2118
2118
|
mkdirSync,
|
|
2119
2119
|
readdirSync,
|
|
2120
2120
|
copyFileSync
|
|
2121
2121
|
} from "fs";
|
|
2122
|
-
import { homedir } from "os";
|
|
2123
|
-
import { join, relative } from "path";
|
|
2124
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
2125
2122
|
import { homedir as homedir2 } from "os";
|
|
2126
|
-
import { join as join2 } from "path";
|
|
2127
|
-
import {
|
|
2128
|
-
import {
|
|
2123
|
+
import { join as join2, relative } from "path";
|
|
2124
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
2125
|
+
import { homedir as homedir22 } from "os";
|
|
2126
|
+
import { join as join22 } from "path";
|
|
2127
|
+
import { readdirSync as readdirSync3, existsSync as existsSync6 } from "fs";
|
|
2128
|
+
import { join as join6 } from "path";
|
|
2129
|
+
import { homedir as homedir5 } from "os";
|
|
2129
2130
|
import { homedir as homedir3 } from "os";
|
|
2130
|
-
import {
|
|
2131
|
-
import { join as
|
|
2132
|
-
import {
|
|
2133
|
-
import { homedir as homedir5, platform } from "os";
|
|
2131
|
+
import { join as join3 } from "path";
|
|
2132
|
+
import { join as join5, dirname } from "path";
|
|
2133
|
+
import { homedir as homedir4, platform } from "os";
|
|
2134
2134
|
function __accessProp2(key) {
|
|
2135
2135
|
return this[key];
|
|
2136
2136
|
}
|
|
@@ -2944,20 +2944,20 @@ function custom(check, _params = {}, fatal) {
|
|
|
2944
2944
|
return ZodAny.create();
|
|
2945
2945
|
}
|
|
2946
2946
|
function getDataDir(serviceName) {
|
|
2947
|
-
const dir =
|
|
2947
|
+
const dir = join2(HASNA_DIR, serviceName);
|
|
2948
2948
|
mkdirSync(dir, { recursive: true });
|
|
2949
2949
|
return dir;
|
|
2950
2950
|
}
|
|
2951
2951
|
function getDbPath(serviceName) {
|
|
2952
2952
|
const dir = getDataDir(serviceName);
|
|
2953
|
-
return
|
|
2953
|
+
return join2(dir, `${serviceName}.db`);
|
|
2954
2954
|
}
|
|
2955
2955
|
function migrateDotfile(serviceName) {
|
|
2956
|
-
const legacyDir =
|
|
2957
|
-
const newDir =
|
|
2958
|
-
if (!
|
|
2956
|
+
const legacyDir = join2(homedir2(), `.${serviceName}`);
|
|
2957
|
+
const newDir = join2(HASNA_DIR, serviceName);
|
|
2958
|
+
if (!existsSync2(legacyDir))
|
|
2959
2959
|
return [];
|
|
2960
|
-
if (
|
|
2960
|
+
if (existsSync2(newDir))
|
|
2961
2961
|
return [];
|
|
2962
2962
|
mkdirSync(newDir, { recursive: true });
|
|
2963
2963
|
const migrated = [];
|
|
@@ -2967,8 +2967,8 @@ function migrateDotfile(serviceName) {
|
|
|
2967
2967
|
function copyDirRecursive(src, dest, root, migrated) {
|
|
2968
2968
|
const entries = readdirSync(src, { withFileTypes: true });
|
|
2969
2969
|
for (const entry of entries) {
|
|
2970
|
-
const srcPath =
|
|
2971
|
-
const destPath =
|
|
2970
|
+
const srcPath = join2(src, entry.name);
|
|
2971
|
+
const destPath = join2(dest, entry.name);
|
|
2972
2972
|
if (entry.isDirectory()) {
|
|
2973
2973
|
mkdirSync(destPath, { recursive: true });
|
|
2974
2974
|
copyDirRecursive(srcPath, destPath, root, migrated);
|
|
@@ -2985,7 +2985,7 @@ function getConfigPath() {
|
|
|
2985
2985
|
return CONFIG_PATH;
|
|
2986
2986
|
}
|
|
2987
2987
|
function getCloudConfig() {
|
|
2988
|
-
if (!
|
|
2988
|
+
if (!existsSync22(CONFIG_PATH)) {
|
|
2989
2989
|
return CloudConfigSchema.parse({});
|
|
2990
2990
|
}
|
|
2991
2991
|
try {
|
|
@@ -3027,11 +3027,11 @@ function isSyncExcludedTable(table) {
|
|
|
3027
3027
|
return SYNC_EXCLUDED_TABLE_PATTERNS.some((p) => p.test(table));
|
|
3028
3028
|
}
|
|
3029
3029
|
function discoverServices() {
|
|
3030
|
-
const dataDir =
|
|
3031
|
-
if (!
|
|
3030
|
+
const dataDir = join6(homedir5(), ".hasna");
|
|
3031
|
+
if (!existsSync6(dataDir))
|
|
3032
3032
|
return [];
|
|
3033
3033
|
try {
|
|
3034
|
-
const entries =
|
|
3034
|
+
const entries = readdirSync3(dataDir, { withFileTypes: true });
|
|
3035
3035
|
return entries.filter((e) => {
|
|
3036
3036
|
if (!e.isDirectory())
|
|
3037
3037
|
return false;
|
|
@@ -3043,30 +3043,30 @@ function discoverServices() {
|
|
|
3043
3043
|
return [];
|
|
3044
3044
|
}
|
|
3045
3045
|
}
|
|
3046
|
-
function
|
|
3046
|
+
function discoverSyncableServices2() {
|
|
3047
3047
|
const local = discoverServices();
|
|
3048
3048
|
const pgSet = new Set(KNOWN_PG_SERVICES);
|
|
3049
3049
|
return local.filter((s) => pgSet.has(s));
|
|
3050
3050
|
}
|
|
3051
3051
|
function getServiceDbPath(service) {
|
|
3052
|
-
const dataDir =
|
|
3053
|
-
if (!
|
|
3052
|
+
const dataDir = join6(homedir5(), ".hasna", service);
|
|
3053
|
+
if (!existsSync6(dataDir))
|
|
3054
3054
|
return null;
|
|
3055
3055
|
const candidates = [
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3056
|
+
join6(dataDir, `${service}.db`),
|
|
3057
|
+
join6(dataDir, "data.db"),
|
|
3058
|
+
join6(dataDir, "database.db")
|
|
3059
3059
|
];
|
|
3060
3060
|
try {
|
|
3061
|
-
const files =
|
|
3061
|
+
const files = readdirSync3(dataDir);
|
|
3062
3062
|
for (const f of files) {
|
|
3063
3063
|
if (f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm")) {
|
|
3064
|
-
candidates.push(
|
|
3064
|
+
candidates.push(join6(dataDir, f));
|
|
3065
3065
|
}
|
|
3066
3066
|
}
|
|
3067
3067
|
} catch {}
|
|
3068
3068
|
for (const p of candidates) {
|
|
3069
|
-
if (
|
|
3069
|
+
if (existsSync6(p))
|
|
3070
3070
|
return p;
|
|
3071
3071
|
}
|
|
3072
3072
|
return null;
|
|
@@ -3098,8 +3098,8 @@ class SyncProgressTracker {
|
|
|
3098
3098
|
}
|
|
3099
3099
|
start(table, total, direction) {
|
|
3100
3100
|
const resumed = this.canResume(table);
|
|
3101
|
-
const
|
|
3102
|
-
this.startTimes.set(table,
|
|
3101
|
+
const now2 = Date.now();
|
|
3102
|
+
this.startTimes.set(table, now2);
|
|
3103
3103
|
const status = resumed ? "resumed" : "in_progress";
|
|
3104
3104
|
const info = {
|
|
3105
3105
|
table,
|
|
@@ -11417,7 +11417,7 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
11417
11417
|
init_external();
|
|
11418
11418
|
});
|
|
11419
11419
|
init_dotfile = __esm2(() => {
|
|
11420
|
-
HASNA_DIR =
|
|
11420
|
+
HASNA_DIR = join2(homedir2(), ".hasna");
|
|
11421
11421
|
});
|
|
11422
11422
|
exports_config = {};
|
|
11423
11423
|
__export2(exports_config, {
|
|
@@ -11448,14 +11448,14 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
11448
11448
|
schedule_minutes: exports_external.number().default(0)
|
|
11449
11449
|
}).default({})
|
|
11450
11450
|
});
|
|
11451
|
-
CONFIG_DIR =
|
|
11452
|
-
CONFIG_PATH =
|
|
11451
|
+
CONFIG_DIR = join22(homedir22(), ".hasna", "cloud");
|
|
11452
|
+
CONFIG_PATH = join22(CONFIG_DIR, "config.json");
|
|
11453
11453
|
});
|
|
11454
11454
|
exports_discover = {};
|
|
11455
11455
|
__export2(exports_discover, {
|
|
11456
11456
|
isSyncExcludedTable: () => isSyncExcludedTable,
|
|
11457
11457
|
getServiceDbPath: () => getServiceDbPath,
|
|
11458
|
-
discoverSyncableServices: () =>
|
|
11458
|
+
discoverSyncableServices: () => discoverSyncableServices2,
|
|
11459
11459
|
discoverServices: () => discoverServices,
|
|
11460
11460
|
SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
|
|
11461
11461
|
KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES
|
|
@@ -11510,15 +11510,13 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
11510
11510
|
init_config();
|
|
11511
11511
|
init_config();
|
|
11512
11512
|
init_dotfile();
|
|
11513
|
-
init_adapter();
|
|
11514
11513
|
init_config();
|
|
11515
|
-
|
|
11516
|
-
AUTO_SYNC_CONFIG_PATH = join4(homedir4(), ".hasna", "cloud", "config.json");
|
|
11514
|
+
AUTO_SYNC_CONFIG_PATH = join3(homedir3(), ".hasna", "cloud", "config.json");
|
|
11517
11515
|
init_config();
|
|
11518
11516
|
init_adapter();
|
|
11519
11517
|
init_dotfile();
|
|
11520
11518
|
init_config();
|
|
11521
|
-
CONFIG_DIR2 =
|
|
11519
|
+
CONFIG_DIR2 = join5(homedir4(), ".hasna", "cloud");
|
|
11522
11520
|
init_adapter();
|
|
11523
11521
|
init_config();
|
|
11524
11522
|
init_discover();
|
|
@@ -11533,8 +11531,8 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
11533
11531
|
|
|
11534
11532
|
// src/db/database.ts
|
|
11535
11533
|
import { mkdirSync as mkdirSync3 } from "fs";
|
|
11536
|
-
import { join as
|
|
11537
|
-
import { randomUUID } from "crypto";
|
|
11534
|
+
import { join as join4 } from "path";
|
|
11535
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
11538
11536
|
function getDbPath2() {
|
|
11539
11537
|
if (process.env["HASNA_CONFIGS_DB_PATH"]) {
|
|
11540
11538
|
return process.env["HASNA_CONFIGS_DB_PATH"];
|
|
@@ -11544,14 +11542,14 @@ function getDbPath2() {
|
|
|
11544
11542
|
}
|
|
11545
11543
|
migrateDotfile("configs");
|
|
11546
11544
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
11547
|
-
const dir =
|
|
11545
|
+
const dir = join4(home, ".hasna", "configs");
|
|
11548
11546
|
mkdirSync3(dir, { recursive: true });
|
|
11549
|
-
return
|
|
11547
|
+
return join4(dir, "configs.db");
|
|
11550
11548
|
}
|
|
11551
11549
|
function uuid() {
|
|
11552
|
-
return
|
|
11550
|
+
return randomUUID3();
|
|
11553
11551
|
}
|
|
11554
|
-
function
|
|
11552
|
+
function now2() {
|
|
11555
11553
|
return new Date().toISOString();
|
|
11556
11554
|
}
|
|
11557
11555
|
function slugify(name) {
|
|
@@ -11681,7 +11679,7 @@ function uniqueSlug(name, db, excludeId) {
|
|
|
11681
11679
|
function createConfig(input, db) {
|
|
11682
11680
|
const d = db || getDatabase();
|
|
11683
11681
|
const id = uuid();
|
|
11684
|
-
const ts =
|
|
11682
|
+
const ts = now2();
|
|
11685
11683
|
const slug = uniqueSlug(input.name, d);
|
|
11686
11684
|
const tags = JSON.stringify(input.tags || []);
|
|
11687
11685
|
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)
|
|
@@ -11755,7 +11753,7 @@ function listConfigs(filter, db) {
|
|
|
11755
11753
|
function updateConfig(idOrSlug, input, db) {
|
|
11756
11754
|
const d = db || getDatabase();
|
|
11757
11755
|
const existing = getConfig(idOrSlug, d);
|
|
11758
|
-
const ts =
|
|
11756
|
+
const ts = now2();
|
|
11759
11757
|
const updates = ["updated_at = ?", "version = version + 1"];
|
|
11760
11758
|
const params = [ts];
|
|
11761
11759
|
if (input.name !== undefined) {
|
|
@@ -11883,7 +11881,7 @@ var init_template = __esm(() => {
|
|
|
11883
11881
|
|
|
11884
11882
|
// src/lib/machine.ts
|
|
11885
11883
|
import { arch as currentArch, homedir as homedir6, hostname as currentHostname, type as currentOsType } from "os";
|
|
11886
|
-
import { existsSync as
|
|
11884
|
+
import { existsSync as existsSync4 } from "fs";
|
|
11887
11885
|
import { join as join7 } from "path";
|
|
11888
11886
|
function normalizeOsFamily(os) {
|
|
11889
11887
|
const value = (os ?? "").trim().toLowerCase();
|
|
@@ -11900,7 +11898,7 @@ function detectMachineContext(overrides = {}) {
|
|
|
11900
11898
|
const os = overrides.os ?? currentOsType();
|
|
11901
11899
|
const osFamily = normalizeOsFamily(os);
|
|
11902
11900
|
const bunBinDir = overrides.bun_bin_dir ?? join7(homeDir, ".bun", "bin");
|
|
11903
|
-
const defaultBunPath = osFamily === "macos" &&
|
|
11901
|
+
const defaultBunPath = osFamily === "macos" && existsSync4(BREW_BUN_PATH) ? BREW_BUN_PATH : join7(bunBinDir, "bun");
|
|
11904
11902
|
return {
|
|
11905
11903
|
id: "current-machine",
|
|
11906
11904
|
hostname: overrides.hostname ?? currentHostname(),
|
|
@@ -11985,7 +11983,7 @@ var init_machine = __esm(() => {
|
|
|
11985
11983
|
function createSnapshot(configId, content, version, db) {
|
|
11986
11984
|
const d = db || getDatabase();
|
|
11987
11985
|
const id = uuid();
|
|
11988
|
-
const ts =
|
|
11986
|
+
const ts = now2();
|
|
11989
11987
|
d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
|
|
11990
11988
|
return { id, config_id: configId, content, version, created_at: ts };
|
|
11991
11989
|
}
|
|
@@ -12008,7 +12006,7 @@ __export(exports_apply, {
|
|
|
12008
12006
|
applyConfigs: () => applyConfigs,
|
|
12009
12007
|
applyConfig: () => applyConfig
|
|
12010
12008
|
});
|
|
12011
|
-
import { existsSync as
|
|
12009
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
12012
12010
|
import { dirname as dirname2, resolve } from "path";
|
|
12013
12011
|
import { homedir as homedir8 } from "os";
|
|
12014
12012
|
function expandPath(p) {
|
|
@@ -12024,11 +12022,11 @@ async function applyConfig(config, opts = {}) {
|
|
|
12024
12022
|
const renderedTargetPath = opts.vars ? renderMachineAwareContent(config.target_path, opts.vars) : config.target_path;
|
|
12025
12023
|
const renderedContent = opts.vars ? renderMachineAwareContent(config.content, opts.vars) : config.content;
|
|
12026
12024
|
const path = expandPath(renderedTargetPath);
|
|
12027
|
-
const previousContent =
|
|
12025
|
+
const previousContent = existsSync5(path) ? readFileSync2(path, "utf-8") : null;
|
|
12028
12026
|
const changed = previousContent !== renderedContent;
|
|
12029
12027
|
if (!opts.dryRun) {
|
|
12030
12028
|
const dir = dirname2(path);
|
|
12031
|
-
if (!
|
|
12029
|
+
if (!existsSync5(dir)) {
|
|
12032
12030
|
mkdirSync5(dir, { recursive: true });
|
|
12033
12031
|
}
|
|
12034
12032
|
if (previousContent !== null && changed) {
|
|
@@ -12037,7 +12035,7 @@ async function applyConfig(config, opts = {}) {
|
|
|
12037
12035
|
}
|
|
12038
12036
|
writeFileSync2(path, renderedContent, "utf-8");
|
|
12039
12037
|
const db = opts.db || getDatabase();
|
|
12040
|
-
updateConfig(config.id, { synced_at:
|
|
12038
|
+
updateConfig(config.id, { synced_at: now2() }, db);
|
|
12041
12039
|
}
|
|
12042
12040
|
return {
|
|
12043
12041
|
config_id: config.id,
|
|
@@ -12245,8 +12243,8 @@ var init_redact = __esm(() => {
|
|
|
12245
12243
|
});
|
|
12246
12244
|
|
|
12247
12245
|
// src/lib/sync-dir.ts
|
|
12248
|
-
import { existsSync as
|
|
12249
|
-
import { join as
|
|
12246
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync } from "fs";
|
|
12247
|
+
import { join as join9, relative as relative2 } from "path";
|
|
12250
12248
|
import { homedir as homedir9 } from "os";
|
|
12251
12249
|
function shouldSkip(p) {
|
|
12252
12250
|
return SKIP.some((s) => p.includes(s));
|
|
@@ -12254,9 +12252,9 @@ function shouldSkip(p) {
|
|
|
12254
12252
|
async function syncFromDir(dir, opts = {}) {
|
|
12255
12253
|
const d = opts.db || getDatabase();
|
|
12256
12254
|
const absDir = expandPath(dir);
|
|
12257
|
-
if (!
|
|
12255
|
+
if (!existsSync7(absDir))
|
|
12258
12256
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
12259
|
-
const files = opts.recursive !== false ? walkDir(absDir) :
|
|
12257
|
+
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync2(absDir).map((f) => join9(absDir, f)).filter((f) => statSync(f).isFile());
|
|
12260
12258
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
12261
12259
|
const home = homedir9();
|
|
12262
12260
|
const allConfigs = listConfigs(undefined, d);
|
|
@@ -12310,8 +12308,8 @@ async function syncToDir(dir, opts = {}) {
|
|
|
12310
12308
|
return result;
|
|
12311
12309
|
}
|
|
12312
12310
|
function walkDir(dir, files = []) {
|
|
12313
|
-
for (const entry of
|
|
12314
|
-
const full =
|
|
12311
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
12312
|
+
const full = join9(dir, entry.name);
|
|
12315
12313
|
if (shouldSkip(full))
|
|
12316
12314
|
continue;
|
|
12317
12315
|
if (entry.isDirectory())
|
|
@@ -12345,8 +12343,8 @@ __export(exports_sync, {
|
|
|
12345
12343
|
PROJECT_CONFIG_FILES: () => PROJECT_CONFIG_FILES,
|
|
12346
12344
|
KNOWN_CONFIGS: () => KNOWN_CONFIGS
|
|
12347
12345
|
});
|
|
12348
|
-
import { existsSync as existsSync9, readdirSync as
|
|
12349
|
-
import { extname, join as
|
|
12346
|
+
import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync4 } from "fs";
|
|
12347
|
+
import { extname, join as join10 } from "path";
|
|
12350
12348
|
import { homedir as homedir10 } from "os";
|
|
12351
12349
|
async function syncProject(opts) {
|
|
12352
12350
|
const d = opts.db || getDatabase();
|
|
@@ -12356,7 +12354,7 @@ async function syncProject(opts) {
|
|
|
12356
12354
|
const allConfigs = listConfigs(undefined, d);
|
|
12357
12355
|
const machine = detectMachineContext();
|
|
12358
12356
|
for (const pf of PROJECT_CONFIG_FILES) {
|
|
12359
|
-
const abs =
|
|
12357
|
+
const abs = join10(absDir, pf.file);
|
|
12360
12358
|
if (!existsSync9(abs))
|
|
12361
12359
|
continue;
|
|
12362
12360
|
try {
|
|
@@ -12388,11 +12386,11 @@ async function syncProject(opts) {
|
|
|
12388
12386
|
result.skipped.push(pf.file);
|
|
12389
12387
|
}
|
|
12390
12388
|
}
|
|
12391
|
-
const rulesDir =
|
|
12389
|
+
const rulesDir = join10(absDir, ".claude", "rules");
|
|
12392
12390
|
if (existsSync9(rulesDir)) {
|
|
12393
|
-
const mdFiles =
|
|
12391
|
+
const mdFiles = readdirSync4(rulesDir).filter((f) => f.endsWith(".md"));
|
|
12394
12392
|
for (const f of mdFiles) {
|
|
12395
|
-
const abs =
|
|
12393
|
+
const abs = join10(rulesDir, f);
|
|
12396
12394
|
const raw = readFileSync4(abs, "utf-8");
|
|
12397
12395
|
const redacted = redactContent(raw, "markdown");
|
|
12398
12396
|
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
@@ -12435,9 +12433,9 @@ async function syncKnown(opts = {}) {
|
|
|
12435
12433
|
result.skipped.push(known.rulesDir);
|
|
12436
12434
|
continue;
|
|
12437
12435
|
}
|
|
12438
|
-
const mdFiles =
|
|
12436
|
+
const mdFiles = readdirSync4(absDir).filter((f) => f.endsWith(".md"));
|
|
12439
12437
|
for (const f of mdFiles) {
|
|
12440
|
-
const abs2 =
|
|
12438
|
+
const abs2 = join10(absDir, f);
|
|
12441
12439
|
const targetPath = abs2.replace(home, "~");
|
|
12442
12440
|
const raw = readFileSync4(abs2, "utf-8");
|
|
12443
12441
|
const redacted = redactContent(raw, "markdown");
|
|
@@ -12641,6 +12639,690 @@ var init_sync = __esm(() => {
|
|
|
12641
12639
|
];
|
|
12642
12640
|
});
|
|
12643
12641
|
|
|
12642
|
+
// node_modules/@hasna/events/dist/commander.js
|
|
12643
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
12644
|
+
import { existsSync } from "fs";
|
|
12645
|
+
import { homedir } from "os";
|
|
12646
|
+
import { join } from "path";
|
|
12647
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
12648
|
+
import { randomUUID } from "crypto";
|
|
12649
|
+
import { spawn } from "child_process";
|
|
12650
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
12651
|
+
function getPathValue(input, path) {
|
|
12652
|
+
return path.split(".").reduce((value, part) => {
|
|
12653
|
+
if (value && typeof value === "object" && part in value) {
|
|
12654
|
+
return value[part];
|
|
12655
|
+
}
|
|
12656
|
+
return;
|
|
12657
|
+
}, input);
|
|
12658
|
+
}
|
|
12659
|
+
function wildcardToRegExp(pattern) {
|
|
12660
|
+
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
12661
|
+
return new RegExp(`^${escaped}$`);
|
|
12662
|
+
}
|
|
12663
|
+
function matchString(value, matcher) {
|
|
12664
|
+
if (matcher === undefined)
|
|
12665
|
+
return true;
|
|
12666
|
+
if (value === undefined)
|
|
12667
|
+
return false;
|
|
12668
|
+
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
12669
|
+
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
12670
|
+
}
|
|
12671
|
+
function matchRecord(input, matcher) {
|
|
12672
|
+
if (!matcher)
|
|
12673
|
+
return true;
|
|
12674
|
+
return Object.entries(matcher).every(([path, expected]) => {
|
|
12675
|
+
const actual = getPathValue(input, path);
|
|
12676
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
12677
|
+
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
12678
|
+
}
|
|
12679
|
+
return actual === expected;
|
|
12680
|
+
});
|
|
12681
|
+
}
|
|
12682
|
+
function eventMatchesFilter(event, filter) {
|
|
12683
|
+
return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
|
|
12684
|
+
}
|
|
12685
|
+
function channelMatchesEvent(channel, event) {
|
|
12686
|
+
if (!channel.enabled)
|
|
12687
|
+
return false;
|
|
12688
|
+
if (!channel.filters || channel.filters.length === 0)
|
|
12689
|
+
return true;
|
|
12690
|
+
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
12691
|
+
}
|
|
12692
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
12693
|
+
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
12694
|
+
function getEventsDataDir(override) {
|
|
12695
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
12696
|
+
}
|
|
12697
|
+
|
|
12698
|
+
class JsonEventsStore {
|
|
12699
|
+
dataDir;
|
|
12700
|
+
channelsPath;
|
|
12701
|
+
eventsPath;
|
|
12702
|
+
deliveriesPath;
|
|
12703
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
12704
|
+
this.dataDir = dataDir;
|
|
12705
|
+
this.channelsPath = join(dataDir, "channels.json");
|
|
12706
|
+
this.eventsPath = join(dataDir, "events.json");
|
|
12707
|
+
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
12708
|
+
}
|
|
12709
|
+
async init() {
|
|
12710
|
+
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
12711
|
+
await chmod(this.dataDir, 448).catch(() => {
|
|
12712
|
+
return;
|
|
12713
|
+
});
|
|
12714
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
12715
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
12716
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
12717
|
+
}
|
|
12718
|
+
async addChannel(channel) {
|
|
12719
|
+
await this.init();
|
|
12720
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
12721
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
12722
|
+
if (index >= 0) {
|
|
12723
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
12724
|
+
} else {
|
|
12725
|
+
channels.push(channel);
|
|
12726
|
+
}
|
|
12727
|
+
await this.writeJson(this.channelsPath, channels);
|
|
12728
|
+
return index >= 0 ? channels[index] : channel;
|
|
12729
|
+
}
|
|
12730
|
+
async listChannels() {
|
|
12731
|
+
await this.init();
|
|
12732
|
+
return this.readJson(this.channelsPath, []);
|
|
12733
|
+
}
|
|
12734
|
+
async getChannel(id) {
|
|
12735
|
+
const channels = await this.listChannels();
|
|
12736
|
+
return channels.find((channel) => channel.id === id);
|
|
12737
|
+
}
|
|
12738
|
+
async removeChannel(id) {
|
|
12739
|
+
await this.init();
|
|
12740
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
12741
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
12742
|
+
await this.writeJson(this.channelsPath, next);
|
|
12743
|
+
return next.length !== channels.length;
|
|
12744
|
+
}
|
|
12745
|
+
async appendEvent(event) {
|
|
12746
|
+
await this.init();
|
|
12747
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
12748
|
+
events.push(event);
|
|
12749
|
+
await this.writeJson(this.eventsPath, events);
|
|
12750
|
+
return event;
|
|
12751
|
+
}
|
|
12752
|
+
async listEvents() {
|
|
12753
|
+
await this.init();
|
|
12754
|
+
return this.readJson(this.eventsPath, []);
|
|
12755
|
+
}
|
|
12756
|
+
async findEventByIdentity(identity) {
|
|
12757
|
+
const events = await this.listEvents();
|
|
12758
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
12759
|
+
}
|
|
12760
|
+
async appendDelivery(result) {
|
|
12761
|
+
await this.init();
|
|
12762
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
12763
|
+
deliveries.push(result);
|
|
12764
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
12765
|
+
return result;
|
|
12766
|
+
}
|
|
12767
|
+
async listDeliveries() {
|
|
12768
|
+
await this.init();
|
|
12769
|
+
return this.readJson(this.deliveriesPath, []);
|
|
12770
|
+
}
|
|
12771
|
+
async exportData() {
|
|
12772
|
+
return {
|
|
12773
|
+
channels: await this.listChannels(),
|
|
12774
|
+
events: await this.listEvents(),
|
|
12775
|
+
deliveries: await this.listDeliveries()
|
|
12776
|
+
};
|
|
12777
|
+
}
|
|
12778
|
+
async ensureArrayFile(path) {
|
|
12779
|
+
if (!existsSync(path)) {
|
|
12780
|
+
await writeFile(path, `[]
|
|
12781
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
12782
|
+
}
|
|
12783
|
+
await chmod(path, 384).catch(() => {
|
|
12784
|
+
return;
|
|
12785
|
+
});
|
|
12786
|
+
}
|
|
12787
|
+
async readJson(path, fallback) {
|
|
12788
|
+
try {
|
|
12789
|
+
const raw = await readFile(path, "utf-8");
|
|
12790
|
+
if (!raw.trim())
|
|
12791
|
+
return fallback;
|
|
12792
|
+
return JSON.parse(raw);
|
|
12793
|
+
} catch (error) {
|
|
12794
|
+
if (error.code === "ENOENT")
|
|
12795
|
+
return fallback;
|
|
12796
|
+
throw error;
|
|
12797
|
+
}
|
|
12798
|
+
}
|
|
12799
|
+
async writeJson(path, value) {
|
|
12800
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
12801
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
12802
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
12803
|
+
await rename(tempPath, path);
|
|
12804
|
+
await chmod(path, 384).catch(() => {
|
|
12805
|
+
return;
|
|
12806
|
+
});
|
|
12807
|
+
}
|
|
12808
|
+
}
|
|
12809
|
+
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
12810
|
+
function buildSignatureBase(timestamp, body) {
|
|
12811
|
+
return `${timestamp}.${body}`;
|
|
12812
|
+
}
|
|
12813
|
+
function signPayload(secret, timestamp, body) {
|
|
12814
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
12815
|
+
return `sha256=${digest}`;
|
|
12816
|
+
}
|
|
12817
|
+
function now() {
|
|
12818
|
+
return new Date().toISOString();
|
|
12819
|
+
}
|
|
12820
|
+
function truncate(value, max = 4096) {
|
|
12821
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
12822
|
+
}
|
|
12823
|
+
function buildWebhookRequest(event, channel) {
|
|
12824
|
+
if (!channel.webhook)
|
|
12825
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
12826
|
+
const body = JSON.stringify(event);
|
|
12827
|
+
const timestamp = event.time;
|
|
12828
|
+
const headers = {
|
|
12829
|
+
"Content-Type": "application/json",
|
|
12830
|
+
"User-Agent": "@hasna/events",
|
|
12831
|
+
"X-Hasna-Event-Id": event.id,
|
|
12832
|
+
"X-Hasna-Event-Type": event.type,
|
|
12833
|
+
"X-Hasna-Timestamp": timestamp,
|
|
12834
|
+
...channel.webhook.headers
|
|
12835
|
+
};
|
|
12836
|
+
if (channel.webhook.secret) {
|
|
12837
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
12838
|
+
}
|
|
12839
|
+
return { body, headers };
|
|
12840
|
+
}
|
|
12841
|
+
async function dispatchWebhook(event, channel, options = {}) {
|
|
12842
|
+
if (!channel.webhook)
|
|
12843
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
12844
|
+
const startedAt = now();
|
|
12845
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
12846
|
+
const controller = new AbortController;
|
|
12847
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
12848
|
+
try {
|
|
12849
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
12850
|
+
method: "POST",
|
|
12851
|
+
headers,
|
|
12852
|
+
body,
|
|
12853
|
+
signal: controller.signal
|
|
12854
|
+
});
|
|
12855
|
+
const responseBody = truncate(await response.text());
|
|
12856
|
+
return {
|
|
12857
|
+
attempt: 1,
|
|
12858
|
+
status: response.ok ? "success" : "failed",
|
|
12859
|
+
startedAt,
|
|
12860
|
+
completedAt: now(),
|
|
12861
|
+
responseStatus: response.status,
|
|
12862
|
+
responseBody,
|
|
12863
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
12864
|
+
};
|
|
12865
|
+
} catch (error) {
|
|
12866
|
+
return {
|
|
12867
|
+
attempt: 1,
|
|
12868
|
+
status: "failed",
|
|
12869
|
+
startedAt,
|
|
12870
|
+
completedAt: now(),
|
|
12871
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12872
|
+
};
|
|
12873
|
+
} finally {
|
|
12874
|
+
clearTimeout(timeout);
|
|
12875
|
+
}
|
|
12876
|
+
}
|
|
12877
|
+
async function dispatchCommand(event, channel) {
|
|
12878
|
+
if (!channel.command)
|
|
12879
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
12880
|
+
const startedAt = now();
|
|
12881
|
+
const eventJson = JSON.stringify(event);
|
|
12882
|
+
const env = {
|
|
12883
|
+
...process.env,
|
|
12884
|
+
...channel.command.env,
|
|
12885
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
12886
|
+
HASNA_EVENT_ID: event.id,
|
|
12887
|
+
HASNA_EVENT_TYPE: event.type,
|
|
12888
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
12889
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
12890
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
12891
|
+
HASNA_EVENT_TIME: event.time,
|
|
12892
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
12893
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
12894
|
+
HASNA_EVENT_JSON: eventJson
|
|
12895
|
+
};
|
|
12896
|
+
return new Promise((resolve) => {
|
|
12897
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
12898
|
+
cwd: channel.command.cwd,
|
|
12899
|
+
env,
|
|
12900
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
12901
|
+
});
|
|
12902
|
+
let stdout = "";
|
|
12903
|
+
let stderr = "";
|
|
12904
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
12905
|
+
child.stdin.end(eventJson);
|
|
12906
|
+
child.stdout.on("data", (chunk) => {
|
|
12907
|
+
stdout += chunk.toString();
|
|
12908
|
+
});
|
|
12909
|
+
child.stderr.on("data", (chunk) => {
|
|
12910
|
+
stderr += chunk.toString();
|
|
12911
|
+
});
|
|
12912
|
+
child.on("error", (error) => {
|
|
12913
|
+
clearTimeout(timeout);
|
|
12914
|
+
resolve({
|
|
12915
|
+
attempt: 1,
|
|
12916
|
+
status: "failed",
|
|
12917
|
+
startedAt,
|
|
12918
|
+
completedAt: now(),
|
|
12919
|
+
stdout: truncate(stdout),
|
|
12920
|
+
stderr: truncate(stderr),
|
|
12921
|
+
error: error.message
|
|
12922
|
+
});
|
|
12923
|
+
});
|
|
12924
|
+
child.on("close", (code, signal) => {
|
|
12925
|
+
clearTimeout(timeout);
|
|
12926
|
+
const success = code === 0;
|
|
12927
|
+
resolve({
|
|
12928
|
+
attempt: 1,
|
|
12929
|
+
status: success ? "success" : "failed",
|
|
12930
|
+
startedAt,
|
|
12931
|
+
completedAt: now(),
|
|
12932
|
+
stdout: truncate(stdout),
|
|
12933
|
+
stderr: truncate(stderr),
|
|
12934
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
12935
|
+
});
|
|
12936
|
+
});
|
|
12937
|
+
});
|
|
12938
|
+
}
|
|
12939
|
+
async function dispatchChannel(event, channel, options = {}) {
|
|
12940
|
+
if (channel.transport === "webhook")
|
|
12941
|
+
return dispatchWebhook(event, channel, options);
|
|
12942
|
+
if (channel.transport === "command")
|
|
12943
|
+
return dispatchCommand(event, channel);
|
|
12944
|
+
return {
|
|
12945
|
+
attempt: 1,
|
|
12946
|
+
status: "skipped",
|
|
12947
|
+
startedAt: now(),
|
|
12948
|
+
completedAt: now(),
|
|
12949
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
12950
|
+
};
|
|
12951
|
+
}
|
|
12952
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
12953
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
12954
|
+
return {
|
|
12955
|
+
id: randomUUID(),
|
|
12956
|
+
eventId: event.id,
|
|
12957
|
+
channelId: channel.id,
|
|
12958
|
+
transport: channel.transport,
|
|
12959
|
+
status,
|
|
12960
|
+
attempts,
|
|
12961
|
+
createdAt: attempts[0]?.startedAt ?? now(),
|
|
12962
|
+
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
12963
|
+
};
|
|
12964
|
+
}
|
|
12965
|
+
function createEvent(input) {
|
|
12966
|
+
return {
|
|
12967
|
+
id: input.id ?? randomUUID2(),
|
|
12968
|
+
source: input.source,
|
|
12969
|
+
type: input.type,
|
|
12970
|
+
time: normalizeTime(input.time),
|
|
12971
|
+
subject: input.subject,
|
|
12972
|
+
severity: input.severity ?? "info",
|
|
12973
|
+
data: input.data ?? {},
|
|
12974
|
+
message: input.message,
|
|
12975
|
+
dedupeKey: input.dedupeKey,
|
|
12976
|
+
schemaVersion: input.schemaVersion ?? "1.0",
|
|
12977
|
+
metadata: input.metadata ?? {}
|
|
12978
|
+
};
|
|
12979
|
+
}
|
|
12980
|
+
|
|
12981
|
+
class EventsClient {
|
|
12982
|
+
store;
|
|
12983
|
+
redactors;
|
|
12984
|
+
transportOptions;
|
|
12985
|
+
constructor(options = {}) {
|
|
12986
|
+
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
12987
|
+
this.redactors = options.redactors ?? [];
|
|
12988
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
12989
|
+
}
|
|
12990
|
+
async addChannel(input) {
|
|
12991
|
+
const timestamp = new Date().toISOString();
|
|
12992
|
+
return this.store.addChannel({
|
|
12993
|
+
...input,
|
|
12994
|
+
createdAt: input.createdAt ?? timestamp,
|
|
12995
|
+
updatedAt: input.updatedAt ?? timestamp
|
|
12996
|
+
});
|
|
12997
|
+
}
|
|
12998
|
+
async listChannels() {
|
|
12999
|
+
return this.store.listChannels();
|
|
13000
|
+
}
|
|
13001
|
+
async removeChannel(id) {
|
|
13002
|
+
return this.store.removeChannel(id);
|
|
13003
|
+
}
|
|
13004
|
+
async emit(input, options = {}) {
|
|
13005
|
+
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
13006
|
+
if (options.dedupe !== false) {
|
|
13007
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
13008
|
+
if (existing) {
|
|
13009
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
13010
|
+
}
|
|
13011
|
+
}
|
|
13012
|
+
await this.store.appendEvent(event);
|
|
13013
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
13014
|
+
return { event, deliveries, deduped: false };
|
|
13015
|
+
}
|
|
13016
|
+
async listEvents() {
|
|
13017
|
+
return this.store.listEvents();
|
|
13018
|
+
}
|
|
13019
|
+
async listDeliveries() {
|
|
13020
|
+
return this.store.listDeliveries();
|
|
13021
|
+
}
|
|
13022
|
+
async deliver(event) {
|
|
13023
|
+
const channels = await this.store.listChannels();
|
|
13024
|
+
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
13025
|
+
const deliveries = [];
|
|
13026
|
+
for (const channel of selected) {
|
|
13027
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
13028
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
13029
|
+
await this.store.appendDelivery(result);
|
|
13030
|
+
deliveries.push(result);
|
|
13031
|
+
}
|
|
13032
|
+
return deliveries;
|
|
13033
|
+
}
|
|
13034
|
+
async testChannel(id, input = {}) {
|
|
13035
|
+
const channel = await this.store.getChannel(id);
|
|
13036
|
+
if (!channel)
|
|
13037
|
+
throw new Error(`Channel not found: ${id}`);
|
|
13038
|
+
const event = createEvent({
|
|
13039
|
+
source: input.source ?? "hasna.events",
|
|
13040
|
+
type: input.type ?? "events.test",
|
|
13041
|
+
subject: input.subject ?? id,
|
|
13042
|
+
severity: input.severity ?? "info",
|
|
13043
|
+
data: input.data ?? { test: true },
|
|
13044
|
+
message: input.message ?? "Hasna events test delivery",
|
|
13045
|
+
dedupeKey: input.dedupeKey,
|
|
13046
|
+
schemaVersion: input.schemaVersion,
|
|
13047
|
+
metadata: input.metadata,
|
|
13048
|
+
time: input.time,
|
|
13049
|
+
id: input.id
|
|
13050
|
+
});
|
|
13051
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
13052
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
13053
|
+
await this.store.appendDelivery(result);
|
|
13054
|
+
return result;
|
|
13055
|
+
}
|
|
13056
|
+
async replay(options = {}) {
|
|
13057
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
13058
|
+
if (options.eventId && event.id !== options.eventId)
|
|
13059
|
+
return false;
|
|
13060
|
+
if (options.source && event.source !== options.source)
|
|
13061
|
+
return false;
|
|
13062
|
+
if (options.type && event.type !== options.type)
|
|
13063
|
+
return false;
|
|
13064
|
+
return true;
|
|
13065
|
+
});
|
|
13066
|
+
if (options.dryRun)
|
|
13067
|
+
return { events, deliveries: [] };
|
|
13068
|
+
const deliveries = [];
|
|
13069
|
+
for (const event of events) {
|
|
13070
|
+
deliveries.push(...await this.deliver(event));
|
|
13071
|
+
}
|
|
13072
|
+
return { events, deliveries };
|
|
13073
|
+
}
|
|
13074
|
+
async applyRedaction(event, channel) {
|
|
13075
|
+
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
13076
|
+
for (const redactor of this.redactors) {
|
|
13077
|
+
next = await redactor(next, channel);
|
|
13078
|
+
}
|
|
13079
|
+
return next;
|
|
13080
|
+
}
|
|
13081
|
+
async deliverWithRetry(event, channel) {
|
|
13082
|
+
const policy = normalizeRetryPolicy(channel.retry);
|
|
13083
|
+
const attempts = [];
|
|
13084
|
+
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
13085
|
+
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
13086
|
+
attempt.attempt = index + 1;
|
|
13087
|
+
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
13088
|
+
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
13089
|
+
}
|
|
13090
|
+
attempts.push(attempt);
|
|
13091
|
+
if (attempt.status !== "failed")
|
|
13092
|
+
break;
|
|
13093
|
+
if (attempt.nextBackoffMs)
|
|
13094
|
+
await Bun.sleep(attempt.nextBackoffMs);
|
|
13095
|
+
}
|
|
13096
|
+
return createDeliveryResult(event, channel, attempts);
|
|
13097
|
+
}
|
|
13098
|
+
}
|
|
13099
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
13100
|
+
if (paths.length === 0)
|
|
13101
|
+
return event;
|
|
13102
|
+
const copy = structuredClone(event);
|
|
13103
|
+
for (const path of paths) {
|
|
13104
|
+
setPath(copy, path, replacement);
|
|
13105
|
+
}
|
|
13106
|
+
return copy;
|
|
13107
|
+
}
|
|
13108
|
+
function sanitizeChannelForOutput(channel) {
|
|
13109
|
+
const copy = structuredClone(channel);
|
|
13110
|
+
if (copy.webhook?.secret)
|
|
13111
|
+
copy.webhook.secret = "[REDACTED]";
|
|
13112
|
+
if (copy.command?.env) {
|
|
13113
|
+
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
13114
|
+
}
|
|
13115
|
+
return copy;
|
|
13116
|
+
}
|
|
13117
|
+
function sanitizeChannelsForOutput(channels) {
|
|
13118
|
+
return channels.map(sanitizeChannelForOutput);
|
|
13119
|
+
}
|
|
13120
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
13121
|
+
return redactValue(event, replacement);
|
|
13122
|
+
}
|
|
13123
|
+
function shouldRedactKey(key) {
|
|
13124
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
13125
|
+
}
|
|
13126
|
+
function redactValue(value, replacement) {
|
|
13127
|
+
if (Array.isArray(value))
|
|
13128
|
+
return value.map((item) => redactValue(item, replacement));
|
|
13129
|
+
if (!value || typeof value !== "object")
|
|
13130
|
+
return value;
|
|
13131
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
13132
|
+
key,
|
|
13133
|
+
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
13134
|
+
]));
|
|
13135
|
+
}
|
|
13136
|
+
function setPath(input, path, replacement) {
|
|
13137
|
+
const parts = path.split(".");
|
|
13138
|
+
let cursor = input;
|
|
13139
|
+
for (const part of parts.slice(0, -1)) {
|
|
13140
|
+
const next = cursor[part];
|
|
13141
|
+
if (!next || typeof next !== "object")
|
|
13142
|
+
return;
|
|
13143
|
+
cursor = next;
|
|
13144
|
+
}
|
|
13145
|
+
const last = parts.at(-1);
|
|
13146
|
+
if (last && last in cursor)
|
|
13147
|
+
cursor[last] = replacement;
|
|
13148
|
+
}
|
|
13149
|
+
function normalizeTime(value) {
|
|
13150
|
+
if (!value)
|
|
13151
|
+
return new Date().toISOString();
|
|
13152
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
13153
|
+
}
|
|
13154
|
+
function normalizeRetryPolicy(policy) {
|
|
13155
|
+
return {
|
|
13156
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
13157
|
+
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
13158
|
+
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
13159
|
+
};
|
|
13160
|
+
}
|
|
13161
|
+
function parseJsonObject(value, fallback) {
|
|
13162
|
+
if (!value)
|
|
13163
|
+
return fallback;
|
|
13164
|
+
const parsed = JSON.parse(value);
|
|
13165
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
13166
|
+
throw new Error("Expected a JSON object");
|
|
13167
|
+
}
|
|
13168
|
+
return parsed;
|
|
13169
|
+
}
|
|
13170
|
+
function parseHeaders(values) {
|
|
13171
|
+
if (!values?.length)
|
|
13172
|
+
return;
|
|
13173
|
+
const headers = {};
|
|
13174
|
+
for (const value of values) {
|
|
13175
|
+
const separator = value.indexOf("=");
|
|
13176
|
+
if (separator === -1)
|
|
13177
|
+
throw new Error(`Invalid header, expected name=value: ${value}`);
|
|
13178
|
+
headers[value.slice(0, separator)] = value.slice(separator + 1);
|
|
13179
|
+
}
|
|
13180
|
+
return headers;
|
|
13181
|
+
}
|
|
13182
|
+
function parseFilter(options) {
|
|
13183
|
+
const filter2 = {};
|
|
13184
|
+
if (options.source)
|
|
13185
|
+
filter2.source = options.source;
|
|
13186
|
+
if (options.type)
|
|
13187
|
+
filter2.type = options.type;
|
|
13188
|
+
if (options.subject)
|
|
13189
|
+
filter2.subject = options.subject;
|
|
13190
|
+
if (options.severity)
|
|
13191
|
+
filter2.severity = options.severity;
|
|
13192
|
+
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
13193
|
+
}
|
|
13194
|
+
function createClient(options) {
|
|
13195
|
+
if (options.createClient)
|
|
13196
|
+
return options.createClient();
|
|
13197
|
+
return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
|
|
13198
|
+
}
|
|
13199
|
+
function print(value, json, text) {
|
|
13200
|
+
if (json)
|
|
13201
|
+
console.log(JSON.stringify(value, null, 2));
|
|
13202
|
+
else
|
|
13203
|
+
console.log(text);
|
|
13204
|
+
}
|
|
13205
|
+
function hasJsonOption(options) {
|
|
13206
|
+
return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
|
|
13207
|
+
}
|
|
13208
|
+
function wantsJson(actionOptions, command) {
|
|
13209
|
+
return hasJsonOption(actionOptions) || hasJsonOption(command);
|
|
13210
|
+
}
|
|
13211
|
+
function registerWebhookCommands(program, options) {
|
|
13212
|
+
const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
|
|
13213
|
+
webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
|
|
13214
|
+
const timestamp = new Date().toISOString();
|
|
13215
|
+
const channel = {
|
|
13216
|
+
id: actionOptions.id,
|
|
13217
|
+
name: actionOptions.name,
|
|
13218
|
+
enabled: !actionOptions.disabled,
|
|
13219
|
+
transport: actionOptions.transport,
|
|
13220
|
+
filters: parseFilter(actionOptions),
|
|
13221
|
+
retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
|
|
13222
|
+
redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
|
|
13223
|
+
createdAt: timestamp,
|
|
13224
|
+
updatedAt: timestamp
|
|
13225
|
+
};
|
|
13226
|
+
if (actionOptions.transport === "webhook") {
|
|
13227
|
+
channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
|
|
13228
|
+
} else if (actionOptions.transport === "command") {
|
|
13229
|
+
channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
|
|
13230
|
+
} else {
|
|
13231
|
+
throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
|
|
13232
|
+
}
|
|
13233
|
+
const saved = await createClient(options).addChannel(channel);
|
|
13234
|
+
print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
|
|
13235
|
+
});
|
|
13236
|
+
webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
13237
|
+
const channels = await createClient(options).listChannels();
|
|
13238
|
+
if (wantsJson(actionOptions, command)) {
|
|
13239
|
+
console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
|
|
13240
|
+
return;
|
|
13241
|
+
}
|
|
13242
|
+
if (!channels.length) {
|
|
13243
|
+
console.log("No channels configured.");
|
|
13244
|
+
return;
|
|
13245
|
+
}
|
|
13246
|
+
for (const channel of channels) {
|
|
13247
|
+
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
13248
|
+
}
|
|
13249
|
+
});
|
|
13250
|
+
webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
13251
|
+
const removed = await createClient(options).removeChannel(id);
|
|
13252
|
+
print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
13253
|
+
});
|
|
13254
|
+
webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
13255
|
+
const result = await createClient(options).testChannel(id, {
|
|
13256
|
+
source: options.source,
|
|
13257
|
+
type: actionOptions.type,
|
|
13258
|
+
subject: actionOptions.subject ?? id,
|
|
13259
|
+
message: actionOptions.message,
|
|
13260
|
+
data: parseJsonObject(actionOptions.data, { test: true })
|
|
13261
|
+
});
|
|
13262
|
+
print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
|
|
13263
|
+
});
|
|
13264
|
+
return webhooks;
|
|
13265
|
+
}
|
|
13266
|
+
function registerEventCommands(program, options) {
|
|
13267
|
+
const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
13268
|
+
events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions, command) => {
|
|
13269
|
+
const result = await createClient(options).emit({
|
|
13270
|
+
source: actionOptions.source ?? options.source,
|
|
13271
|
+
type,
|
|
13272
|
+
subject: actionOptions.subject,
|
|
13273
|
+
severity: actionOptions.severity,
|
|
13274
|
+
message: actionOptions.message,
|
|
13275
|
+
dedupeKey: actionOptions.dedupeKey,
|
|
13276
|
+
data: parseJsonObject(actionOptions.data, {}),
|
|
13277
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
13278
|
+
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
13279
|
+
print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
13280
|
+
});
|
|
13281
|
+
events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
13282
|
+
let rows = await createClient(options).listEvents();
|
|
13283
|
+
if (actionOptions.source)
|
|
13284
|
+
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
13285
|
+
if (actionOptions.type)
|
|
13286
|
+
rows = rows.filter((event) => event.type === actionOptions.type);
|
|
13287
|
+
if (actionOptions.limit)
|
|
13288
|
+
rows = rows.slice(-actionOptions.limit);
|
|
13289
|
+
if (wantsJson(actionOptions, command)) {
|
|
13290
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
13291
|
+
return;
|
|
13292
|
+
}
|
|
13293
|
+
if (!rows.length) {
|
|
13294
|
+
console.log("No events recorded.");
|
|
13295
|
+
return;
|
|
13296
|
+
}
|
|
13297
|
+
for (const event of rows)
|
|
13298
|
+
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
13299
|
+
});
|
|
13300
|
+
events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
13301
|
+
const result = await createClient(options).replay({
|
|
13302
|
+
eventId: actionOptions.id,
|
|
13303
|
+
source: actionOptions.source,
|
|
13304
|
+
type: actionOptions.type,
|
|
13305
|
+
dryRun: actionOptions.dryRun
|
|
13306
|
+
});
|
|
13307
|
+
print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
|
|
13308
|
+
});
|
|
13309
|
+
return events;
|
|
13310
|
+
}
|
|
13311
|
+
function registerEventsCommands(program, options) {
|
|
13312
|
+
registerWebhookCommands(program, options);
|
|
13313
|
+
registerEventCommands(program, options);
|
|
13314
|
+
}
|
|
13315
|
+
function parseNumber(value) {
|
|
13316
|
+
const parsed = Number(value);
|
|
13317
|
+
if (!Number.isFinite(parsed))
|
|
13318
|
+
throw new Error(`Expected a number, got ${value}`);
|
|
13319
|
+
return parsed;
|
|
13320
|
+
}
|
|
13321
|
+
function collectValues(value, previous) {
|
|
13322
|
+
previous.push(value);
|
|
13323
|
+
return previous;
|
|
13324
|
+
}
|
|
13325
|
+
|
|
12644
13326
|
// node_modules/commander/esm.mjs
|
|
12645
13327
|
var import__ = __toESM(require_commander(), 1);
|
|
12646
13328
|
var {
|
|
@@ -12662,7 +13344,7 @@ init_configs();
|
|
|
12662
13344
|
import chalk from "chalk";
|
|
12663
13345
|
import { existsSync as existsSync12, readFileSync as readFileSync6 } from "fs";
|
|
12664
13346
|
import { homedir as homedir11 } from "os";
|
|
12665
|
-
import { join as
|
|
13347
|
+
import { join as join13, resolve as resolve4 } from "path";
|
|
12666
13348
|
|
|
12667
13349
|
// src/db/profiles.ts
|
|
12668
13350
|
init_types();
|
|
@@ -12690,7 +13372,7 @@ function uniqueProfileSlug(name, db, excludeId) {
|
|
|
12690
13372
|
function createProfile(input, db) {
|
|
12691
13373
|
const d = db || getDatabase();
|
|
12692
13374
|
const id = uuid();
|
|
12693
|
-
const ts =
|
|
13375
|
+
const ts = now2();
|
|
12694
13376
|
const slug = uniqueProfileSlug(input.name, d);
|
|
12695
13377
|
d.run("INSERT INTO profiles (id, name, slug, description, selectors, variables, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
12696
13378
|
id,
|
|
@@ -12718,7 +13400,7 @@ function listProfiles(db) {
|
|
|
12718
13400
|
function updateProfile(idOrSlug, input, db) {
|
|
12719
13401
|
const d = db || getDatabase();
|
|
12720
13402
|
const existing = getProfile(idOrSlug, d);
|
|
12721
|
-
const ts =
|
|
13403
|
+
const ts = now2();
|
|
12722
13404
|
const updates = ["updated_at = ?"];
|
|
12723
13405
|
const params = [ts];
|
|
12724
13406
|
if (input.name !== undefined) {
|
|
@@ -12802,25 +13484,25 @@ init_redact();
|
|
|
12802
13484
|
init_database();
|
|
12803
13485
|
init_configs();
|
|
12804
13486
|
import { existsSync as existsSync10, mkdirSync as mkdirSync6, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
12805
|
-
import { join as
|
|
13487
|
+
import { join as join11, resolve as resolve2 } from "path";
|
|
12806
13488
|
import { tmpdir } from "os";
|
|
12807
13489
|
async function exportConfigs(outputPath, opts = {}) {
|
|
12808
13490
|
const d = opts.db || getDatabase();
|
|
12809
13491
|
const configs = listConfigs(opts.filter, d);
|
|
12810
13492
|
const absOutput = resolve2(outputPath);
|
|
12811
|
-
const tmpDir =
|
|
12812
|
-
const contentsDir =
|
|
13493
|
+
const tmpDir = join11(tmpdir(), `configs-export-${Date.now()}`);
|
|
13494
|
+
const contentsDir = join11(tmpDir, "contents");
|
|
12813
13495
|
try {
|
|
12814
13496
|
mkdirSync6(contentsDir, { recursive: true });
|
|
12815
13497
|
const manifest = {
|
|
12816
13498
|
version: "1.0.0",
|
|
12817
|
-
exported_at:
|
|
13499
|
+
exported_at: now2(),
|
|
12818
13500
|
configs: configs.map(({ content: _content, ...meta }) => meta)
|
|
12819
13501
|
};
|
|
12820
|
-
writeFileSync3(
|
|
13502
|
+
writeFileSync3(join11(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
12821
13503
|
for (const config of configs) {
|
|
12822
13504
|
const fileName = `${config.slug}.${config.format === "text" ? "txt" : config.format}`;
|
|
12823
|
-
writeFileSync3(
|
|
13505
|
+
writeFileSync3(join11(contentsDir, fileName), config.content, "utf-8");
|
|
12824
13506
|
}
|
|
12825
13507
|
const proc = Bun.spawn(["tar", "czf", absOutput, "-C", tmpDir, "."], {
|
|
12826
13508
|
stdout: "pipe",
|
|
@@ -12843,13 +13525,13 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
12843
13525
|
init_database();
|
|
12844
13526
|
init_configs();
|
|
12845
13527
|
import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync5, rmSync as rmSync2 } from "fs";
|
|
12846
|
-
import { join as
|
|
13528
|
+
import { join as join12, resolve as resolve3 } from "path";
|
|
12847
13529
|
import { tmpdir as tmpdir2 } from "os";
|
|
12848
13530
|
async function importConfigs(bundlePath, opts = {}) {
|
|
12849
13531
|
const d = opts.db || getDatabase();
|
|
12850
13532
|
const conflict = opts.conflict ?? "skip";
|
|
12851
13533
|
const absPath = resolve3(bundlePath);
|
|
12852
|
-
const tmpDir =
|
|
13534
|
+
const tmpDir = join12(tmpdir2(), `configs-import-${Date.now()}`);
|
|
12853
13535
|
const result = { created: 0, updated: 0, skipped: 0, errors: [] };
|
|
12854
13536
|
try {
|
|
12855
13537
|
mkdirSync7(tmpDir, { recursive: true });
|
|
@@ -12862,14 +13544,14 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
12862
13544
|
const stderr = await new Response(proc.stderr).text();
|
|
12863
13545
|
throw new Error(`tar extraction failed: ${stderr}`);
|
|
12864
13546
|
}
|
|
12865
|
-
const manifestPath =
|
|
13547
|
+
const manifestPath = join12(tmpDir, "manifest.json");
|
|
12866
13548
|
if (!existsSync11(manifestPath))
|
|
12867
13549
|
throw new Error("Invalid bundle: missing manifest.json");
|
|
12868
13550
|
const manifest = JSON.parse(readFileSync5(manifestPath, "utf-8"));
|
|
12869
13551
|
for (const meta of manifest.configs) {
|
|
12870
13552
|
try {
|
|
12871
13553
|
const ext = meta.format === "text" ? "txt" : meta.format;
|
|
12872
|
-
const contentFile =
|
|
13554
|
+
const contentFile = join12(tmpDir, "contents", `${meta.slug}.${ext}`);
|
|
12873
13555
|
const content = existsSync11(contentFile) ? readFileSync5(contentFile, "utf-8") : "";
|
|
12874
13556
|
let existing = null;
|
|
12875
13557
|
try {
|
|
@@ -13160,15 +13842,15 @@ program.command("sync").description("Sync known AI coding configs from disk into
|
|
|
13160
13842
|
if (opts.project) {
|
|
13161
13843
|
const dir = typeof opts.project === "string" ? opts.project : process.cwd();
|
|
13162
13844
|
if (opts.all) {
|
|
13163
|
-
const { readdirSync:
|
|
13845
|
+
const { readdirSync: readdirSync5, statSync: st } = await import("fs");
|
|
13164
13846
|
const absDir = expandPath(dir);
|
|
13165
|
-
const entries =
|
|
13847
|
+
const entries = readdirSync5(absDir, { withFileTypes: true });
|
|
13166
13848
|
let totalAdded = 0, totalUpdated = 0, totalUnchanged = 0, projects = 0;
|
|
13167
13849
|
for (const entry of entries) {
|
|
13168
13850
|
if (!entry.isDirectory())
|
|
13169
13851
|
continue;
|
|
13170
|
-
const projDir =
|
|
13171
|
-
const hasClaude = existsSync12(
|
|
13852
|
+
const projDir = join13(absDir, entry.name);
|
|
13853
|
+
const hasClaude = existsSync12(join13(projDir, "CLAUDE.md")) || existsSync12(join13(projDir, ".mcp.json")) || existsSync12(join13(projDir, ".claude"));
|
|
13172
13854
|
if (!hasClaude)
|
|
13173
13855
|
continue;
|
|
13174
13856
|
const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun });
|
|
@@ -13216,7 +13898,7 @@ program.command("import <file>").description("Import configs from a tar.gz bundl
|
|
|
13216
13898
|
}
|
|
13217
13899
|
});
|
|
13218
13900
|
program.command("whoami").description("Show setup summary").action(async () => {
|
|
13219
|
-
const dbPath = process.env["CONFIGS_DB_PATH"] ||
|
|
13901
|
+
const dbPath = process.env["CONFIGS_DB_PATH"] || join13(homedir11(), ".hasna", "configs", "configs.db");
|
|
13220
13902
|
const stats = getConfigStats();
|
|
13221
13903
|
console.log(chalk.bold("@hasna/configs") + chalk.dim(" v" + pkg.version));
|
|
13222
13904
|
console.log(chalk.cyan("DB:") + " " + dbPath);
|
|
@@ -13590,7 +14272,7 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
|
|
|
13590
14272
|
}
|
|
13591
14273
|
});
|
|
13592
14274
|
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) => {
|
|
13593
|
-
const dbPath =
|
|
14275
|
+
const dbPath = join13(homedir11(), ".hasna", "configs", "configs.db");
|
|
13594
14276
|
if (opts.force && existsSync12(dbPath)) {
|
|
13595
14277
|
const { rmSync: rmSync3 } = await import("fs");
|
|
13596
14278
|
rmSync3(dbPath);
|
|
@@ -13644,7 +14326,7 @@ DB stats:`));
|
|
|
13644
14326
|
DB: ${dbPath}`));
|
|
13645
14327
|
});
|
|
13646
14328
|
program.command("status").description("Health check: total configs, drift from disk, unredacted secrets").action(async () => {
|
|
13647
|
-
const dbPath =
|
|
14329
|
+
const dbPath = join13(homedir11(), ".hasna", "configs", "configs.db");
|
|
13648
14330
|
const stats = getConfigStats();
|
|
13649
14331
|
const { statSync: st } = await import("fs");
|
|
13650
14332
|
const dbSize = existsSync12(dbPath) ? st(dbPath).size : 0;
|
|
@@ -13681,10 +14363,10 @@ program.command("status").description("Health check: total configs, drift from d
|
|
|
13681
14363
|
});
|
|
13682
14364
|
program.command("backup").description("Export configs to a timestamped backup file").action(async () => {
|
|
13683
14365
|
const { mkdirSync: mk } = await import("fs");
|
|
13684
|
-
const backupDir =
|
|
14366
|
+
const backupDir = join13(homedir11(), ".hasna", "configs", "backups");
|
|
13685
14367
|
mk(backupDir, { recursive: true });
|
|
13686
14368
|
const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 19);
|
|
13687
|
-
const outPath =
|
|
14369
|
+
const outPath = join13(backupDir, `configs-${ts}.tar.gz`);
|
|
13688
14370
|
const result = await exportConfigs(outPath);
|
|
13689
14371
|
const { statSync: st } = await import("fs");
|
|
13690
14372
|
const size = st(outPath).size;
|
|
@@ -13837,9 +14519,9 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
13837
14519
|
const absDir = expandPath2(k.rulesDir);
|
|
13838
14520
|
if (!existsSync12(absDir))
|
|
13839
14521
|
continue;
|
|
13840
|
-
const { readdirSync:
|
|
13841
|
-
for (const f of
|
|
13842
|
-
const abs =
|
|
14522
|
+
const { readdirSync: readdirSync5 } = await import("fs");
|
|
14523
|
+
for (const f of readdirSync5(absDir).filter((f2) => f2.endsWith(".md"))) {
|
|
14524
|
+
const abs = join13(absDir, f);
|
|
13843
14525
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
13844
14526
|
}
|
|
13845
14527
|
} else {
|
|
@@ -13867,7 +14549,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
13867
14549
|
if (!existsSync12(absDir))
|
|
13868
14550
|
continue;
|
|
13869
14551
|
for (const f of rd(absDir).filter((f2) => f2.endsWith(".md"))) {
|
|
13870
|
-
const abs =
|
|
14552
|
+
const abs = join13(absDir, f);
|
|
13871
14553
|
if (!mtimes.has(abs)) {
|
|
13872
14554
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
13873
14555
|
changed++;
|
|
@@ -14059,4 +14741,5 @@ program.command("feedback <message>").description("Send feedback about this serv
|
|
|
14059
14741
|
console.log(chalk.green("\u2713") + " Feedback saved. Thank you!");
|
|
14060
14742
|
});
|
|
14061
14743
|
program.version(pkg.version).name("configs");
|
|
14744
|
+
registerEventsCommands(program, { source: "configs" });
|
|
14062
14745
|
program.parse(process.argv);
|