@hasna/hooks 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/bin/index.js +303 -48
- package/hooks/codewith-native-common.test.ts +1508 -3
- package/hooks/codewith-native-common.ts +1627 -37
- package/hooks/pre-bash/README.md +72 -2
- package/hooks/worktree-guard/README.md +9 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,10 +67,21 @@ hooks install session-start prompt-guard pre-bash worktree-guard stop-sync knowl
|
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
The scoped destructive-operation guard does not block every cleanup command. It
|
|
70
|
-
blocks resolved shell/file-tool targets that threaten
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
blocks resolved shell/file-tool targets that threaten `/` or a system root
|
|
71
|
+
(`/usr`, `/etc`, `/bin`, `/lib`, `/var`, `/boot`, `/home`, `/Users`, and the
|
|
72
|
+
other FHS and macOS equivalents), `~/.hasna`, configured workspace roots, Hasna
|
|
73
|
+
division/scope roots, or active repo/worktree roots, including recursive `rm`,
|
|
74
|
+
`rsync --delete`, destructive `find`, and destructive `git clean` / `git reset
|
|
75
|
+
--hard` forms.
|
|
76
|
+
|
|
77
|
+
It also blocks by *shape*: a destructive target containing a command
|
|
78
|
+
substitution or variable expansion immediately followed by `/` is checked as the
|
|
79
|
+
shell would render it if that expansion returned empty, so
|
|
80
|
+
`rm -rf "$(anything)"/*` and `rm -rf "$VAR"/*` are refused whatever the
|
|
81
|
+
expansion is. Wrapped forms (`bash -c`, `su -c`, `eval`, `ssh host '…'`) are
|
|
82
|
+
unwrapped first. See [`hooks/pre-bash/README.md`](hooks/pre-bash/README.md) for
|
|
83
|
+
the full rules, the deliberate exemptions (`${VAR:?}`, bare `"$(cmd)"` with no
|
|
84
|
+
trailing separator), and the recommended safe form.
|
|
74
85
|
|
|
75
86
|
Apply that fragment through `open-configs` or the managed config renderer. A
|
|
76
87
|
direct write path exists only for explicit local/test use:
|
package/bin/index.js
CHANGED
|
@@ -6270,6 +6270,7 @@ var init_http = () => {};
|
|
|
6270
6270
|
|
|
6271
6271
|
// node_modules/@hasna/events/dist/commander.js
|
|
6272
6272
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
6273
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
6273
6274
|
import { existsSync } from "fs";
|
|
6274
6275
|
import { homedir } from "os";
|
|
6275
6276
|
import { join } from "path";
|
|
@@ -6374,6 +6375,9 @@ function channelMatchesEvent(channel, event) {
|
|
|
6374
6375
|
}
|
|
6375
6376
|
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
6376
6377
|
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
6378
|
+
var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
|
|
6379
|
+
var DEFAULT_EVENT_PAGE_LIMIT = 100;
|
|
6380
|
+
var MAX_EVENT_PAGE_LIMIT = 1000;
|
|
6377
6381
|
function getEventsDataDir(override) {
|
|
6378
6382
|
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
6379
6383
|
}
|
|
@@ -6387,11 +6391,13 @@ function getActiveEventsDirEnv() {
|
|
|
6387
6391
|
|
|
6388
6392
|
class JsonEventsStore {
|
|
6389
6393
|
dataDir;
|
|
6394
|
+
runtime;
|
|
6390
6395
|
channelsPath;
|
|
6391
6396
|
eventsPath;
|
|
6392
6397
|
deliveriesPath;
|
|
6393
6398
|
constructor(dataDir = getEventsDataDir()) {
|
|
6394
6399
|
this.dataDir = dataDir;
|
|
6400
|
+
this.runtime = localJsonRuntime(dataDir);
|
|
6395
6401
|
this.channelsPath = join(dataDir, "channels.json");
|
|
6396
6402
|
this.eventsPath = join(dataDir, "events.json");
|
|
6397
6403
|
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
@@ -6439,13 +6445,58 @@ class JsonEventsStore {
|
|
|
6439
6445
|
await this.writeJson(this.eventsPath, events);
|
|
6440
6446
|
return event;
|
|
6441
6447
|
}
|
|
6442
|
-
async
|
|
6448
|
+
async appendEventOnce(event, options = {}) {
|
|
6443
6449
|
await this.init();
|
|
6444
|
-
|
|
6450
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
6451
|
+
const dedupe = options.dedupe !== false;
|
|
6452
|
+
if (dedupe) {
|
|
6453
|
+
const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
|
|
6454
|
+
if (existing) {
|
|
6455
|
+
return {
|
|
6456
|
+
event: existing,
|
|
6457
|
+
stored: false,
|
|
6458
|
+
deduped: true,
|
|
6459
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
6460
|
+
};
|
|
6461
|
+
}
|
|
6462
|
+
}
|
|
6463
|
+
events.push(event);
|
|
6464
|
+
await this.writeJson(this.eventsPath, events);
|
|
6465
|
+
return {
|
|
6466
|
+
event,
|
|
6467
|
+
stored: true,
|
|
6468
|
+
deduped: false,
|
|
6469
|
+
identity: { id: event.id, dedupeKey: event.dedupeKey }
|
|
6470
|
+
};
|
|
6471
|
+
}
|
|
6472
|
+
async listEvents(options = {}) {
|
|
6473
|
+
await this.init();
|
|
6474
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
6475
|
+
return queryEvents(events, options);
|
|
6476
|
+
}
|
|
6477
|
+
async listEventsPage(options = {}) {
|
|
6478
|
+
await this.init();
|
|
6479
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
6480
|
+
const queried = queryEvents(events, {
|
|
6481
|
+
eventId: options.eventId,
|
|
6482
|
+
source: options.source,
|
|
6483
|
+
type: options.type
|
|
6484
|
+
});
|
|
6485
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
6486
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
6487
|
+
const pageEvents = queried.slice(offset, offset + limit);
|
|
6488
|
+
const nextOffset = offset + pageEvents.length;
|
|
6489
|
+
const hasMore = nextOffset < queried.length;
|
|
6490
|
+
return {
|
|
6491
|
+
events: pageEvents,
|
|
6492
|
+
cursor: options.cursor,
|
|
6493
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
6494
|
+
hasMore
|
|
6495
|
+
};
|
|
6445
6496
|
}
|
|
6446
6497
|
async findEventByIdentity(identity) {
|
|
6447
6498
|
const events = await this.listEvents();
|
|
6448
|
-
return events
|
|
6499
|
+
return findEventByIdentity(events, identity);
|
|
6449
6500
|
}
|
|
6450
6501
|
async appendDelivery(result) {
|
|
6451
6502
|
await this.init();
|
|
@@ -6496,6 +6547,83 @@ class JsonEventsStore {
|
|
|
6496
6547
|
});
|
|
6497
6548
|
}
|
|
6498
6549
|
}
|
|
6550
|
+
function localJsonRuntime(dataDir = getEventsDataDir()) {
|
|
6551
|
+
return {
|
|
6552
|
+
mode: "local-files",
|
|
6553
|
+
name: "json-events-store",
|
|
6554
|
+
remote: false,
|
|
6555
|
+
localFiles: true,
|
|
6556
|
+
localSqlite: false,
|
|
6557
|
+
postgres: false,
|
|
6558
|
+
s3: false,
|
|
6559
|
+
aws: false,
|
|
6560
|
+
durable: true,
|
|
6561
|
+
idempotency: "best-effort-local",
|
|
6562
|
+
replayCursors: true,
|
|
6563
|
+
description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
6564
|
+
};
|
|
6565
|
+
}
|
|
6566
|
+
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
6567
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
6568
|
+
throw new Error(`Invalid event cursor offset: ${offset}`);
|
|
6569
|
+
const payload = {
|
|
6570
|
+
offset,
|
|
6571
|
+
eventId: options.eventId,
|
|
6572
|
+
source: options.source,
|
|
6573
|
+
type: options.type
|
|
6574
|
+
};
|
|
6575
|
+
return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
|
|
6576
|
+
}
|
|
6577
|
+
function decodeLocalJsonEventCursor(cursor, options = {}) {
|
|
6578
|
+
if (!cursor)
|
|
6579
|
+
return 0;
|
|
6580
|
+
if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
|
|
6581
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
6582
|
+
const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
|
|
6583
|
+
let payload;
|
|
6584
|
+
try {
|
|
6585
|
+
payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
|
|
6586
|
+
} catch {
|
|
6587
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
6588
|
+
}
|
|
6589
|
+
const offset = payload.offset;
|
|
6590
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
6591
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
6592
|
+
assertCursorFilter("eventId", payload.eventId, options.eventId);
|
|
6593
|
+
assertCursorFilter("source", payload.source, options.source);
|
|
6594
|
+
assertCursorFilter("type", payload.type, options.type);
|
|
6595
|
+
return offset;
|
|
6596
|
+
}
|
|
6597
|
+
function normalizeEventPageLimit(limit) {
|
|
6598
|
+
if (limit === undefined)
|
|
6599
|
+
return DEFAULT_EVENT_PAGE_LIMIT;
|
|
6600
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
6601
|
+
throw new Error(`Event page limit must be a positive integer, got ${limit}`);
|
|
6602
|
+
return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
|
|
6603
|
+
}
|
|
6604
|
+
function queryEvents(events, options) {
|
|
6605
|
+
let rows = events;
|
|
6606
|
+
if (options.eventId)
|
|
6607
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
6608
|
+
if (options.source)
|
|
6609
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
6610
|
+
if (options.type)
|
|
6611
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
6612
|
+
if (options.cursor) {
|
|
6613
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
6614
|
+
rows = rows.slice(offset);
|
|
6615
|
+
}
|
|
6616
|
+
if (options.limit !== undefined)
|
|
6617
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
6618
|
+
return rows;
|
|
6619
|
+
}
|
|
6620
|
+
function assertCursorFilter(name, cursorValue, optionValue) {
|
|
6621
|
+
if (cursorValue !== optionValue)
|
|
6622
|
+
throw new Error(`Local JSON event cursor ${name} filter mismatch`);
|
|
6623
|
+
}
|
|
6624
|
+
function findEventByIdentity(events, identity) {
|
|
6625
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
6626
|
+
}
|
|
6499
6627
|
async function getEventsStatus(dataDir) {
|
|
6500
6628
|
const store = new JsonEventsStore(dataDir);
|
|
6501
6629
|
await store.init();
|
|
@@ -6512,6 +6640,7 @@ async function getEventsStatus(dataDir) {
|
|
|
6512
6640
|
service: "events",
|
|
6513
6641
|
schemaVersion: "1.0",
|
|
6514
6642
|
dataDir: store.dataDir,
|
|
6643
|
+
storage: store.runtime,
|
|
6515
6644
|
env: {
|
|
6516
6645
|
primary: HASNA_EVENTS_DIR_ENV,
|
|
6517
6646
|
fallback: HASNA_EVENTS_HOME_ENV,
|
|
@@ -6698,6 +6827,51 @@ function createDeliveryResult(event, channel, attempts) {
|
|
|
6698
6827
|
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
6699
6828
|
};
|
|
6700
6829
|
}
|
|
6830
|
+
|
|
6831
|
+
class EventValidationError extends Error {
|
|
6832
|
+
eventType;
|
|
6833
|
+
issues;
|
|
6834
|
+
constructor(eventType, issues) {
|
|
6835
|
+
const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
|
|
6836
|
+
super(`Event validation failed for type "${eventType}": ${detail}`);
|
|
6837
|
+
this.name = "EventValidationError";
|
|
6838
|
+
this.eventType = eventType;
|
|
6839
|
+
this.issues = issues;
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
|
|
6843
|
+
class EventTypeCatalog {
|
|
6844
|
+
definitions = new Map;
|
|
6845
|
+
register(definition) {
|
|
6846
|
+
this.definitions.set(definition.type, definition);
|
|
6847
|
+
return this;
|
|
6848
|
+
}
|
|
6849
|
+
unregister(type) {
|
|
6850
|
+
return this.definitions.delete(type);
|
|
6851
|
+
}
|
|
6852
|
+
has(type) {
|
|
6853
|
+
return this.definitions.has(type);
|
|
6854
|
+
}
|
|
6855
|
+
get(type) {
|
|
6856
|
+
return this.definitions.get(type);
|
|
6857
|
+
}
|
|
6858
|
+
list() {
|
|
6859
|
+
return [...this.definitions.values()];
|
|
6860
|
+
}
|
|
6861
|
+
validateEvent(event) {
|
|
6862
|
+
const definition = this.definitions.get(event.type);
|
|
6863
|
+
if (!definition)
|
|
6864
|
+
return { ok: true };
|
|
6865
|
+
return definition.validate(event.data, event);
|
|
6866
|
+
}
|
|
6867
|
+
assertEventValid(event) {
|
|
6868
|
+
const result = this.validateEvent(event);
|
|
6869
|
+
if (!result.ok) {
|
|
6870
|
+
throw new EventValidationError(event.type, result.issues);
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6874
|
+
var defaultEventTypeCatalog = new EventTypeCatalog;
|
|
6701
6875
|
function createEvent(input) {
|
|
6702
6876
|
return {
|
|
6703
6877
|
id: input.id ?? randomUUID2(),
|
|
@@ -6718,10 +6892,14 @@ class EventsClient {
|
|
|
6718
6892
|
store;
|
|
6719
6893
|
redactors;
|
|
6720
6894
|
transportOptions;
|
|
6895
|
+
catalog;
|
|
6896
|
+
validateCatalogTypes;
|
|
6721
6897
|
constructor(options = {}) {
|
|
6722
6898
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
6723
6899
|
this.redactors = options.redactors ?? [];
|
|
6724
6900
|
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
6901
|
+
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
6902
|
+
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
6725
6903
|
}
|
|
6726
6904
|
async addChannel(input) {
|
|
6727
6905
|
const timestamp = new Date().toISOString();
|
|
@@ -6739,18 +6917,40 @@ class EventsClient {
|
|
|
6739
6917
|
}
|
|
6740
6918
|
async emit(input, options = {}) {
|
|
6741
6919
|
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
6742
|
-
if (options.
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
6750
|
-
return { event, deliveries, deduped: false };
|
|
6751
|
-
}
|
|
6752
|
-
async listEvents() {
|
|
6753
|
-
|
|
6920
|
+
if (options.validate ?? this.validateCatalogTypes) {
|
|
6921
|
+
this.catalog.assertEventValid(event);
|
|
6922
|
+
}
|
|
6923
|
+
const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
|
|
6924
|
+
if (append.deduped) {
|
|
6925
|
+
return { event: append.event, deliveries: [], deduped: true };
|
|
6926
|
+
}
|
|
6927
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
|
|
6928
|
+
return { event: append.event, deliveries, deduped: false };
|
|
6929
|
+
}
|
|
6930
|
+
async listEvents(options = {}) {
|
|
6931
|
+
if (Object.keys(options).length === 0)
|
|
6932
|
+
return this.store.listEvents();
|
|
6933
|
+
return queryClientEvents(await this.store.listEvents(), options);
|
|
6934
|
+
}
|
|
6935
|
+
async listEventsPage(options = {}) {
|
|
6936
|
+
if (this.store.listEventsPage)
|
|
6937
|
+
return this.store.listEventsPage(options);
|
|
6938
|
+
const events = queryClientEvents(await this.store.listEvents(), {
|
|
6939
|
+
eventId: options.eventId,
|
|
6940
|
+
source: options.source,
|
|
6941
|
+
type: options.type
|
|
6942
|
+
});
|
|
6943
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
6944
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
6945
|
+
const pageEvents = events.slice(offset, offset + limit);
|
|
6946
|
+
const nextOffset = offset + pageEvents.length;
|
|
6947
|
+
const hasMore = nextOffset < events.length;
|
|
6948
|
+
return {
|
|
6949
|
+
events: pageEvents,
|
|
6950
|
+
cursor: options.cursor,
|
|
6951
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
6952
|
+
hasMore
|
|
6953
|
+
};
|
|
6754
6954
|
}
|
|
6755
6955
|
async listDeliveries() {
|
|
6756
6956
|
return this.store.listDeliveries();
|
|
@@ -6818,22 +7018,37 @@ class EventsClient {
|
|
|
6818
7018
|
return result;
|
|
6819
7019
|
}
|
|
6820
7020
|
async replay(options = {}) {
|
|
6821
|
-
const
|
|
6822
|
-
if (options.eventId && event.id !== options.eventId)
|
|
6823
|
-
return false;
|
|
6824
|
-
if (options.source && event.source !== options.source)
|
|
6825
|
-
return false;
|
|
6826
|
-
if (options.type && event.type !== options.type)
|
|
6827
|
-
return false;
|
|
6828
|
-
return true;
|
|
6829
|
-
});
|
|
7021
|
+
const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
|
|
6830
7022
|
if (options.dryRun)
|
|
6831
|
-
return { events, deliveries: [] };
|
|
7023
|
+
return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
6832
7024
|
const deliveries = [];
|
|
6833
|
-
for (const event of events) {
|
|
7025
|
+
for (const event of page.events) {
|
|
6834
7026
|
deliveries.push(...await this.deliver(event));
|
|
6835
7027
|
}
|
|
6836
|
-
return { events, deliveries };
|
|
7028
|
+
return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
7029
|
+
}
|
|
7030
|
+
async appendEvent(event, options) {
|
|
7031
|
+
if (this.store.appendEventOnce) {
|
|
7032
|
+
return this.store.appendEventOnce(event, { dedupe: options.dedupe });
|
|
7033
|
+
}
|
|
7034
|
+
if (options.dedupe) {
|
|
7035
|
+
const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
|
|
7036
|
+
if (existing) {
|
|
7037
|
+
return {
|
|
7038
|
+
event: existing,
|
|
7039
|
+
stored: false,
|
|
7040
|
+
deduped: true,
|
|
7041
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
7042
|
+
};
|
|
7043
|
+
}
|
|
7044
|
+
}
|
|
7045
|
+
const stored = await this.store.appendEvent(event);
|
|
7046
|
+
return {
|
|
7047
|
+
event: stored,
|
|
7048
|
+
stored: true,
|
|
7049
|
+
deduped: false,
|
|
7050
|
+
identity: { id: stored.id, dedupeKey: stored.dedupeKey }
|
|
7051
|
+
};
|
|
6837
7052
|
}
|
|
6838
7053
|
async applyRedaction(event, channel) {
|
|
6839
7054
|
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
@@ -6910,6 +7125,20 @@ function setPath(input, path, replacement) {
|
|
|
6910
7125
|
if (last && last in cursor)
|
|
6911
7126
|
cursor[last] = replacement;
|
|
6912
7127
|
}
|
|
7128
|
+
function queryClientEvents(events, options) {
|
|
7129
|
+
let rows = events;
|
|
7130
|
+
if (options.eventId)
|
|
7131
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
7132
|
+
if (options.source)
|
|
7133
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
7134
|
+
if (options.type)
|
|
7135
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
7136
|
+
if (options.cursor)
|
|
7137
|
+
rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
|
|
7138
|
+
if (options.limit !== undefined)
|
|
7139
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
7140
|
+
return rows;
|
|
7141
|
+
}
|
|
6913
7142
|
function normalizeTime(value) {
|
|
6914
7143
|
if (!value)
|
|
6915
7144
|
return new Date().toISOString();
|
|
@@ -6992,6 +7221,7 @@ function parseMatcherExpression(value, label) {
|
|
|
6992
7221
|
negated: false
|
|
6993
7222
|
};
|
|
6994
7223
|
}
|
|
7224
|
+
var DEFAULT_EVENT_LIST_LIMIT = 100;
|
|
6995
7225
|
function parseJsonObject(value, fallback) {
|
|
6996
7226
|
if (!value)
|
|
6997
7227
|
return fallback;
|
|
@@ -7024,6 +7254,14 @@ function print(value, json, text) {
|
|
|
7024
7254
|
else
|
|
7025
7255
|
console.log(text);
|
|
7026
7256
|
}
|
|
7257
|
+
function fail(error, json) {
|
|
7258
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7259
|
+
if (json)
|
|
7260
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
7261
|
+
else
|
|
7262
|
+
console.error(message);
|
|
7263
|
+
process.exitCode = 1;
|
|
7264
|
+
}
|
|
7027
7265
|
function hasJsonOption(options) {
|
|
7028
7266
|
return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
|
|
7029
7267
|
}
|
|
@@ -7078,26 +7316,36 @@ function registerChannelCommands(program, options) {
|
|
|
7078
7316
|
print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
7079
7317
|
});
|
|
7080
7318
|
channels.command("test").description("Send a test event to one channel").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").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("--metadata <json>", "Event metadata JSON object").option("--honor-filters", "Skip delivery when the sample event does not match channel filters", false).option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
7081
|
-
const
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7319
|
+
const json = wantsJson(actionOptions, command);
|
|
7320
|
+
try {
|
|
7321
|
+
const result = await createClient(options).testChannel(id, {
|
|
7322
|
+
source: actionOptions.source ?? options.source,
|
|
7323
|
+
type: actionOptions.type,
|
|
7324
|
+
subject: actionOptions.subject ?? id,
|
|
7325
|
+
message: actionOptions.message,
|
|
7326
|
+
data: parseJsonObject(actionOptions.data, { test: true }),
|
|
7327
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
7328
|
+
}, { honorFilters: actionOptions.honorFilters });
|
|
7329
|
+
print(result, json, `${result.status}: ${result.channelId}`);
|
|
7330
|
+
} catch (error) {
|
|
7331
|
+
fail(error, json);
|
|
7332
|
+
}
|
|
7090
7333
|
});
|
|
7091
7334
|
channels.command("match").description("Check whether a sample event matches one channel without delivering").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events match preview").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
7092
|
-
const
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7335
|
+
const json = wantsJson(actionOptions, command);
|
|
7336
|
+
try {
|
|
7337
|
+
const result = await createClient(options).matchChannel(id, {
|
|
7338
|
+
source: actionOptions.source ?? options.source,
|
|
7339
|
+
type: actionOptions.type,
|
|
7340
|
+
subject: actionOptions.subject ?? id,
|
|
7341
|
+
message: actionOptions.message,
|
|
7342
|
+
data: parseJsonObject(actionOptions.data, { test: true }),
|
|
7343
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
7344
|
+
});
|
|
7345
|
+
print(result, json, `${result.matched ? "matched" : "skipped"}: ${result.channelId}`);
|
|
7346
|
+
} catch (error) {
|
|
7347
|
+
fail(error, json);
|
|
7348
|
+
}
|
|
7101
7349
|
});
|
|
7102
7350
|
return channels;
|
|
7103
7351
|
}
|
|
@@ -7116,7 +7364,8 @@ function registerEventCommands(program, options) {
|
|
|
7116
7364
|
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
7117
7365
|
print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
7118
7366
|
});
|
|
7119
|
-
|
|
7367
|
+
const defaultListLimit = options.defaultEventListLimit ?? DEFAULT_EVENT_LIST_LIMIT;
|
|
7368
|
+
events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", `Limit to the most recent <n> events (default ${defaultListLimit}; use 0 for all)`, parseNumber, defaultListLimit).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
7120
7369
|
let rows = await createClient(options).listEvents();
|
|
7121
7370
|
if (actionOptions.source)
|
|
7122
7371
|
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
@@ -7135,14 +7384,16 @@ function registerEventCommands(program, options) {
|
|
|
7135
7384
|
for (const event of rows)
|
|
7136
7385
|
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
7137
7386
|
});
|
|
7138
|
-
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) => {
|
|
7387
|
+
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("--cursor <cursor>", "Opaque replay cursor from a previous page").option("--limit <n>", "Maximum events to replay", parseNumber).option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
7139
7388
|
const result = await createClient(options).replay({
|
|
7140
7389
|
eventId: actionOptions.id,
|
|
7141
7390
|
source: actionOptions.source,
|
|
7142
7391
|
type: actionOptions.type,
|
|
7392
|
+
cursor: actionOptions.cursor,
|
|
7393
|
+
limit: actionOptions.limit,
|
|
7143
7394
|
dryRun: actionOptions.dryRun
|
|
7144
7395
|
});
|
|
7145
|
-
print(result, wantsJson(actionOptions, command),
|
|
7396
|
+
print(result, wantsJson(actionOptions, command), replaySummary(result.events.length, result.deliveries.length, result.nextCursor));
|
|
7146
7397
|
});
|
|
7147
7398
|
return events;
|
|
7148
7399
|
}
|
|
@@ -7160,6 +7411,10 @@ function collectValues(value, previous) {
|
|
|
7160
7411
|
previous.push(value);
|
|
7161
7412
|
return previous;
|
|
7162
7413
|
}
|
|
7414
|
+
function replaySummary(events, deliveries, nextCursor) {
|
|
7415
|
+
const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
|
|
7416
|
+
return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
|
|
7417
|
+
}
|
|
7163
7418
|
|
|
7164
7419
|
// src/cli/index.tsx
|
|
7165
7420
|
import { render } from "ink";
|