@hasna/hooks 0.2.15 → 0.2.17
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/bin/index.js +788 -105
- package/package.json +3 -2
- package/hooks/hook-agentmessages/bin/cli.ts +0 -125
- package/hooks/hook-checkdocs/bun.lock +0 -25
package/bin/index.js
CHANGED
|
@@ -861,7 +861,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
861
861
|
this._exitCallback = (err) => {
|
|
862
862
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
863
863
|
throw err;
|
|
864
|
-
}
|
|
864
|
+
}
|
|
865
865
|
};
|
|
866
866
|
}
|
|
867
867
|
return this;
|
|
@@ -3946,9 +3946,9 @@ var require_cli_spinners = __commonJS((exports, module) => {
|
|
|
3946
3946
|
});
|
|
3947
3947
|
|
|
3948
3948
|
// src/lib/installer.ts
|
|
3949
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3950
|
-
import { join, dirname } from "path";
|
|
3951
|
-
import { homedir } from "os";
|
|
3949
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3950
|
+
import { join as join2, dirname } from "path";
|
|
3951
|
+
import { homedir as homedir2 } from "os";
|
|
3952
3952
|
import { fileURLToPath } from "url";
|
|
3953
3953
|
function normalizeHookName(name) {
|
|
3954
3954
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
@@ -3970,20 +3970,20 @@ function getTargetSettingsDir(target) {
|
|
|
3970
3970
|
function getSettingsPath(scope = "global", target = "claude") {
|
|
3971
3971
|
const dir = getTargetSettingsDir(target);
|
|
3972
3972
|
if (scope === "project") {
|
|
3973
|
-
return
|
|
3973
|
+
return join2(process.cwd(), dir, "settings.json");
|
|
3974
3974
|
}
|
|
3975
|
-
return
|
|
3975
|
+
return join2(homedir2(), dir, "settings.json");
|
|
3976
3976
|
}
|
|
3977
3977
|
function getHookPath(name) {
|
|
3978
|
-
return
|
|
3978
|
+
return join2(HOOKS_DIR, normalizeHookName(name));
|
|
3979
3979
|
}
|
|
3980
3980
|
function hookExists(name) {
|
|
3981
|
-
return
|
|
3981
|
+
return existsSync2(getHookPath(name));
|
|
3982
3982
|
}
|
|
3983
3983
|
function readSettings(scope = "global", target = "claude") {
|
|
3984
3984
|
const path = getSettingsPath(scope, target);
|
|
3985
3985
|
try {
|
|
3986
|
-
if (
|
|
3986
|
+
if (existsSync2(path)) {
|
|
3987
3987
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
3988
3988
|
}
|
|
3989
3989
|
} catch (error) {
|
|
@@ -3994,7 +3994,7 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
3994
3994
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
3995
3995
|
const path = getSettingsPath(scope, target);
|
|
3996
3996
|
const dir = dirname(path);
|
|
3997
|
-
if (!
|
|
3997
|
+
if (!existsSync2(dir)) {
|
|
3998
3998
|
mkdirSync(dir, { recursive: true });
|
|
3999
3999
|
}
|
|
4000
4000
|
writeFileSync(path, JSON.stringify(settings, null, 2) + `
|
|
@@ -4136,7 +4136,7 @@ var __dirname2, HOOKS_DIR, EVENT_MAP, getInstalledHooks;
|
|
|
4136
4136
|
var init_installer = __esm(() => {
|
|
4137
4137
|
init_registry();
|
|
4138
4138
|
__dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
4139
|
-
HOOKS_DIR =
|
|
4139
|
+
HOOKS_DIR = existsSync2(join2(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join2(__dirname2, "..", "..", "hooks") : join2(__dirname2, "..", "hooks");
|
|
4140
4140
|
EVENT_MAP = {
|
|
4141
4141
|
claude: {
|
|
4142
4142
|
PreToolUse: "PreToolUse",
|
|
@@ -4155,25 +4155,25 @@ var init_installer = __esm(() => {
|
|
|
4155
4155
|
});
|
|
4156
4156
|
|
|
4157
4157
|
// src/lib/profiles.ts
|
|
4158
|
-
import { existsSync as
|
|
4159
|
-
import { join as
|
|
4160
|
-
import { homedir as
|
|
4158
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync, rmSync, cpSync } from "fs";
|
|
4159
|
+
import { join as join3 } from "path";
|
|
4160
|
+
import { homedir as homedir3 } from "os";
|
|
4161
4161
|
function resolveProfilesDir() {
|
|
4162
|
-
const newDir =
|
|
4163
|
-
const oldDir =
|
|
4164
|
-
if (!
|
|
4165
|
-
mkdirSync2(
|
|
4162
|
+
const newDir = join3(homedir3(), ".hasna", "hooks", "profiles");
|
|
4163
|
+
const oldDir = join3(homedir3(), ".hooks", "profiles");
|
|
4164
|
+
if (!existsSync3(newDir) && existsSync3(oldDir)) {
|
|
4165
|
+
mkdirSync2(join3(homedir3(), ".hasna", "hooks"), { recursive: true });
|
|
4166
4166
|
cpSync(oldDir, newDir, { recursive: true });
|
|
4167
4167
|
}
|
|
4168
4168
|
return newDir;
|
|
4169
4169
|
}
|
|
4170
4170
|
function ensureProfilesDir() {
|
|
4171
|
-
if (!
|
|
4171
|
+
if (!existsSync3(PROFILES_DIR)) {
|
|
4172
4172
|
mkdirSync2(PROFILES_DIR, { recursive: true });
|
|
4173
4173
|
}
|
|
4174
4174
|
}
|
|
4175
4175
|
function profilePath(id) {
|
|
4176
|
-
return
|
|
4176
|
+
return join3(PROFILES_DIR, `${id}.json`);
|
|
4177
4177
|
}
|
|
4178
4178
|
function shortUuid() {
|
|
4179
4179
|
return crypto.randomUUID().slice(0, 8);
|
|
@@ -4181,12 +4181,12 @@ function shortUuid() {
|
|
|
4181
4181
|
function createProfile(input) {
|
|
4182
4182
|
ensureProfilesDir();
|
|
4183
4183
|
const id = shortUuid();
|
|
4184
|
-
const
|
|
4184
|
+
const now2 = new Date().toISOString();
|
|
4185
4185
|
const profile = {
|
|
4186
4186
|
agent_id: id,
|
|
4187
4187
|
agent_type: input.agent_type,
|
|
4188
|
-
created_at:
|
|
4189
|
-
last_seen_at:
|
|
4188
|
+
created_at: now2,
|
|
4189
|
+
last_seen_at: now2,
|
|
4190
4190
|
preferences: {}
|
|
4191
4191
|
};
|
|
4192
4192
|
if (input.name) {
|
|
@@ -4199,7 +4199,7 @@ function createProfile(input) {
|
|
|
4199
4199
|
function getProfile(id) {
|
|
4200
4200
|
const path = profilePath(id);
|
|
4201
4201
|
try {
|
|
4202
|
-
if (!
|
|
4202
|
+
if (!existsSync3(path))
|
|
4203
4203
|
return null;
|
|
4204
4204
|
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
4205
4205
|
} catch {
|
|
@@ -4207,14 +4207,14 @@ function getProfile(id) {
|
|
|
4207
4207
|
}
|
|
4208
4208
|
}
|
|
4209
4209
|
function listProfiles() {
|
|
4210
|
-
if (!
|
|
4210
|
+
if (!existsSync3(PROFILES_DIR))
|
|
4211
4211
|
return [];
|
|
4212
4212
|
try {
|
|
4213
4213
|
const files = readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".json"));
|
|
4214
4214
|
const profiles = [];
|
|
4215
4215
|
for (const file of files) {
|
|
4216
4216
|
try {
|
|
4217
|
-
const content = readFileSync2(
|
|
4217
|
+
const content = readFileSync2(join3(PROFILES_DIR, file), "utf-8");
|
|
4218
4218
|
profiles.push(JSON.parse(content));
|
|
4219
4219
|
} catch {}
|
|
4220
4220
|
}
|
|
@@ -4244,7 +4244,7 @@ function importProfiles(profiles) {
|
|
|
4244
4244
|
continue;
|
|
4245
4245
|
}
|
|
4246
4246
|
const path = profilePath(profile.agent_id);
|
|
4247
|
-
if (
|
|
4247
|
+
if (existsSync3(path)) {
|
|
4248
4248
|
skipped++;
|
|
4249
4249
|
continue;
|
|
4250
4250
|
}
|
|
@@ -4263,23 +4263,23 @@ var init_profiles = __esm(() => {
|
|
|
4263
4263
|
import { createRequire } from "module";
|
|
4264
4264
|
import { Database } from "bun:sqlite";
|
|
4265
4265
|
import {
|
|
4266
|
-
existsSync as
|
|
4266
|
+
existsSync as existsSync4,
|
|
4267
4267
|
mkdirSync as mkdirSync3,
|
|
4268
4268
|
readdirSync as readdirSync2,
|
|
4269
4269
|
copyFileSync
|
|
4270
4270
|
} from "fs";
|
|
4271
|
-
import { homedir as
|
|
4272
|
-
import { join as
|
|
4271
|
+
import { homedir as homedir4 } from "os";
|
|
4272
|
+
import { join as join4, relative } from "path";
|
|
4273
4273
|
import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
4274
4274
|
import { homedir as homedir22 } from "os";
|
|
4275
4275
|
import { join as join22 } from "path";
|
|
4276
|
-
import { readdirSync as
|
|
4277
|
-
import { join as
|
|
4276
|
+
import { readdirSync as readdirSync3, existsSync as existsSync6 } from "fs";
|
|
4277
|
+
import { join as join6 } from "path";
|
|
4278
|
+
import { homedir as homedir5 } from "os";
|
|
4278
4279
|
import { homedir as homedir32 } from "os";
|
|
4279
|
-
import {
|
|
4280
|
-
import { join as
|
|
4281
|
-
import {
|
|
4282
|
-
import { homedir as homedir5, platform } from "os";
|
|
4280
|
+
import { join as join32 } from "path";
|
|
4281
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
4282
|
+
import { homedir as homedir42, platform } from "os";
|
|
4283
4283
|
function __accessProp2(key) {
|
|
4284
4284
|
return this[key];
|
|
4285
4285
|
}
|
|
@@ -5093,13 +5093,13 @@ function custom(check, _params = {}, fatal) {
|
|
|
5093
5093
|
return ZodAny.create();
|
|
5094
5094
|
}
|
|
5095
5095
|
function getDataDir(serviceName) {
|
|
5096
|
-
const dir =
|
|
5096
|
+
const dir = join4(HASNA_DIR, serviceName);
|
|
5097
5097
|
mkdirSync3(dir, { recursive: true });
|
|
5098
5098
|
return dir;
|
|
5099
5099
|
}
|
|
5100
5100
|
function getDbPath(serviceName) {
|
|
5101
5101
|
const dir = getDataDir(serviceName);
|
|
5102
|
-
return
|
|
5102
|
+
return join4(dir, `${serviceName}.db`);
|
|
5103
5103
|
}
|
|
5104
5104
|
function getConfigDir() {
|
|
5105
5105
|
return CONFIG_DIR;
|
|
@@ -5150,11 +5150,11 @@ function isSyncExcludedTable(table) {
|
|
|
5150
5150
|
return SYNC_EXCLUDED_TABLE_PATTERNS.some((p) => p.test(table));
|
|
5151
5151
|
}
|
|
5152
5152
|
function discoverServices() {
|
|
5153
|
-
const dataDir =
|
|
5154
|
-
if (!
|
|
5153
|
+
const dataDir = join6(homedir5(), ".hasna");
|
|
5154
|
+
if (!existsSync6(dataDir))
|
|
5155
5155
|
return [];
|
|
5156
5156
|
try {
|
|
5157
|
-
const entries =
|
|
5157
|
+
const entries = readdirSync3(dataDir, { withFileTypes: true });
|
|
5158
5158
|
return entries.filter((e) => {
|
|
5159
5159
|
if (!e.isDirectory())
|
|
5160
5160
|
return false;
|
|
@@ -5166,30 +5166,30 @@ function discoverServices() {
|
|
|
5166
5166
|
return [];
|
|
5167
5167
|
}
|
|
5168
5168
|
}
|
|
5169
|
-
function
|
|
5169
|
+
function discoverSyncableServices2() {
|
|
5170
5170
|
const local = discoverServices();
|
|
5171
5171
|
const pgSet = new Set(KNOWN_PG_SERVICES);
|
|
5172
5172
|
return local.filter((s) => pgSet.has(s));
|
|
5173
5173
|
}
|
|
5174
5174
|
function getServiceDbPath(service) {
|
|
5175
|
-
const dataDir =
|
|
5176
|
-
if (!
|
|
5175
|
+
const dataDir = join6(homedir5(), ".hasna", service);
|
|
5176
|
+
if (!existsSync6(dataDir))
|
|
5177
5177
|
return null;
|
|
5178
5178
|
const candidates = [
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5179
|
+
join6(dataDir, `${service}.db`),
|
|
5180
|
+
join6(dataDir, "data.db"),
|
|
5181
|
+
join6(dataDir, "database.db")
|
|
5182
5182
|
];
|
|
5183
5183
|
try {
|
|
5184
|
-
const files =
|
|
5184
|
+
const files = readdirSync3(dataDir);
|
|
5185
5185
|
for (const f of files) {
|
|
5186
5186
|
if (f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm")) {
|
|
5187
|
-
candidates.push(
|
|
5187
|
+
candidates.push(join6(dataDir, f));
|
|
5188
5188
|
}
|
|
5189
5189
|
}
|
|
5190
5190
|
} catch {}
|
|
5191
5191
|
for (const p of candidates) {
|
|
5192
|
-
if (
|
|
5192
|
+
if (existsSync6(p))
|
|
5193
5193
|
return p;
|
|
5194
5194
|
}
|
|
5195
5195
|
return null;
|
|
@@ -5218,8 +5218,8 @@ class SyncProgressTracker {
|
|
|
5218
5218
|
}
|
|
5219
5219
|
start(table, total, direction) {
|
|
5220
5220
|
const resumed = this.canResume(table);
|
|
5221
|
-
const
|
|
5222
|
-
this.startTimes.set(table,
|
|
5221
|
+
const now2 = Date.now();
|
|
5222
|
+
this.startTimes.set(table, now2);
|
|
5223
5223
|
const status = resumed ? "resumed" : "in_progress";
|
|
5224
5224
|
const info = {
|
|
5225
5225
|
table,
|
|
@@ -13528,7 +13528,7 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
13528
13528
|
init_external();
|
|
13529
13529
|
});
|
|
13530
13530
|
init_dotfile = __esm2(() => {
|
|
13531
|
-
HASNA_DIR =
|
|
13531
|
+
HASNA_DIR = join4(homedir4(), ".hasna");
|
|
13532
13532
|
});
|
|
13533
13533
|
exports_config = {};
|
|
13534
13534
|
__export2(exports_config, {
|
|
@@ -13566,7 +13566,7 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
13566
13566
|
__export2(exports_discover, {
|
|
13567
13567
|
isSyncExcludedTable: () => isSyncExcludedTable,
|
|
13568
13568
|
getServiceDbPath: () => getServiceDbPath,
|
|
13569
|
-
discoverSyncableServices: () =>
|
|
13569
|
+
discoverSyncableServices: () => discoverSyncableServices2,
|
|
13570
13570
|
discoverServices: () => discoverServices,
|
|
13571
13571
|
SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
|
|
13572
13572
|
KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES
|
|
@@ -13621,15 +13621,13 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
13621
13621
|
init_config();
|
|
13622
13622
|
init_config();
|
|
13623
13623
|
init_dotfile();
|
|
13624
|
-
init_adapter();
|
|
13625
13624
|
init_config();
|
|
13626
|
-
|
|
13627
|
-
AUTO_SYNC_CONFIG_PATH = join4(homedir4(), ".hasna", "cloud", "config.json");
|
|
13625
|
+
AUTO_SYNC_CONFIG_PATH = join32(homedir32(), ".hasna", "cloud", "config.json");
|
|
13628
13626
|
init_config();
|
|
13629
13627
|
init_adapter();
|
|
13630
13628
|
init_dotfile();
|
|
13631
13629
|
init_config();
|
|
13632
|
-
CONFIG_DIR2 =
|
|
13630
|
+
CONFIG_DIR2 = join5(homedir42(), ".hasna", "cloud");
|
|
13633
13631
|
init_adapter();
|
|
13634
13632
|
init_config();
|
|
13635
13633
|
init_discover();
|
|
@@ -13713,8 +13711,8 @@ var init_migrations = __esm(() => {
|
|
|
13713
13711
|
});
|
|
13714
13712
|
|
|
13715
13713
|
// src/db/legacy-import.ts
|
|
13716
|
-
import { existsSync as
|
|
13717
|
-
import { join as
|
|
13714
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4 } from "fs";
|
|
13715
|
+
import { join as join7 } from "path";
|
|
13718
13716
|
import { homedir as homedir6 } from "os";
|
|
13719
13717
|
function ensureMetaTable(db) {
|
|
13720
13718
|
db.exec(`
|
|
@@ -13794,20 +13792,20 @@ function runLegacyImport(db) {
|
|
|
13794
13792
|
if (isAlreadyDone(db))
|
|
13795
13793
|
return;
|
|
13796
13794
|
let total = 0;
|
|
13797
|
-
const claudeProjectsDir =
|
|
13798
|
-
if (
|
|
13795
|
+
const claudeProjectsDir = join7(homedir6(), ".claude", "projects");
|
|
13796
|
+
if (existsSync5(claudeProjectsDir)) {
|
|
13799
13797
|
try {
|
|
13800
|
-
const projectDirs =
|
|
13798
|
+
const projectDirs = readdirSync4(claudeProjectsDir);
|
|
13801
13799
|
for (const dir of projectDirs) {
|
|
13802
|
-
const projectDir =
|
|
13800
|
+
const projectDir = join7(claudeProjectsDir, dir);
|
|
13803
13801
|
try {
|
|
13804
|
-
const files =
|
|
13802
|
+
const files = readdirSync4(projectDir);
|
|
13805
13803
|
for (const file of files) {
|
|
13806
13804
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
13807
|
-
total += importJsonlFile(db,
|
|
13805
|
+
total += importJsonlFile(db, join7(projectDir, file));
|
|
13808
13806
|
}
|
|
13809
13807
|
if (file === "errors.log") {
|
|
13810
|
-
total += importErrorsLog(db,
|
|
13808
|
+
total += importErrorsLog(db, join7(projectDir, file));
|
|
13811
13809
|
}
|
|
13812
13810
|
}
|
|
13813
13811
|
} catch {}
|
|
@@ -13849,17 +13847,17 @@ __export(exports_db, {
|
|
|
13849
13847
|
createTestDb: () => createTestDb,
|
|
13850
13848
|
closeDb: () => closeDb
|
|
13851
13849
|
});
|
|
13852
|
-
import { existsSync as
|
|
13853
|
-
import { join as
|
|
13850
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, cpSync as cpSync2 } from "fs";
|
|
13851
|
+
import { join as join9 } from "path";
|
|
13854
13852
|
import { homedir as homedir8 } from "os";
|
|
13855
13853
|
function resolveDataDir() {
|
|
13856
13854
|
const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
|
|
13857
13855
|
if (explicit)
|
|
13858
13856
|
return explicit;
|
|
13859
|
-
const newDir =
|
|
13860
|
-
const oldDir =
|
|
13861
|
-
if (!
|
|
13862
|
-
mkdirSync4(
|
|
13857
|
+
const newDir = join9(homedir8(), ".hasna", "hooks");
|
|
13858
|
+
const oldDir = join9(homedir8(), ".hooks");
|
|
13859
|
+
if (!existsSync7(newDir) && existsSync7(oldDir)) {
|
|
13860
|
+
mkdirSync4(join9(homedir8(), ".hasna"), { recursive: true });
|
|
13863
13861
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
13864
13862
|
}
|
|
13865
13863
|
return newDir;
|
|
@@ -13869,11 +13867,11 @@ function getDbPath2() {
|
|
|
13869
13867
|
if (explicitDb)
|
|
13870
13868
|
return explicitDb;
|
|
13871
13869
|
const dataDir = resolveDataDir();
|
|
13872
|
-
return
|
|
13870
|
+
return join9(dataDir, "hooks.db");
|
|
13873
13871
|
}
|
|
13874
13872
|
function ensureDir(dbPath) {
|
|
13875
13873
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
13876
|
-
if (dir && !
|
|
13874
|
+
if (dir && !existsSync7(dir)) {
|
|
13877
13875
|
mkdirSync4(dir, { recursive: true });
|
|
13878
13876
|
}
|
|
13879
13877
|
}
|
|
@@ -13881,7 +13879,7 @@ function getDb() {
|
|
|
13881
13879
|
if (instance)
|
|
13882
13880
|
return instance;
|
|
13883
13881
|
const dbPath = getDbPath2();
|
|
13884
|
-
const isNew = dbPath === ":memory:" || !
|
|
13882
|
+
const isNew = dbPath === ":memory:" || !existsSync7(dbPath);
|
|
13885
13883
|
ensureDir(dbPath);
|
|
13886
13884
|
instance = new SqliteAdapter(dbPath);
|
|
13887
13885
|
instance.exec("PRAGMA journal_mode=WAL");
|
|
@@ -13934,8 +13932,8 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
13934
13932
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13935
13933
|
import { z } from "zod";
|
|
13936
13934
|
import { createServer } from "http";
|
|
13937
|
-
import { existsSync as
|
|
13938
|
-
import { join as
|
|
13935
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
13936
|
+
import { join as join10, dirname as dirname3 } from "path";
|
|
13939
13937
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
13940
13938
|
function formatInstallResults(results, extra) {
|
|
13941
13939
|
const installed = results.filter((r) => r.success).map((r) => r.hook);
|
|
@@ -14032,7 +14030,7 @@ function createHooksServer() {
|
|
|
14032
14030
|
const settingsPath = getSettingsPath(scope);
|
|
14033
14031
|
const issues = [];
|
|
14034
14032
|
const healthy = [];
|
|
14035
|
-
const settingsExist =
|
|
14033
|
+
const settingsExist = existsSync9(settingsPath);
|
|
14036
14034
|
if (!settingsExist) {
|
|
14037
14035
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
14038
14036
|
}
|
|
@@ -14045,7 +14043,7 @@ function createHooksServer() {
|
|
|
14045
14043
|
continue;
|
|
14046
14044
|
}
|
|
14047
14045
|
const hookDir = getHookPath(name);
|
|
14048
|
-
if (!
|
|
14046
|
+
if (!existsSync9(join10(hookDir, "src", "hook.ts"))) {
|
|
14049
14047
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
14050
14048
|
hookHealthy = false;
|
|
14051
14049
|
}
|
|
@@ -14082,9 +14080,9 @@ function createHooksServer() {
|
|
|
14082
14080
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
14083
14081
|
}
|
|
14084
14082
|
const hookPath = getHookPath(name);
|
|
14085
|
-
const readmePath =
|
|
14083
|
+
const readmePath = join10(hookPath, "README.md");
|
|
14086
14084
|
let readme = "";
|
|
14087
|
-
if (
|
|
14085
|
+
if (existsSync9(readmePath)) {
|
|
14088
14086
|
readme = readFileSync5(readmePath, "utf-8");
|
|
14089
14087
|
}
|
|
14090
14088
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
@@ -14134,8 +14132,8 @@ function createHooksServer() {
|
|
|
14134
14132
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
14135
14133
|
}
|
|
14136
14134
|
const hookDir = getHookPath(name);
|
|
14137
|
-
const hookScript =
|
|
14138
|
-
if (!
|
|
14135
|
+
const hookScript = join10(hookDir, "src", "hook.ts");
|
|
14136
|
+
if (!existsSync9(hookScript)) {
|
|
14139
14137
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
|
|
14140
14138
|
}
|
|
14141
14139
|
let hookInput = { ...input };
|
|
@@ -14226,7 +14224,7 @@ function createHooksServer() {
|
|
|
14226
14224
|
const ctx = {
|
|
14227
14225
|
scope,
|
|
14228
14226
|
settings_path: settingsPath,
|
|
14229
|
-
settings_exists:
|
|
14227
|
+
settings_exists: existsSync9(settingsPath),
|
|
14230
14228
|
registered_hooks: hooks,
|
|
14231
14229
|
hook_count: hooks.length,
|
|
14232
14230
|
healthy,
|
|
@@ -14264,8 +14262,8 @@ function createHooksServer() {
|
|
|
14264
14262
|
const input = { tool_name, tool_input };
|
|
14265
14263
|
const results = await Promise.all(matchingHooks.map(async (name) => {
|
|
14266
14264
|
const hookDir = getHookPath(name);
|
|
14267
|
-
const hookScript =
|
|
14268
|
-
if (!
|
|
14265
|
+
const hookScript = join10(hookDir, "src", "hook.ts");
|
|
14266
|
+
if (!existsSync9(hookScript))
|
|
14269
14267
|
return { name, decision: "approve", error: "script not found" };
|
|
14270
14268
|
const proc = Bun.spawn(["bun", "run", hookScript], {
|
|
14271
14269
|
stdin: new Response(JSON.stringify(input)),
|
|
@@ -14333,8 +14331,8 @@ function createHooksServer() {
|
|
|
14333
14331
|
const meta = getHook(name);
|
|
14334
14332
|
if (!meta)
|
|
14335
14333
|
return { name, error: `Hook '${name}' not found` };
|
|
14336
|
-
const hookScript =
|
|
14337
|
-
if (!
|
|
14334
|
+
const hookScript = join10(getHookPath(name), "src", "hook.ts");
|
|
14335
|
+
if (!existsSync9(hookScript))
|
|
14338
14336
|
return { name, error: "script not found" };
|
|
14339
14337
|
const proc = Bun.spawn(["bun", "run", hookScript], {
|
|
14340
14338
|
stdin: new Response(JSON.stringify(input)),
|
|
@@ -14367,7 +14365,7 @@ function createHooksServer() {
|
|
|
14367
14365
|
const settingsPath = getSettingsPath(scope);
|
|
14368
14366
|
let settings = {};
|
|
14369
14367
|
try {
|
|
14370
|
-
if (
|
|
14368
|
+
if (existsSync9(settingsPath))
|
|
14371
14369
|
settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
|
|
14372
14370
|
} catch {}
|
|
14373
14371
|
if (!settings.hooks)
|
|
@@ -14390,7 +14388,7 @@ function createHooksServer() {
|
|
|
14390
14388
|
const settingsPath = getSettingsPath(scope);
|
|
14391
14389
|
let settings = {};
|
|
14392
14390
|
try {
|
|
14393
|
-
if (
|
|
14391
|
+
if (existsSync9(settingsPath))
|
|
14394
14392
|
settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
|
|
14395
14393
|
} catch {}
|
|
14396
14394
|
if (settings.hooks?.__disabled) {
|
|
@@ -14623,8 +14621,8 @@ var init_server = __esm(() => {
|
|
|
14623
14621
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
14624
14622
|
try {
|
|
14625
14623
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
14626
|
-
const p =
|
|
14627
|
-
if (
|
|
14624
|
+
const p = join10(__dirname3, rel);
|
|
14625
|
+
if (existsSync9(p)) {
|
|
14628
14626
|
pkg = JSON.parse(readFileSync5(p, "utf-8"));
|
|
14629
14627
|
break;
|
|
14630
14628
|
}
|
|
@@ -14692,6 +14690,690 @@ function startMcpHttpServer(options) {
|
|
|
14692
14690
|
var DEFAULT_MCP_HTTP_PORT = 8847, MCP_HTTP_HOST = "127.0.0.1", MCP_SERVICE_NAME = "hooks";
|
|
14693
14691
|
var init_http = () => {};
|
|
14694
14692
|
|
|
14693
|
+
// node_modules/@hasna/events/dist/commander.js
|
|
14694
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
14695
|
+
import { existsSync } from "fs";
|
|
14696
|
+
import { homedir } from "os";
|
|
14697
|
+
import { join } from "path";
|
|
14698
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
14699
|
+
import { randomUUID } from "crypto";
|
|
14700
|
+
import { spawn } from "child_process";
|
|
14701
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
14702
|
+
function getPathValue(input, path) {
|
|
14703
|
+
return path.split(".").reduce((value, part) => {
|
|
14704
|
+
if (value && typeof value === "object" && part in value) {
|
|
14705
|
+
return value[part];
|
|
14706
|
+
}
|
|
14707
|
+
return;
|
|
14708
|
+
}, input);
|
|
14709
|
+
}
|
|
14710
|
+
function wildcardToRegExp(pattern) {
|
|
14711
|
+
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
14712
|
+
return new RegExp(`^${escaped}$`);
|
|
14713
|
+
}
|
|
14714
|
+
function matchString(value, matcher) {
|
|
14715
|
+
if (matcher === undefined)
|
|
14716
|
+
return true;
|
|
14717
|
+
if (value === undefined)
|
|
14718
|
+
return false;
|
|
14719
|
+
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
14720
|
+
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
14721
|
+
}
|
|
14722
|
+
function matchRecord(input, matcher) {
|
|
14723
|
+
if (!matcher)
|
|
14724
|
+
return true;
|
|
14725
|
+
return Object.entries(matcher).every(([path, expected]) => {
|
|
14726
|
+
const actual = getPathValue(input, path);
|
|
14727
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
14728
|
+
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
14729
|
+
}
|
|
14730
|
+
return actual === expected;
|
|
14731
|
+
});
|
|
14732
|
+
}
|
|
14733
|
+
function eventMatchesFilter(event, filter) {
|
|
14734
|
+
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);
|
|
14735
|
+
}
|
|
14736
|
+
function channelMatchesEvent(channel, event) {
|
|
14737
|
+
if (!channel.enabled)
|
|
14738
|
+
return false;
|
|
14739
|
+
if (!channel.filters || channel.filters.length === 0)
|
|
14740
|
+
return true;
|
|
14741
|
+
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
14742
|
+
}
|
|
14743
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
14744
|
+
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
14745
|
+
function getEventsDataDir(override) {
|
|
14746
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
14747
|
+
}
|
|
14748
|
+
|
|
14749
|
+
class JsonEventsStore {
|
|
14750
|
+
dataDir;
|
|
14751
|
+
channelsPath;
|
|
14752
|
+
eventsPath;
|
|
14753
|
+
deliveriesPath;
|
|
14754
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
14755
|
+
this.dataDir = dataDir;
|
|
14756
|
+
this.channelsPath = join(dataDir, "channels.json");
|
|
14757
|
+
this.eventsPath = join(dataDir, "events.json");
|
|
14758
|
+
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
14759
|
+
}
|
|
14760
|
+
async init() {
|
|
14761
|
+
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
14762
|
+
await chmod(this.dataDir, 448).catch(() => {
|
|
14763
|
+
return;
|
|
14764
|
+
});
|
|
14765
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
14766
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
14767
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
14768
|
+
}
|
|
14769
|
+
async addChannel(channel) {
|
|
14770
|
+
await this.init();
|
|
14771
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
14772
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
14773
|
+
if (index >= 0) {
|
|
14774
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
14775
|
+
} else {
|
|
14776
|
+
channels.push(channel);
|
|
14777
|
+
}
|
|
14778
|
+
await this.writeJson(this.channelsPath, channels);
|
|
14779
|
+
return index >= 0 ? channels[index] : channel;
|
|
14780
|
+
}
|
|
14781
|
+
async listChannels() {
|
|
14782
|
+
await this.init();
|
|
14783
|
+
return this.readJson(this.channelsPath, []);
|
|
14784
|
+
}
|
|
14785
|
+
async getChannel(id) {
|
|
14786
|
+
const channels = await this.listChannels();
|
|
14787
|
+
return channels.find((channel) => channel.id === id);
|
|
14788
|
+
}
|
|
14789
|
+
async removeChannel(id) {
|
|
14790
|
+
await this.init();
|
|
14791
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
14792
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
14793
|
+
await this.writeJson(this.channelsPath, next);
|
|
14794
|
+
return next.length !== channels.length;
|
|
14795
|
+
}
|
|
14796
|
+
async appendEvent(event) {
|
|
14797
|
+
await this.init();
|
|
14798
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
14799
|
+
events.push(event);
|
|
14800
|
+
await this.writeJson(this.eventsPath, events);
|
|
14801
|
+
return event;
|
|
14802
|
+
}
|
|
14803
|
+
async listEvents() {
|
|
14804
|
+
await this.init();
|
|
14805
|
+
return this.readJson(this.eventsPath, []);
|
|
14806
|
+
}
|
|
14807
|
+
async findEventByIdentity(identity) {
|
|
14808
|
+
const events = await this.listEvents();
|
|
14809
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
14810
|
+
}
|
|
14811
|
+
async appendDelivery(result) {
|
|
14812
|
+
await this.init();
|
|
14813
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
14814
|
+
deliveries.push(result);
|
|
14815
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
14816
|
+
return result;
|
|
14817
|
+
}
|
|
14818
|
+
async listDeliveries() {
|
|
14819
|
+
await this.init();
|
|
14820
|
+
return this.readJson(this.deliveriesPath, []);
|
|
14821
|
+
}
|
|
14822
|
+
async exportData() {
|
|
14823
|
+
return {
|
|
14824
|
+
channels: await this.listChannels(),
|
|
14825
|
+
events: await this.listEvents(),
|
|
14826
|
+
deliveries: await this.listDeliveries()
|
|
14827
|
+
};
|
|
14828
|
+
}
|
|
14829
|
+
async ensureArrayFile(path) {
|
|
14830
|
+
if (!existsSync(path)) {
|
|
14831
|
+
await writeFile(path, `[]
|
|
14832
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
14833
|
+
}
|
|
14834
|
+
await chmod(path, 384).catch(() => {
|
|
14835
|
+
return;
|
|
14836
|
+
});
|
|
14837
|
+
}
|
|
14838
|
+
async readJson(path, fallback) {
|
|
14839
|
+
try {
|
|
14840
|
+
const raw = await readFile(path, "utf-8");
|
|
14841
|
+
if (!raw.trim())
|
|
14842
|
+
return fallback;
|
|
14843
|
+
return JSON.parse(raw);
|
|
14844
|
+
} catch (error) {
|
|
14845
|
+
if (error.code === "ENOENT")
|
|
14846
|
+
return fallback;
|
|
14847
|
+
throw error;
|
|
14848
|
+
}
|
|
14849
|
+
}
|
|
14850
|
+
async writeJson(path, value) {
|
|
14851
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
14852
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
14853
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
14854
|
+
await rename(tempPath, path);
|
|
14855
|
+
await chmod(path, 384).catch(() => {
|
|
14856
|
+
return;
|
|
14857
|
+
});
|
|
14858
|
+
}
|
|
14859
|
+
}
|
|
14860
|
+
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
14861
|
+
function buildSignatureBase(timestamp, body) {
|
|
14862
|
+
return `${timestamp}.${body}`;
|
|
14863
|
+
}
|
|
14864
|
+
function signPayload(secret, timestamp, body) {
|
|
14865
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
14866
|
+
return `sha256=${digest}`;
|
|
14867
|
+
}
|
|
14868
|
+
function now() {
|
|
14869
|
+
return new Date().toISOString();
|
|
14870
|
+
}
|
|
14871
|
+
function truncate(value, max = 4096) {
|
|
14872
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
14873
|
+
}
|
|
14874
|
+
function buildWebhookRequest(event, channel) {
|
|
14875
|
+
if (!channel.webhook)
|
|
14876
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
14877
|
+
const body = JSON.stringify(event);
|
|
14878
|
+
const timestamp = event.time;
|
|
14879
|
+
const headers = {
|
|
14880
|
+
"Content-Type": "application/json",
|
|
14881
|
+
"User-Agent": "@hasna/events",
|
|
14882
|
+
"X-Hasna-Event-Id": event.id,
|
|
14883
|
+
"X-Hasna-Event-Type": event.type,
|
|
14884
|
+
"X-Hasna-Timestamp": timestamp,
|
|
14885
|
+
...channel.webhook.headers
|
|
14886
|
+
};
|
|
14887
|
+
if (channel.webhook.secret) {
|
|
14888
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
14889
|
+
}
|
|
14890
|
+
return { body, headers };
|
|
14891
|
+
}
|
|
14892
|
+
async function dispatchWebhook(event, channel, options = {}) {
|
|
14893
|
+
if (!channel.webhook)
|
|
14894
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
14895
|
+
const startedAt = now();
|
|
14896
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
14897
|
+
const controller = new AbortController;
|
|
14898
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
14899
|
+
try {
|
|
14900
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
14901
|
+
method: "POST",
|
|
14902
|
+
headers,
|
|
14903
|
+
body,
|
|
14904
|
+
signal: controller.signal
|
|
14905
|
+
});
|
|
14906
|
+
const responseBody = truncate(await response.text());
|
|
14907
|
+
return {
|
|
14908
|
+
attempt: 1,
|
|
14909
|
+
status: response.ok ? "success" : "failed",
|
|
14910
|
+
startedAt,
|
|
14911
|
+
completedAt: now(),
|
|
14912
|
+
responseStatus: response.status,
|
|
14913
|
+
responseBody,
|
|
14914
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
14915
|
+
};
|
|
14916
|
+
} catch (error) {
|
|
14917
|
+
return {
|
|
14918
|
+
attempt: 1,
|
|
14919
|
+
status: "failed",
|
|
14920
|
+
startedAt,
|
|
14921
|
+
completedAt: now(),
|
|
14922
|
+
error: error instanceof Error ? error.message : String(error)
|
|
14923
|
+
};
|
|
14924
|
+
} finally {
|
|
14925
|
+
clearTimeout(timeout);
|
|
14926
|
+
}
|
|
14927
|
+
}
|
|
14928
|
+
async function dispatchCommand(event, channel) {
|
|
14929
|
+
if (!channel.command)
|
|
14930
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
14931
|
+
const startedAt = now();
|
|
14932
|
+
const eventJson = JSON.stringify(event);
|
|
14933
|
+
const env = {
|
|
14934
|
+
...process.env,
|
|
14935
|
+
...channel.command.env,
|
|
14936
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
14937
|
+
HASNA_EVENT_ID: event.id,
|
|
14938
|
+
HASNA_EVENT_TYPE: event.type,
|
|
14939
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
14940
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
14941
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
14942
|
+
HASNA_EVENT_TIME: event.time,
|
|
14943
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
14944
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
14945
|
+
HASNA_EVENT_JSON: eventJson
|
|
14946
|
+
};
|
|
14947
|
+
return new Promise((resolve) => {
|
|
14948
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
14949
|
+
cwd: channel.command.cwd,
|
|
14950
|
+
env,
|
|
14951
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
14952
|
+
});
|
|
14953
|
+
let stdout = "";
|
|
14954
|
+
let stderr = "";
|
|
14955
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
14956
|
+
child.stdin.end(eventJson);
|
|
14957
|
+
child.stdout.on("data", (chunk) => {
|
|
14958
|
+
stdout += chunk.toString();
|
|
14959
|
+
});
|
|
14960
|
+
child.stderr.on("data", (chunk) => {
|
|
14961
|
+
stderr += chunk.toString();
|
|
14962
|
+
});
|
|
14963
|
+
child.on("error", (error) => {
|
|
14964
|
+
clearTimeout(timeout);
|
|
14965
|
+
resolve({
|
|
14966
|
+
attempt: 1,
|
|
14967
|
+
status: "failed",
|
|
14968
|
+
startedAt,
|
|
14969
|
+
completedAt: now(),
|
|
14970
|
+
stdout: truncate(stdout),
|
|
14971
|
+
stderr: truncate(stderr),
|
|
14972
|
+
error: error.message
|
|
14973
|
+
});
|
|
14974
|
+
});
|
|
14975
|
+
child.on("close", (code, signal) => {
|
|
14976
|
+
clearTimeout(timeout);
|
|
14977
|
+
const success = code === 0;
|
|
14978
|
+
resolve({
|
|
14979
|
+
attempt: 1,
|
|
14980
|
+
status: success ? "success" : "failed",
|
|
14981
|
+
startedAt,
|
|
14982
|
+
completedAt: now(),
|
|
14983
|
+
stdout: truncate(stdout),
|
|
14984
|
+
stderr: truncate(stderr),
|
|
14985
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
14986
|
+
});
|
|
14987
|
+
});
|
|
14988
|
+
});
|
|
14989
|
+
}
|
|
14990
|
+
async function dispatchChannel(event, channel, options = {}) {
|
|
14991
|
+
if (channel.transport === "webhook")
|
|
14992
|
+
return dispatchWebhook(event, channel, options);
|
|
14993
|
+
if (channel.transport === "command")
|
|
14994
|
+
return dispatchCommand(event, channel);
|
|
14995
|
+
return {
|
|
14996
|
+
attempt: 1,
|
|
14997
|
+
status: "skipped",
|
|
14998
|
+
startedAt: now(),
|
|
14999
|
+
completedAt: now(),
|
|
15000
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
15001
|
+
};
|
|
15002
|
+
}
|
|
15003
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
15004
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
15005
|
+
return {
|
|
15006
|
+
id: randomUUID(),
|
|
15007
|
+
eventId: event.id,
|
|
15008
|
+
channelId: channel.id,
|
|
15009
|
+
transport: channel.transport,
|
|
15010
|
+
status,
|
|
15011
|
+
attempts,
|
|
15012
|
+
createdAt: attempts[0]?.startedAt ?? now(),
|
|
15013
|
+
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
15014
|
+
};
|
|
15015
|
+
}
|
|
15016
|
+
function createEvent(input) {
|
|
15017
|
+
return {
|
|
15018
|
+
id: input.id ?? randomUUID2(),
|
|
15019
|
+
source: input.source,
|
|
15020
|
+
type: input.type,
|
|
15021
|
+
time: normalizeTime(input.time),
|
|
15022
|
+
subject: input.subject,
|
|
15023
|
+
severity: input.severity ?? "info",
|
|
15024
|
+
data: input.data ?? {},
|
|
15025
|
+
message: input.message,
|
|
15026
|
+
dedupeKey: input.dedupeKey,
|
|
15027
|
+
schemaVersion: input.schemaVersion ?? "1.0",
|
|
15028
|
+
metadata: input.metadata ?? {}
|
|
15029
|
+
};
|
|
15030
|
+
}
|
|
15031
|
+
|
|
15032
|
+
class EventsClient {
|
|
15033
|
+
store;
|
|
15034
|
+
redactors;
|
|
15035
|
+
transportOptions;
|
|
15036
|
+
constructor(options = {}) {
|
|
15037
|
+
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
15038
|
+
this.redactors = options.redactors ?? [];
|
|
15039
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
15040
|
+
}
|
|
15041
|
+
async addChannel(input) {
|
|
15042
|
+
const timestamp = new Date().toISOString();
|
|
15043
|
+
return this.store.addChannel({
|
|
15044
|
+
...input,
|
|
15045
|
+
createdAt: input.createdAt ?? timestamp,
|
|
15046
|
+
updatedAt: input.updatedAt ?? timestamp
|
|
15047
|
+
});
|
|
15048
|
+
}
|
|
15049
|
+
async listChannels() {
|
|
15050
|
+
return this.store.listChannels();
|
|
15051
|
+
}
|
|
15052
|
+
async removeChannel(id) {
|
|
15053
|
+
return this.store.removeChannel(id);
|
|
15054
|
+
}
|
|
15055
|
+
async emit(input, options = {}) {
|
|
15056
|
+
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
15057
|
+
if (options.dedupe !== false) {
|
|
15058
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
15059
|
+
if (existing) {
|
|
15060
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
15061
|
+
}
|
|
15062
|
+
}
|
|
15063
|
+
await this.store.appendEvent(event);
|
|
15064
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
15065
|
+
return { event, deliveries, deduped: false };
|
|
15066
|
+
}
|
|
15067
|
+
async listEvents() {
|
|
15068
|
+
return this.store.listEvents();
|
|
15069
|
+
}
|
|
15070
|
+
async listDeliveries() {
|
|
15071
|
+
return this.store.listDeliveries();
|
|
15072
|
+
}
|
|
15073
|
+
async deliver(event) {
|
|
15074
|
+
const channels = await this.store.listChannels();
|
|
15075
|
+
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
15076
|
+
const deliveries = [];
|
|
15077
|
+
for (const channel of selected) {
|
|
15078
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
15079
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
15080
|
+
await this.store.appendDelivery(result);
|
|
15081
|
+
deliveries.push(result);
|
|
15082
|
+
}
|
|
15083
|
+
return deliveries;
|
|
15084
|
+
}
|
|
15085
|
+
async testChannel(id, input = {}) {
|
|
15086
|
+
const channel = await this.store.getChannel(id);
|
|
15087
|
+
if (!channel)
|
|
15088
|
+
throw new Error(`Channel not found: ${id}`);
|
|
15089
|
+
const event = createEvent({
|
|
15090
|
+
source: input.source ?? "hasna.events",
|
|
15091
|
+
type: input.type ?? "events.test",
|
|
15092
|
+
subject: input.subject ?? id,
|
|
15093
|
+
severity: input.severity ?? "info",
|
|
15094
|
+
data: input.data ?? { test: true },
|
|
15095
|
+
message: input.message ?? "Hasna events test delivery",
|
|
15096
|
+
dedupeKey: input.dedupeKey,
|
|
15097
|
+
schemaVersion: input.schemaVersion,
|
|
15098
|
+
metadata: input.metadata,
|
|
15099
|
+
time: input.time,
|
|
15100
|
+
id: input.id
|
|
15101
|
+
});
|
|
15102
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
15103
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
15104
|
+
await this.store.appendDelivery(result);
|
|
15105
|
+
return result;
|
|
15106
|
+
}
|
|
15107
|
+
async replay(options = {}) {
|
|
15108
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
15109
|
+
if (options.eventId && event.id !== options.eventId)
|
|
15110
|
+
return false;
|
|
15111
|
+
if (options.source && event.source !== options.source)
|
|
15112
|
+
return false;
|
|
15113
|
+
if (options.type && event.type !== options.type)
|
|
15114
|
+
return false;
|
|
15115
|
+
return true;
|
|
15116
|
+
});
|
|
15117
|
+
if (options.dryRun)
|
|
15118
|
+
return { events, deliveries: [] };
|
|
15119
|
+
const deliveries = [];
|
|
15120
|
+
for (const event of events) {
|
|
15121
|
+
deliveries.push(...await this.deliver(event));
|
|
15122
|
+
}
|
|
15123
|
+
return { events, deliveries };
|
|
15124
|
+
}
|
|
15125
|
+
async applyRedaction(event, channel) {
|
|
15126
|
+
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
15127
|
+
for (const redactor of this.redactors) {
|
|
15128
|
+
next = await redactor(next, channel);
|
|
15129
|
+
}
|
|
15130
|
+
return next;
|
|
15131
|
+
}
|
|
15132
|
+
async deliverWithRetry(event, channel) {
|
|
15133
|
+
const policy = normalizeRetryPolicy(channel.retry);
|
|
15134
|
+
const attempts = [];
|
|
15135
|
+
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
15136
|
+
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
15137
|
+
attempt.attempt = index + 1;
|
|
15138
|
+
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
15139
|
+
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
15140
|
+
}
|
|
15141
|
+
attempts.push(attempt);
|
|
15142
|
+
if (attempt.status !== "failed")
|
|
15143
|
+
break;
|
|
15144
|
+
if (attempt.nextBackoffMs)
|
|
15145
|
+
await Bun.sleep(attempt.nextBackoffMs);
|
|
15146
|
+
}
|
|
15147
|
+
return createDeliveryResult(event, channel, attempts);
|
|
15148
|
+
}
|
|
15149
|
+
}
|
|
15150
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
15151
|
+
if (paths.length === 0)
|
|
15152
|
+
return event;
|
|
15153
|
+
const copy = structuredClone(event);
|
|
15154
|
+
for (const path of paths) {
|
|
15155
|
+
setPath(copy, path, replacement);
|
|
15156
|
+
}
|
|
15157
|
+
return copy;
|
|
15158
|
+
}
|
|
15159
|
+
function sanitizeChannelForOutput(channel) {
|
|
15160
|
+
const copy = structuredClone(channel);
|
|
15161
|
+
if (copy.webhook?.secret)
|
|
15162
|
+
copy.webhook.secret = "[REDACTED]";
|
|
15163
|
+
if (copy.command?.env) {
|
|
15164
|
+
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
15165
|
+
}
|
|
15166
|
+
return copy;
|
|
15167
|
+
}
|
|
15168
|
+
function sanitizeChannelsForOutput(channels) {
|
|
15169
|
+
return channels.map(sanitizeChannelForOutput);
|
|
15170
|
+
}
|
|
15171
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
15172
|
+
return redactValue(event, replacement);
|
|
15173
|
+
}
|
|
15174
|
+
function shouldRedactKey(key) {
|
|
15175
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
15176
|
+
}
|
|
15177
|
+
function redactValue(value, replacement) {
|
|
15178
|
+
if (Array.isArray(value))
|
|
15179
|
+
return value.map((item) => redactValue(item, replacement));
|
|
15180
|
+
if (!value || typeof value !== "object")
|
|
15181
|
+
return value;
|
|
15182
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
15183
|
+
key,
|
|
15184
|
+
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
15185
|
+
]));
|
|
15186
|
+
}
|
|
15187
|
+
function setPath(input, path, replacement) {
|
|
15188
|
+
const parts = path.split(".");
|
|
15189
|
+
let cursor = input;
|
|
15190
|
+
for (const part of parts.slice(0, -1)) {
|
|
15191
|
+
const next = cursor[part];
|
|
15192
|
+
if (!next || typeof next !== "object")
|
|
15193
|
+
return;
|
|
15194
|
+
cursor = next;
|
|
15195
|
+
}
|
|
15196
|
+
const last = parts.at(-1);
|
|
15197
|
+
if (last && last in cursor)
|
|
15198
|
+
cursor[last] = replacement;
|
|
15199
|
+
}
|
|
15200
|
+
function normalizeTime(value) {
|
|
15201
|
+
if (!value)
|
|
15202
|
+
return new Date().toISOString();
|
|
15203
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
15204
|
+
}
|
|
15205
|
+
function normalizeRetryPolicy(policy) {
|
|
15206
|
+
return {
|
|
15207
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
15208
|
+
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
15209
|
+
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
15210
|
+
};
|
|
15211
|
+
}
|
|
15212
|
+
function parseJsonObject(value, fallback) {
|
|
15213
|
+
if (!value)
|
|
15214
|
+
return fallback;
|
|
15215
|
+
const parsed = JSON.parse(value);
|
|
15216
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15217
|
+
throw new Error("Expected a JSON object");
|
|
15218
|
+
}
|
|
15219
|
+
return parsed;
|
|
15220
|
+
}
|
|
15221
|
+
function parseHeaders(values) {
|
|
15222
|
+
if (!values?.length)
|
|
15223
|
+
return;
|
|
15224
|
+
const headers = {};
|
|
15225
|
+
for (const value of values) {
|
|
15226
|
+
const separator = value.indexOf("=");
|
|
15227
|
+
if (separator === -1)
|
|
15228
|
+
throw new Error(`Invalid header, expected name=value: ${value}`);
|
|
15229
|
+
headers[value.slice(0, separator)] = value.slice(separator + 1);
|
|
15230
|
+
}
|
|
15231
|
+
return headers;
|
|
15232
|
+
}
|
|
15233
|
+
function parseFilter(options) {
|
|
15234
|
+
const filter2 = {};
|
|
15235
|
+
if (options.source)
|
|
15236
|
+
filter2.source = options.source;
|
|
15237
|
+
if (options.type)
|
|
15238
|
+
filter2.type = options.type;
|
|
15239
|
+
if (options.subject)
|
|
15240
|
+
filter2.subject = options.subject;
|
|
15241
|
+
if (options.severity)
|
|
15242
|
+
filter2.severity = options.severity;
|
|
15243
|
+
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
15244
|
+
}
|
|
15245
|
+
function createClient(options) {
|
|
15246
|
+
if (options.createClient)
|
|
15247
|
+
return options.createClient();
|
|
15248
|
+
return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
|
|
15249
|
+
}
|
|
15250
|
+
function print(value, json, text) {
|
|
15251
|
+
if (json)
|
|
15252
|
+
console.log(JSON.stringify(value, null, 2));
|
|
15253
|
+
else
|
|
15254
|
+
console.log(text);
|
|
15255
|
+
}
|
|
15256
|
+
function hasJsonOption(options) {
|
|
15257
|
+
return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
|
|
15258
|
+
}
|
|
15259
|
+
function wantsJson(actionOptions, command) {
|
|
15260
|
+
return hasJsonOption(actionOptions) || hasJsonOption(command);
|
|
15261
|
+
}
|
|
15262
|
+
function registerWebhookCommands(program, options) {
|
|
15263
|
+
const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
|
|
15264
|
+
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) => {
|
|
15265
|
+
const timestamp = new Date().toISOString();
|
|
15266
|
+
const channel = {
|
|
15267
|
+
id: actionOptions.id,
|
|
15268
|
+
name: actionOptions.name,
|
|
15269
|
+
enabled: !actionOptions.disabled,
|
|
15270
|
+
transport: actionOptions.transport,
|
|
15271
|
+
filters: parseFilter(actionOptions),
|
|
15272
|
+
retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
|
|
15273
|
+
redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
|
|
15274
|
+
createdAt: timestamp,
|
|
15275
|
+
updatedAt: timestamp
|
|
15276
|
+
};
|
|
15277
|
+
if (actionOptions.transport === "webhook") {
|
|
15278
|
+
channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
|
|
15279
|
+
} else if (actionOptions.transport === "command") {
|
|
15280
|
+
channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
|
|
15281
|
+
} else {
|
|
15282
|
+
throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
|
|
15283
|
+
}
|
|
15284
|
+
const saved = await createClient(options).addChannel(channel);
|
|
15285
|
+
print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
|
|
15286
|
+
});
|
|
15287
|
+
webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
15288
|
+
const channels = await createClient(options).listChannels();
|
|
15289
|
+
if (wantsJson(actionOptions, command)) {
|
|
15290
|
+
console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
|
|
15291
|
+
return;
|
|
15292
|
+
}
|
|
15293
|
+
if (!channels.length) {
|
|
15294
|
+
console.log("No channels configured.");
|
|
15295
|
+
return;
|
|
15296
|
+
}
|
|
15297
|
+
for (const channel of channels) {
|
|
15298
|
+
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
15299
|
+
}
|
|
15300
|
+
});
|
|
15301
|
+
webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
15302
|
+
const removed = await createClient(options).removeChannel(id);
|
|
15303
|
+
print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
15304
|
+
});
|
|
15305
|
+
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) => {
|
|
15306
|
+
const result = await createClient(options).testChannel(id, {
|
|
15307
|
+
source: options.source,
|
|
15308
|
+
type: actionOptions.type,
|
|
15309
|
+
subject: actionOptions.subject ?? id,
|
|
15310
|
+
message: actionOptions.message,
|
|
15311
|
+
data: parseJsonObject(actionOptions.data, { test: true })
|
|
15312
|
+
});
|
|
15313
|
+
print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
|
|
15314
|
+
});
|
|
15315
|
+
return webhooks;
|
|
15316
|
+
}
|
|
15317
|
+
function registerEventCommands(program, options) {
|
|
15318
|
+
const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
15319
|
+
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) => {
|
|
15320
|
+
const result = await createClient(options).emit({
|
|
15321
|
+
source: actionOptions.source ?? options.source,
|
|
15322
|
+
type,
|
|
15323
|
+
subject: actionOptions.subject,
|
|
15324
|
+
severity: actionOptions.severity,
|
|
15325
|
+
message: actionOptions.message,
|
|
15326
|
+
dedupeKey: actionOptions.dedupeKey,
|
|
15327
|
+
data: parseJsonObject(actionOptions.data, {}),
|
|
15328
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
15329
|
+
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
15330
|
+
print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
15331
|
+
});
|
|
15332
|
+
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) => {
|
|
15333
|
+
let rows = await createClient(options).listEvents();
|
|
15334
|
+
if (actionOptions.source)
|
|
15335
|
+
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
15336
|
+
if (actionOptions.type)
|
|
15337
|
+
rows = rows.filter((event) => event.type === actionOptions.type);
|
|
15338
|
+
if (actionOptions.limit)
|
|
15339
|
+
rows = rows.slice(-actionOptions.limit);
|
|
15340
|
+
if (wantsJson(actionOptions, command)) {
|
|
15341
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
15342
|
+
return;
|
|
15343
|
+
}
|
|
15344
|
+
if (!rows.length) {
|
|
15345
|
+
console.log("No events recorded.");
|
|
15346
|
+
return;
|
|
15347
|
+
}
|
|
15348
|
+
for (const event of rows)
|
|
15349
|
+
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
15350
|
+
});
|
|
15351
|
+
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) => {
|
|
15352
|
+
const result = await createClient(options).replay({
|
|
15353
|
+
eventId: actionOptions.id,
|
|
15354
|
+
source: actionOptions.source,
|
|
15355
|
+
type: actionOptions.type,
|
|
15356
|
+
dryRun: actionOptions.dryRun
|
|
15357
|
+
});
|
|
15358
|
+
print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
|
|
15359
|
+
});
|
|
15360
|
+
return events;
|
|
15361
|
+
}
|
|
15362
|
+
function registerEventsCommands(program, options) {
|
|
15363
|
+
registerWebhookCommands(program, options);
|
|
15364
|
+
registerEventCommands(program, options);
|
|
15365
|
+
}
|
|
15366
|
+
function parseNumber(value) {
|
|
15367
|
+
const parsed = Number(value);
|
|
15368
|
+
if (!Number.isFinite(parsed))
|
|
15369
|
+
throw new Error(`Expected a number, got ${value}`);
|
|
15370
|
+
return parsed;
|
|
15371
|
+
}
|
|
15372
|
+
function collectValues(value, previous) {
|
|
15373
|
+
previous.push(value);
|
|
15374
|
+
return previous;
|
|
15375
|
+
}
|
|
15376
|
+
|
|
14695
15377
|
// src/cli/index.tsx
|
|
14696
15378
|
import { render } from "ink";
|
|
14697
15379
|
|
|
@@ -14713,8 +15395,8 @@ var {
|
|
|
14713
15395
|
|
|
14714
15396
|
// src/cli/index.tsx
|
|
14715
15397
|
import chalk2 from "chalk";
|
|
14716
|
-
import { existsSync as
|
|
14717
|
-
import { join as
|
|
15398
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
|
|
15399
|
+
import { join as join11, dirname as dirname4 } from "path";
|
|
14718
15400
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
14719
15401
|
|
|
14720
15402
|
// src/cli/components/App.tsx
|
|
@@ -15191,7 +15873,7 @@ var COL_NAME = 18;
|
|
|
15191
15873
|
var COL_VERSION = 9;
|
|
15192
15874
|
var COL_EVENT = 14;
|
|
15193
15875
|
var COL_DESC = 40;
|
|
15194
|
-
function
|
|
15876
|
+
function truncate2(str, max) {
|
|
15195
15877
|
return str.length > max ? str.slice(0, max - 3) + "..." : str;
|
|
15196
15878
|
}
|
|
15197
15879
|
function pad(str, width) {
|
|
@@ -15268,7 +15950,7 @@ function DataTable({
|
|
|
15268
15950
|
pad(hook.displayName, COL_NAME),
|
|
15269
15951
|
pad(hook.version, COL_VERSION),
|
|
15270
15952
|
pad(hook.event, COL_EVENT),
|
|
15271
|
-
|
|
15953
|
+
truncate2(hook.description, COL_DESC)
|
|
15272
15954
|
]
|
|
15273
15955
|
}, undefined, true, undefined, this)
|
|
15274
15956
|
}, hook.name, false, undefined, this);
|
|
@@ -15892,7 +16574,7 @@ init_installer();
|
|
|
15892
16574
|
init_profiles();
|
|
15893
16575
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
15894
16576
|
var __dirname4 = dirname4(fileURLToPath3(import.meta.url));
|
|
15895
|
-
var pkgPath =
|
|
16577
|
+
var pkgPath = existsSync10(join11(__dirname4, "..", "package.json")) ? join11(__dirname4, "..", "package.json") : join11(__dirname4, "..", "..", "package.json");
|
|
15896
16578
|
var pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
15897
16579
|
var program2 = new Command;
|
|
15898
16580
|
function resolveScope(options) {
|
|
@@ -15963,8 +16645,8 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
15963
16645
|
process.exit(1);
|
|
15964
16646
|
}
|
|
15965
16647
|
const hookDir = getHookPath(hook);
|
|
15966
|
-
const hookScript =
|
|
15967
|
-
if (!
|
|
16648
|
+
const hookScript = join11(hookDir, "src", "hook.ts");
|
|
16649
|
+
if (!existsSync10(hookScript)) {
|
|
15968
16650
|
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
|
|
15969
16651
|
process.exit(1);
|
|
15970
16652
|
}
|
|
@@ -16264,7 +16946,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
16264
16946
|
const settingsPath = getSettingsPath(scope);
|
|
16265
16947
|
const issues = [];
|
|
16266
16948
|
const healthy = [];
|
|
16267
|
-
const settingsExist =
|
|
16949
|
+
const settingsExist = existsSync10(settingsPath);
|
|
16268
16950
|
if (!settingsExist) {
|
|
16269
16951
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
16270
16952
|
}
|
|
@@ -16278,8 +16960,8 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
16278
16960
|
continue;
|
|
16279
16961
|
}
|
|
16280
16962
|
const hookDir = getHookPath(name);
|
|
16281
|
-
const hookScript =
|
|
16282
|
-
if (!
|
|
16963
|
+
const hookScript = join11(hookDir, "src", "hook.ts");
|
|
16964
|
+
if (!existsSync10(hookScript)) {
|
|
16283
16965
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
16284
16966
|
hookHealthy = false;
|
|
16285
16967
|
}
|
|
@@ -16383,9 +17065,9 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
16383
17065
|
return;
|
|
16384
17066
|
}
|
|
16385
17067
|
const hookPath = getHookPath(hook);
|
|
16386
|
-
const readmePath =
|
|
17068
|
+
const readmePath = join11(hookPath, "README.md");
|
|
16387
17069
|
let readme = "";
|
|
16388
|
-
if (
|
|
17070
|
+
if (existsSync10(readmePath)) {
|
|
16389
17071
|
readme = readFileSync6(readmePath, "utf-8");
|
|
16390
17072
|
}
|
|
16391
17073
|
if (options.json) {
|
|
@@ -16763,4 +17445,5 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
16763
17445
|
startMcpHttpServer2({ name: "hooks", port: resolveMcpHttpPort2(args), buildServer: createHooksServer2 });
|
|
16764
17446
|
}
|
|
16765
17447
|
});
|
|
17448
|
+
registerEventsCommands(program2, { source: "hooks" });
|
|
16766
17449
|
program2.parse();
|