wicked-bus 1.0.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 +153 -0
- package/commands/cli.js +129 -0
- package/commands/cmd-ack.js +31 -0
- package/commands/cmd-cleanup.js +44 -0
- package/commands/cmd-deregister.js +26 -0
- package/commands/cmd-emit.js +52 -0
- package/commands/cmd-init.js +30 -0
- package/commands/cmd-list.js +39 -0
- package/commands/cmd-register.js +29 -0
- package/commands/cmd-replay.js +65 -0
- package/commands/cmd-status.js +70 -0
- package/commands/cmd-subscribe.js +102 -0
- package/install.mjs +83 -0
- package/lib/config.js +87 -0
- package/lib/db.js +72 -0
- package/lib/emit.js +111 -0
- package/lib/errors.js +45 -0
- package/lib/index.cjs +20 -0
- package/lib/index.js +13 -0
- package/lib/paths.js +69 -0
- package/lib/poll.js +212 -0
- package/lib/register.js +164 -0
- package/lib/schema.sql +68 -0
- package/lib/sweep.js +71 -0
- package/lib/validate.js +156 -0
- package/package.json +56 -0
- package/scripts/postinstall.js +14 -0
- package/skills/wicked-bus/emit/SKILL.md +147 -0
- package/skills/wicked-bus/init/SKILL.md +94 -0
- package/skills/wicked-bus/naming/SKILL.md +151 -0
- package/skills/wicked-bus/query/SKILL.md +177 -0
- package/skills/wicked-bus/subscribe/SKILL.md +164 -0
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
```
|
|
2
|
+
██╗ ██╗██╗ ██████╗██╗ ██╗███████╗██████╗ ██████╗ ██╗ ██╗███████╗
|
|
3
|
+
██║ ██║██║██╔════╝██║ ██╔╝██╔════╝██╔══██╗ ██╔══██╗██║ ██║██╔════╝
|
|
4
|
+
██║ █╗ ██║██║██║ █████╔╝ █████╗ ██║ ██║ ██████╔╝██║ ██║███████╗
|
|
5
|
+
██║███╗██║██║██║ ██╔═██╗ ██╔══╝ ██║ ██║ ██╔══██╗██║ ██║╚════██║
|
|
6
|
+
╚███╔███╔╝██║╚██████╗██║ ██╗███████╗██████╔╝ ██████╔╝╚██████╔╝███████║
|
|
7
|
+
╚══╝╚══╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
A lightweight, local-first event bus for AI agents and developer tools.
|
|
11
|
+
|
|
12
|
+
SQLite-backed, single-host, poll-based delivery with at-least-once semantics. No servers, no network transport, no infrastructure. Events stay on your machine.
|
|
13
|
+
|
|
14
|
+
Built for agent ecosystems where multiple tools need to communicate without coupling to each other — AI coding assistants, test runners, knowledge systems, deployment tools, or anything that benefits from local event-driven architecture.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install wicked-bus
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`better-sqlite3` is a required peer dependency (compiles a native addon).
|
|
25
|
+
|
|
26
|
+
### Initialize
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
wicked-bus init
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Creates `~/.something-wicked/wicked-bus/` with a WAL-mode SQLite database.
|
|
33
|
+
|
|
34
|
+
### Emit an event
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
wicked-bus emit \
|
|
38
|
+
--type wicked.task.completed \
|
|
39
|
+
--domain my-plugin \
|
|
40
|
+
--payload '{"taskId": "abc", "status": "done"}'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Subscribe to events
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
wicked-bus subscribe --filter 'wicked.task.*'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Streams events as NDJSON. Use `--filter` with wildcards and `@domain` scoping.
|
|
50
|
+
|
|
51
|
+
## Programmatic API
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
import { emit, poll, ack, register } from 'wicked-bus';
|
|
55
|
+
import { loadConfig } from 'wicked-bus/lib/config.js';
|
|
56
|
+
import { openDb } from 'wicked-bus/lib/db.js';
|
|
57
|
+
|
|
58
|
+
const config = loadConfig();
|
|
59
|
+
const db = openDb(config);
|
|
60
|
+
|
|
61
|
+
// Emit
|
|
62
|
+
const result = emit(db, config, {
|
|
63
|
+
event_type: 'wicked.deploy.completed',
|
|
64
|
+
domain: 'my-deploy',
|
|
65
|
+
subdomain: 'deploy.production',
|
|
66
|
+
payload: { version: '2.0.0' },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Subscribe
|
|
70
|
+
const sub = register(db, {
|
|
71
|
+
plugin: 'my-consumer',
|
|
72
|
+
role: 'subscriber',
|
|
73
|
+
event_type_filter: 'wicked.deploy.*',
|
|
74
|
+
cursor_init: 'latest',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Poll
|
|
78
|
+
const events = poll(db, config, {
|
|
79
|
+
cursor_id: sub.cursor_id,
|
|
80
|
+
filter: 'wicked.deploy.*',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Acknowledge
|
|
84
|
+
if (events.events.length > 0) {
|
|
85
|
+
const lastId = events.events.at(-1).event_id;
|
|
86
|
+
ack(db, { cursor_id: sub.cursor_id, event_id: lastId });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
db.close();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## CLI Commands
|
|
93
|
+
|
|
94
|
+
| Command | Description |
|
|
95
|
+
|---------|-------------|
|
|
96
|
+
| `init` | Create data directory and database |
|
|
97
|
+
| `emit` | Publish an event |
|
|
98
|
+
| `subscribe` | Stream events matching a filter |
|
|
99
|
+
| `status` | Show bus health and stats |
|
|
100
|
+
| `register` | Register as provider or subscriber |
|
|
101
|
+
| `deregister` | Soft-delete a registration |
|
|
102
|
+
| `list` | List registrations |
|
|
103
|
+
| `ack` | Acknowledge events (advance cursor) |
|
|
104
|
+
| `replay` | Reset a cursor to a specific position |
|
|
105
|
+
| `cleanup` | Run TTL sweep (delete expired events) |
|
|
106
|
+
|
|
107
|
+
All commands output structured JSON. Errors go to stderr with error codes (WB-001 through WB-006).
|
|
108
|
+
|
|
109
|
+
## AI CLI Skills
|
|
110
|
+
|
|
111
|
+
wicked-bus ships skills for AI coding assistants (Claude, Gemini, Copilot, Codex, Cursor).
|
|
112
|
+
|
|
113
|
+
### Install skills
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx wicked-bus-install
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Auto-detects installed CLIs and copies skills. Available skills:
|
|
120
|
+
|
|
121
|
+
| Skill | Purpose |
|
|
122
|
+
|-------|---------|
|
|
123
|
+
| `wicked-bus/init` | Initialize or connect to the bus |
|
|
124
|
+
| `wicked-bus/emit` | Publish events |
|
|
125
|
+
| `wicked-bus/subscribe` | Consume events |
|
|
126
|
+
| `wicked-bus/naming` | Event naming conventions |
|
|
127
|
+
| `wicked-bus/query` | Query and debug |
|
|
128
|
+
|
|
129
|
+
## Why wicked-bus?
|
|
130
|
+
|
|
131
|
+
Agent ecosystems have a communication problem. Tools that should work together — test runners, code reviewers, knowledge systems, deployment pipelines — end up tightly coupled or completely siloed. wicked-bus solves this with a dead-simple local event bridge.
|
|
132
|
+
|
|
133
|
+
- **Local-first**: everything lives in a single SQLite file. No servers to run, no ports to manage, no infrastructure.
|
|
134
|
+
- **At-least-once delivery**: cursors persist across restarts. Unacked events are re-delivered. No lost events.
|
|
135
|
+
- **Fire-and-forget**: producers are non-blocking. The bus never slows the caller. If it's not installed, callers degrade gracefully.
|
|
136
|
+
- **Agent-native**: designed for AI coding assistants and the tools around them. Ships with skills for Claude, Gemini, Copilot, Codex, and Cursor.
|
|
137
|
+
- **Two-timer TTL**: events auto-expire. No manual cleanup, no unbounded growth.
|
|
138
|
+
|
|
139
|
+
## Documentation
|
|
140
|
+
|
|
141
|
+
- [ARCHITECTURE.md](./ARCHITECTURE.md) -- system design and module structure
|
|
142
|
+
- [USERS_GUIDE.md](./USERS_GUIDE.md) -- event naming, payload conventions, integration patterns
|
|
143
|
+
- [reqs/SPEC.md](./reqs/SPEC.md) -- full specification
|
|
144
|
+
|
|
145
|
+
## Requirements
|
|
146
|
+
|
|
147
|
+
- Node.js >= 18.0.0
|
|
148
|
+
- `better-sqlite3` >= 9.0.0 (peer dependency)
|
|
149
|
+
- macOS, Linux, or Windows
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT
|
package/commands/cli.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* wicked-bus CLI entry point.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { WBError, EXIT_CODES } from '../lib/errors.js';
|
|
8
|
+
|
|
9
|
+
// Argument parser
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
const args = {};
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
if (argv[i].startsWith('--')) {
|
|
14
|
+
const key = argv[i].slice(2);
|
|
15
|
+
if (i + 1 >= argv.length || argv[i + 1].startsWith('--')) {
|
|
16
|
+
args[key] = true;
|
|
17
|
+
} else {
|
|
18
|
+
args[key] = argv[++i];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return args;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function printUsage() {
|
|
26
|
+
const usage = {
|
|
27
|
+
usage: 'wicked-bus <command> [options]',
|
|
28
|
+
commands: [
|
|
29
|
+
'init', 'emit', 'subscribe', 'status', 'replay',
|
|
30
|
+
'cleanup', 'register', 'deregister', 'list', 'ack',
|
|
31
|
+
],
|
|
32
|
+
global_flags: ['--db-path <path>', '--json', '--log-level <level>'],
|
|
33
|
+
};
|
|
34
|
+
process.stdout.write(JSON.stringify(usage, null, 2) + '\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function handleError(err) {
|
|
38
|
+
if (err instanceof WBError) {
|
|
39
|
+
process.stderr.write(JSON.stringify(err.toJSON()) + '\n');
|
|
40
|
+
process.exit(EXIT_CODES[err.error] || 1);
|
|
41
|
+
}
|
|
42
|
+
process.stderr.write(JSON.stringify({
|
|
43
|
+
error: 'UNKNOWN',
|
|
44
|
+
code: 'INTERNAL_ERROR',
|
|
45
|
+
message: err.message,
|
|
46
|
+
}) + '\n');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function main() {
|
|
51
|
+
const argv = process.argv.slice(2);
|
|
52
|
+
const command = argv[0];
|
|
53
|
+
const flagArgv = argv.slice(1);
|
|
54
|
+
const args = parseArgs(flagArgv);
|
|
55
|
+
|
|
56
|
+
// Extract global flags
|
|
57
|
+
const globals = {
|
|
58
|
+
db_path: args['db-path'] || null,
|
|
59
|
+
json: args.json !== false,
|
|
60
|
+
log_level: args['log-level'] || null,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Remove global flags from args
|
|
64
|
+
delete args['db-path'];
|
|
65
|
+
delete args.json;
|
|
66
|
+
delete args['log-level'];
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
switch (command) {
|
|
70
|
+
case 'init': {
|
|
71
|
+
const { cmdInit } = await import('./cmd-init.js');
|
|
72
|
+
await cmdInit(args, globals);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case 'emit': {
|
|
76
|
+
const { cmdEmit } = await import('./cmd-emit.js');
|
|
77
|
+
await cmdEmit(args, globals);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case 'subscribe': {
|
|
81
|
+
const { cmdSubscribe } = await import('./cmd-subscribe.js');
|
|
82
|
+
await cmdSubscribe(args, globals);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case 'status': {
|
|
86
|
+
const { cmdStatus } = await import('./cmd-status.js');
|
|
87
|
+
await cmdStatus(args, globals);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case 'replay': {
|
|
91
|
+
const { cmdReplay } = await import('./cmd-replay.js');
|
|
92
|
+
await cmdReplay(args, globals);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case 'cleanup': {
|
|
96
|
+
const { cmdCleanup } = await import('./cmd-cleanup.js');
|
|
97
|
+
await cmdCleanup(args, globals);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case 'register': {
|
|
101
|
+
const { cmdRegister } = await import('./cmd-register.js');
|
|
102
|
+
await cmdRegister(args, globals);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case 'deregister': {
|
|
106
|
+
const { cmdDeregister } = await import('./cmd-deregister.js');
|
|
107
|
+
await cmdDeregister(args, globals);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case 'list': {
|
|
111
|
+
const { cmdList } = await import('./cmd-list.js');
|
|
112
|
+
await cmdList(args, globals);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case 'ack': {
|
|
116
|
+
const { cmdAck } = await import('./cmd-ack.js');
|
|
117
|
+
await cmdAck(args, globals);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
default:
|
|
121
|
+
printUsage();
|
|
122
|
+
process.exit(command ? 1 : 0);
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
handleError(err);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
main();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wicked-bus ack command.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { loadConfig } from '../lib/config.js';
|
|
6
|
+
import { openDb } from '../lib/db.js';
|
|
7
|
+
import { ack } from '../lib/poll.js';
|
|
8
|
+
|
|
9
|
+
export async function cmdAck(args, globals) {
|
|
10
|
+
const configOverrides = {};
|
|
11
|
+
if (globals.db_path) configOverrides.db_path = globals.db_path;
|
|
12
|
+
if (globals.log_level) configOverrides.log_level = globals.log_level;
|
|
13
|
+
|
|
14
|
+
const config = loadConfig(configOverrides);
|
|
15
|
+
const db = openDb(config);
|
|
16
|
+
|
|
17
|
+
const cursorId = args['cursor-id'];
|
|
18
|
+
const lastEventId = Number(args['last-event-id']);
|
|
19
|
+
|
|
20
|
+
if (!cursorId) {
|
|
21
|
+
throw new Error('--cursor-id is required');
|
|
22
|
+
}
|
|
23
|
+
if (isNaN(lastEventId)) {
|
|
24
|
+
throw new Error('--last-event-id must be a number');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = ack(db, cursorId, lastEventId);
|
|
28
|
+
db.close();
|
|
29
|
+
|
|
30
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
31
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wicked-bus cleanup command -- sweep expired events.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { loadConfig } from '../lib/config.js';
|
|
6
|
+
import { openDb } from '../lib/db.js';
|
|
7
|
+
import { runSweep } from '../lib/sweep.js';
|
|
8
|
+
|
|
9
|
+
export async function cmdCleanup(args, globals) {
|
|
10
|
+
const configOverrides = {};
|
|
11
|
+
if (globals.db_path) configOverrides.db_path = globals.db_path;
|
|
12
|
+
if (globals.log_level) configOverrides.log_level = globals.log_level;
|
|
13
|
+
|
|
14
|
+
const config = loadConfig(configOverrides);
|
|
15
|
+
|
|
16
|
+
// --archive flag overrides config
|
|
17
|
+
if (args.archive === true) {
|
|
18
|
+
config.archive_mode = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const db = openDb(config);
|
|
22
|
+
const dryRun = args['dry-run'] === true;
|
|
23
|
+
|
|
24
|
+
if (dryRun) {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const count = db.prepare(
|
|
27
|
+
'SELECT COUNT(*) as count FROM events WHERE dedup_expires_at < ?'
|
|
28
|
+
).get(now);
|
|
29
|
+
|
|
30
|
+
const result = {
|
|
31
|
+
events_deleted: count.count,
|
|
32
|
+
dry_run: true,
|
|
33
|
+
};
|
|
34
|
+
db.close();
|
|
35
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const result = runSweep(db, config);
|
|
40
|
+
result.dry_run = false;
|
|
41
|
+
|
|
42
|
+
db.close();
|
|
43
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wicked-bus deregister command.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { loadConfig } from '../lib/config.js';
|
|
6
|
+
import { openDb } from '../lib/db.js';
|
|
7
|
+
import { deregister } from '../lib/register.js';
|
|
8
|
+
|
|
9
|
+
export async function cmdDeregister(args, globals) {
|
|
10
|
+
const configOverrides = {};
|
|
11
|
+
if (globals.db_path) configOverrides.db_path = globals.db_path;
|
|
12
|
+
if (globals.log_level) configOverrides.log_level = globals.log_level;
|
|
13
|
+
|
|
14
|
+
const config = loadConfig(configOverrides);
|
|
15
|
+
const db = openDb(config);
|
|
16
|
+
|
|
17
|
+
const subscriptionId = args['subscription-id'];
|
|
18
|
+
if (!subscriptionId) {
|
|
19
|
+
throw new Error('--subscription-id is required');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = deregister(db, subscriptionId);
|
|
23
|
+
db.close();
|
|
24
|
+
|
|
25
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
26
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wicked-bus emit command.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { loadConfig } from '../lib/config.js';
|
|
7
|
+
import { openDb } from '../lib/db.js';
|
|
8
|
+
import { emit } from '../lib/emit.js';
|
|
9
|
+
|
|
10
|
+
export async function cmdEmit(args, globals) {
|
|
11
|
+
const configOverrides = {};
|
|
12
|
+
if (globals.db_path) configOverrides.db_path = globals.db_path;
|
|
13
|
+
if (globals.log_level) configOverrides.log_level = globals.log_level;
|
|
14
|
+
|
|
15
|
+
const config = loadConfig(configOverrides);
|
|
16
|
+
const db = openDb(config);
|
|
17
|
+
|
|
18
|
+
// Parse payload -- support @file syntax
|
|
19
|
+
let payload = args.payload;
|
|
20
|
+
if (typeof payload === 'string' && payload.startsWith('@')) {
|
|
21
|
+
const filePath = payload.slice(1);
|
|
22
|
+
payload = readFileSync(filePath, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Parse payload as JSON if it's a string
|
|
26
|
+
if (typeof payload === 'string') {
|
|
27
|
+
try {
|
|
28
|
+
payload = JSON.parse(payload);
|
|
29
|
+
} catch (_) {
|
|
30
|
+
// Will be caught by validation
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const event = {
|
|
35
|
+
event_type: args.type,
|
|
36
|
+
domain: args.domain,
|
|
37
|
+
subdomain: args.subdomain || '',
|
|
38
|
+
payload,
|
|
39
|
+
schema_version: args['schema-version'] || undefined,
|
|
40
|
+
idempotency_key: args['idempotency-key'] || undefined,
|
|
41
|
+
metadata: args.metadata ? JSON.parse(args.metadata) : undefined,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (args['ttl-hours'] != null) {
|
|
45
|
+
event.ttl_hours = Number(args['ttl-hours']);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = emit(db, config, event);
|
|
49
|
+
db.close();
|
|
50
|
+
|
|
51
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
52
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wicked-bus init command.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ensureDataDir, resolveDbPath } from '../lib/paths.js';
|
|
6
|
+
import { loadConfig, writeDefaultConfig } from '../lib/config.js';
|
|
7
|
+
import { openDb } from '../lib/db.js';
|
|
8
|
+
|
|
9
|
+
export async function cmdInit(args, globals) {
|
|
10
|
+
const dataDir = ensureDataDir();
|
|
11
|
+
const configOverrides = {};
|
|
12
|
+
if (globals.db_path) configOverrides.db_path = globals.db_path;
|
|
13
|
+
if (globals.log_level) configOverrides.log_level = globals.log_level;
|
|
14
|
+
|
|
15
|
+
// Write default config (won't overwrite unless --force)
|
|
16
|
+
writeDefaultConfig(dataDir, args.force === true);
|
|
17
|
+
|
|
18
|
+
const config = loadConfig(configOverrides);
|
|
19
|
+
const db = openDb(config);
|
|
20
|
+
const dbPath = resolveDbPath(config);
|
|
21
|
+
db.close();
|
|
22
|
+
|
|
23
|
+
const result = {
|
|
24
|
+
initialized: true,
|
|
25
|
+
data_dir: dataDir,
|
|
26
|
+
db_path: dbPath,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wicked-bus list command.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { loadConfig } from '../lib/config.js';
|
|
6
|
+
import { openDb } from '../lib/db.js';
|
|
7
|
+
|
|
8
|
+
export async function cmdList(args, globals) {
|
|
9
|
+
const configOverrides = {};
|
|
10
|
+
if (globals.db_path) configOverrides.db_path = globals.db_path;
|
|
11
|
+
if (globals.log_level) configOverrides.log_level = globals.log_level;
|
|
12
|
+
|
|
13
|
+
const config = loadConfig(configOverrides);
|
|
14
|
+
const db = openDb(config);
|
|
15
|
+
|
|
16
|
+
let sql = 'SELECT * FROM subscriptions';
|
|
17
|
+
const conditions = [];
|
|
18
|
+
const params = {};
|
|
19
|
+
|
|
20
|
+
if (args.role) {
|
|
21
|
+
conditions.push('role = :role');
|
|
22
|
+
params.role = args.role;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!args['include-deregistered']) {
|
|
26
|
+
conditions.push('deregistered_at IS NULL');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (conditions.length > 0) {
|
|
30
|
+
sql += ' WHERE ' + conditions.join(' AND ');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
sql += ' ORDER BY registered_at DESC';
|
|
34
|
+
|
|
35
|
+
const rows = db.prepare(sql).all(params);
|
|
36
|
+
db.close();
|
|
37
|
+
|
|
38
|
+
process.stdout.write(JSON.stringify(rows) + '\n');
|
|
39
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wicked-bus register command.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { loadConfig } from '../lib/config.js';
|
|
6
|
+
import { openDb } from '../lib/db.js';
|
|
7
|
+
import { register } from '../lib/register.js';
|
|
8
|
+
|
|
9
|
+
export async function cmdRegister(args, globals) {
|
|
10
|
+
const configOverrides = {};
|
|
11
|
+
if (globals.db_path) configOverrides.db_path = globals.db_path;
|
|
12
|
+
if (globals.log_level) configOverrides.log_level = globals.log_level;
|
|
13
|
+
|
|
14
|
+
const config = loadConfig(configOverrides);
|
|
15
|
+
const db = openDb(config);
|
|
16
|
+
|
|
17
|
+
const opts = {
|
|
18
|
+
plugin: args.plugin,
|
|
19
|
+
role: args.role,
|
|
20
|
+
filter: args.events || args.filter || '',
|
|
21
|
+
schema_version: args['schema-version'] || undefined,
|
|
22
|
+
cursor_init: args['cursor-init'] || 'latest',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const result = register(db, opts);
|
|
26
|
+
db.close();
|
|
27
|
+
|
|
28
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
29
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wicked-bus replay command -- reset cursor to a specific event ID.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { loadConfig } from '../lib/config.js';
|
|
6
|
+
import { openDb } from '../lib/db.js';
|
|
7
|
+
import { WBError } from '../lib/errors.js';
|
|
8
|
+
|
|
9
|
+
export async function cmdReplay(args, globals) {
|
|
10
|
+
const configOverrides = {};
|
|
11
|
+
if (globals.db_path) configOverrides.db_path = globals.db_path;
|
|
12
|
+
if (globals.log_level) configOverrides.log_level = globals.log_level;
|
|
13
|
+
|
|
14
|
+
const config = loadConfig(configOverrides);
|
|
15
|
+
const db = openDb(config);
|
|
16
|
+
|
|
17
|
+
const cursorId = args['cursor-id'];
|
|
18
|
+
const fromEventId = Number(args['from-event-id']);
|
|
19
|
+
|
|
20
|
+
if (!cursorId) {
|
|
21
|
+
throw new Error('--cursor-id is required');
|
|
22
|
+
}
|
|
23
|
+
if (isNaN(fromEventId)) {
|
|
24
|
+
throw new Error('--from-event-id must be a number');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Verify cursor exists
|
|
28
|
+
const cursor = db.prepare(
|
|
29
|
+
'SELECT * FROM cursors WHERE cursor_id = ? AND deregistered_at IS NULL'
|
|
30
|
+
).get(cursorId);
|
|
31
|
+
|
|
32
|
+
if (!cursor) {
|
|
33
|
+
throw new WBError('WB-006', 'CURSOR_NOT_FOUND', {
|
|
34
|
+
message: `Cursor not found: ${cursorId}`,
|
|
35
|
+
cursor_id: cursorId,
|
|
36
|
+
reason: 'cursor not found or deregistered',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check that from_event_id is not below the oldest available event
|
|
41
|
+
const oldest = db.prepare('SELECT MIN(event_id) as min_id FROM events').get();
|
|
42
|
+
if (oldest && oldest.min_id != null && fromEventId < oldest.min_id) {
|
|
43
|
+
throw new WBError('WB-003', 'CURSOR_BEHIND_TTL_WINDOW', {
|
|
44
|
+
message: `from_event_id ${fromEventId} is below the oldest available event (${oldest.min_id})`,
|
|
45
|
+
cursor_last_event_id: cursor.last_event_id,
|
|
46
|
+
oldest_available_event_id: oldest.min_id,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Reset cursor to from_event_id - 1
|
|
51
|
+
const resetTo = fromEventId - 1;
|
|
52
|
+
db.prepare(
|
|
53
|
+
'UPDATE cursors SET last_event_id = ? WHERE cursor_id = ?'
|
|
54
|
+
).run(resetTo, cursorId);
|
|
55
|
+
|
|
56
|
+
const result = {
|
|
57
|
+
replayed: true,
|
|
58
|
+
cursor_id: cursorId,
|
|
59
|
+
reset_to: resetTo,
|
|
60
|
+
from_event_id: fromEventId,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
db.close();
|
|
64
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
65
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wicked-bus status command.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { loadConfig } from '../lib/config.js';
|
|
6
|
+
import { openDb } from '../lib/db.js';
|
|
7
|
+
import { resolveDbPath } from '../lib/paths.js';
|
|
8
|
+
|
|
9
|
+
export async function cmdStatus(args, globals) {
|
|
10
|
+
const configOverrides = {};
|
|
11
|
+
if (globals.db_path) configOverrides.db_path = globals.db_path;
|
|
12
|
+
if (globals.log_level) configOverrides.log_level = globals.log_level;
|
|
13
|
+
|
|
14
|
+
const config = loadConfig(configOverrides);
|
|
15
|
+
const db = openDb(config);
|
|
16
|
+
const dbPath = resolveDbPath(config);
|
|
17
|
+
|
|
18
|
+
const totalEvents = db.prepare('SELECT COUNT(*) as count FROM events').get().count;
|
|
19
|
+
const oldest = db.prepare('SELECT MIN(event_id) as min_id FROM events').get();
|
|
20
|
+
const newest = db.prepare('SELECT MAX(event_id) as max_id FROM events').get();
|
|
21
|
+
|
|
22
|
+
// Events by type
|
|
23
|
+
const byType = db.prepare(
|
|
24
|
+
'SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type'
|
|
25
|
+
).all();
|
|
26
|
+
const eventsByType = {};
|
|
27
|
+
for (const row of byType) {
|
|
28
|
+
eventsByType[row.event_type] = row.count;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Subscribers with lag
|
|
32
|
+
const subscribers = db.prepare(`
|
|
33
|
+
SELECT s.subscription_id, s.plugin, s.event_type_filter,
|
|
34
|
+
c.cursor_id, c.last_event_id, c.acked_at
|
|
35
|
+
FROM subscriptions s
|
|
36
|
+
JOIN cursors c ON c.subscription_id = s.subscription_id
|
|
37
|
+
WHERE s.role = 'subscriber' AND s.deregistered_at IS NULL AND c.deregistered_at IS NULL
|
|
38
|
+
`).all();
|
|
39
|
+
|
|
40
|
+
const newestId = newest.max_id || 0;
|
|
41
|
+
const subscriberList = subscribers.map(s => ({
|
|
42
|
+
subscription_id: s.subscription_id,
|
|
43
|
+
plugin: s.plugin,
|
|
44
|
+
filter: s.event_type_filter,
|
|
45
|
+
cursor_id: s.cursor_id,
|
|
46
|
+
last_event_id: s.last_event_id,
|
|
47
|
+
lag: newestId - s.last_event_id,
|
|
48
|
+
acked_at: s.acked_at,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// Providers
|
|
52
|
+
const providers = db.prepare(`
|
|
53
|
+
SELECT subscription_id, plugin, event_type_filter, schema_version
|
|
54
|
+
FROM subscriptions
|
|
55
|
+
WHERE role = 'provider' AND deregistered_at IS NULL
|
|
56
|
+
`).all();
|
|
57
|
+
|
|
58
|
+
const result = {
|
|
59
|
+
db_path: dbPath,
|
|
60
|
+
total_events: totalEvents,
|
|
61
|
+
oldest_event_id: oldest.min_id || null,
|
|
62
|
+
newest_event_id: newest.max_id || null,
|
|
63
|
+
events_by_type: eventsByType,
|
|
64
|
+
subscribers: subscriberList,
|
|
65
|
+
providers,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
db.close();
|
|
69
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
70
|
+
}
|