@sigil-dev/plugin-health 0.9.2 → 0.9.3
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 +131 -131
- package/index.ts +150 -148
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
# @sigil-dev/plugin-health
|
|
2
|
-
|
|
3
|
-
Adds a `/health` endpoint to your Grimoire application.
|
|
4
|
-
In coordinator mode it fans out to all workers and aggregates their status, respecting your names defined in the namepool plugin
|
|
5
|
-
In worker mode it reports local uptime, version, and any custom fields you provide.
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
|
|
9
|
-
```sh
|
|
10
|
-
bun add @sigil-dev/plugin-health
|
|
11
|
-
```
|
|
12
|
-
## Why?
|
|
13
|
-
Unified health endpoint format allows for easier library development, for example a health -> Discord webhook or a
|
|
14
|
-
headless status page
|
|
15
|
-
|
|
16
|
-
## Usage
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
import { defineConfig } from "@sigil-dev/grimoire";
|
|
20
|
-
import { healthPlugin } from "@sigil-dev/plugin-health";
|
|
21
|
-
|
|
22
|
-
export default defineConfig({
|
|
23
|
-
plugins: [
|
|
24
|
-
healthPlugin(),
|
|
25
|
-
],
|
|
26
|
-
});
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Hit `/health` to get a full cluster report:
|
|
30
|
-
|
|
31
|
-
```json
|
|
32
|
-
{
|
|
33
|
-
"ok": true,
|
|
34
|
-
"version": "1.0.0",
|
|
35
|
-
"uptime": 3600,
|
|
36
|
-
"cluster": [
|
|
37
|
-
{ "name": "api-0", "status": "up", "pid": 1234, "version": "1.0.0", "lastSeen": 1718000000000 },
|
|
38
|
-
{ "name": "ws-0", "status": "up", "pid": 1235, "version": "1.0.0", "lastSeen": 1718000000000 }
|
|
39
|
-
]
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Returns `200` when all workers are up or reserved, `503` when any worker is down.
|
|
44
|
-
|
|
45
|
-
## Options
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
interface HealthPluginOptions {
|
|
49
|
-
/** Route to serve health at. Default: "/health" */
|
|
50
|
-
path?: string;
|
|
51
|
-
/** Extra fields to include in the response. */
|
|
52
|
-
extend?: () => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
53
|
-
/** Worker names to track. Auto-detected from plugin-namepool if present. */
|
|
54
|
-
names?: string[];
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Custom fields
|
|
59
|
-
|
|
60
|
-
Use `extend` to add dependency health checks or any other metadata:
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
healthPlugin({
|
|
64
|
-
extend: async () => ({
|
|
65
|
-
db: await checkPostgres(),
|
|
66
|
-
redis: await checkRedis(),
|
|
67
|
-
version_sha: process.env.GIT_SHA,
|
|
68
|
-
}),
|
|
69
|
-
})
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
## Worker status values
|
|
73
|
-
|
|
74
|
-
| Status | Meaning |
|
|
75
|
-
|---|---|
|
|
76
|
-
| `"up"` | Worker is running and responding. |
|
|
77
|
-
| `"down"` | Worker exists in coordinator but did not respond within 2 seconds. |
|
|
78
|
-
| `"reserved"` | Name is in the pool but no worker slot is configured for it. |
|
|
79
|
-
|
|
80
|
-
`"reserved"` workers do not fail the health check — they indicate a name is allocated but not yet deployed.
|
|
81
|
-
|
|
82
|
-
## Response shape
|
|
83
|
-
|
|
84
|
-
### Coordinator (`/health`)
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
interface HealthResponse {
|
|
88
|
-
ok: boolean;
|
|
89
|
-
version: string;
|
|
90
|
-
uptime: number; // seconds since coordinator start
|
|
91
|
-
cluster: WorkerHealth[];
|
|
92
|
-
[key: string]: unknown; // any fields added via extend()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
interface WorkerHealth {
|
|
96
|
-
name: string;
|
|
97
|
-
status: "up" | "down" | "reserved";
|
|
98
|
-
pid?: number;
|
|
99
|
-
version: string;
|
|
100
|
-
lastSeen: number | null; // null if the worker never started
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Worker (`/health`)
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
{
|
|
108
|
-
ok: true,
|
|
109
|
-
version: "1.0.0",
|
|
110
|
-
uptime: 120,
|
|
111
|
-
instanceId: "alpha",
|
|
112
|
-
pid: 1234,
|
|
113
|
-
// ...any fields from extend()
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
## Integration with plugin-namepool
|
|
118
|
-
|
|
119
|
-
When `@sigil-dev/plugin-namepool` is also registered, `plugin-health` automatically detects it via the plugin registry and uses the name pool to report `reserved` workers. No configuration needed.
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
import { namepool } from "@sigil-dev/plugin-namepool";
|
|
123
|
-
import { healthPlugin } from "@sigil-dev/plugin-health";
|
|
124
|
-
|
|
125
|
-
export default defineConfig({
|
|
126
|
-
plugins: [
|
|
127
|
-
namepool({ names: ["alpha", "beta", "gamma"] }),
|
|
128
|
-
healthPlugin(),
|
|
129
|
-
],
|
|
130
|
-
scale: { api: 2 }, // only 2 workers — "gamma" shows as reserved
|
|
131
|
-
});
|
|
1
|
+
# @sigil-dev/plugin-health
|
|
2
|
+
|
|
3
|
+
Adds a `/health` endpoint to your Grimoire application.
|
|
4
|
+
In coordinator mode it fans out to all workers and aggregates their status, respecting your names defined in the namepool plugin
|
|
5
|
+
In worker mode it reports local uptime, version, and any custom fields you provide.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun add @sigil-dev/plugin-health
|
|
11
|
+
```
|
|
12
|
+
## Why?
|
|
13
|
+
Unified health endpoint format allows for easier library development, for example a health -> Discord webhook or a
|
|
14
|
+
headless status page
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { defineConfig } from "@sigil-dev/grimoire";
|
|
20
|
+
import { healthPlugin } from "@sigil-dev/plugin-health";
|
|
21
|
+
|
|
22
|
+
export default defineConfig({
|
|
23
|
+
plugins: [
|
|
24
|
+
healthPlugin(),
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Hit `/health` to get a full cluster report:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"ok": true,
|
|
34
|
+
"version": "1.0.0",
|
|
35
|
+
"uptime": 3600,
|
|
36
|
+
"cluster": [
|
|
37
|
+
{ "name": "api-0", "status": "up", "pid": 1234, "version": "1.0.0", "lastSeen": 1718000000000 },
|
|
38
|
+
{ "name": "ws-0", "status": "up", "pid": 1235, "version": "1.0.0", "lastSeen": 1718000000000 }
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Returns `200` when all workers are up or reserved, `503` when any worker is down.
|
|
44
|
+
|
|
45
|
+
## Options
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
interface HealthPluginOptions {
|
|
49
|
+
/** Route to serve health at. Default: "/health" */
|
|
50
|
+
path?: string;
|
|
51
|
+
/** Extra fields to include in the response. */
|
|
52
|
+
extend?: () => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
53
|
+
/** Worker names to track. Auto-detected from plugin-namepool if present. */
|
|
54
|
+
names?: string[];
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Custom fields
|
|
59
|
+
|
|
60
|
+
Use `extend` to add dependency health checks or any other metadata:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
healthPlugin({
|
|
64
|
+
extend: async () => ({
|
|
65
|
+
db: await checkPostgres(),
|
|
66
|
+
redis: await checkRedis(),
|
|
67
|
+
version_sha: process.env.GIT_SHA,
|
|
68
|
+
}),
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Worker status values
|
|
73
|
+
|
|
74
|
+
| Status | Meaning |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `"up"` | Worker is running and responding. |
|
|
77
|
+
| `"down"` | Worker exists in coordinator but did not respond within 2 seconds. |
|
|
78
|
+
| `"reserved"` | Name is in the pool but no worker slot is configured for it. |
|
|
79
|
+
|
|
80
|
+
`"reserved"` workers do not fail the health check — they indicate a name is allocated but not yet deployed.
|
|
81
|
+
|
|
82
|
+
## Response shape
|
|
83
|
+
|
|
84
|
+
### Coordinator (`/health`)
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
interface HealthResponse {
|
|
88
|
+
ok: boolean;
|
|
89
|
+
version: string;
|
|
90
|
+
uptime: number; // seconds since coordinator start
|
|
91
|
+
cluster: WorkerHealth[];
|
|
92
|
+
[key: string]: unknown; // any fields added via extend()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface WorkerHealth {
|
|
96
|
+
name: string;
|
|
97
|
+
status: "up" | "down" | "reserved";
|
|
98
|
+
pid?: number;
|
|
99
|
+
version: string;
|
|
100
|
+
lastSeen: number | null; // null if the worker never started
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Worker (`/health`)
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
{
|
|
108
|
+
ok: true,
|
|
109
|
+
version: "1.0.0",
|
|
110
|
+
uptime: 120,
|
|
111
|
+
instanceId: "alpha",
|
|
112
|
+
pid: 1234,
|
|
113
|
+
// ...any fields from extend()
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Integration with plugin-namepool
|
|
118
|
+
|
|
119
|
+
When `@sigil-dev/plugin-namepool` is also registered, `plugin-health` automatically detects it via the plugin registry and uses the name pool to report `reserved` workers. No configuration needed.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { namepool } from "@sigil-dev/plugin-namepool";
|
|
123
|
+
import { healthPlugin } from "@sigil-dev/plugin-health";
|
|
124
|
+
|
|
125
|
+
export default defineConfig({
|
|
126
|
+
plugins: [
|
|
127
|
+
namepool({ names: ["alpha", "beta", "gamma"] }),
|
|
128
|
+
healthPlugin(),
|
|
129
|
+
],
|
|
130
|
+
scale: { api: 2 }, // only 2 workers — "gamma" shows as reserved
|
|
131
|
+
});
|
|
132
132
|
```
|
package/index.ts
CHANGED
|
@@ -1,148 +1,150 @@
|
|
|
1
|
-
import type { CoordinatorContext, GrimoirePlugin } from "@sigil-dev/grimoire";
|
|
2
|
-
|
|
3
|
-
export interface HealthPluginOptions {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type WorkerHealthStatus = "up" | "down" | "reserved";
|
|
13
|
-
|
|
14
|
-
export interface WorkerHealth {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
export interface HealthResponse {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function healthPlugin(
|
|
31
|
-
|
|
32
|
-
): GrimoirePlugin {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
1
|
+
import type { CoordinatorContext, GrimoirePlugin } from "@sigil-dev/grimoire";
|
|
2
|
+
|
|
3
|
+
export interface HealthPluginOptions {
|
|
4
|
+
/** Route to serve health at. Default: /health */
|
|
5
|
+
path?: string;
|
|
6
|
+
/** Extra fields to include in the response */
|
|
7
|
+
extend?: () => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
8
|
+
/** By default the plugin pulls names from @sigil-dev/plugin-namepool, but you can specify overrides.*/
|
|
9
|
+
names?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type WorkerHealthStatus = "up" | "down" | "reserved";
|
|
13
|
+
|
|
14
|
+
export interface WorkerHealth {
|
|
15
|
+
name: string;
|
|
16
|
+
status: WorkerHealthStatus;
|
|
17
|
+
pid?: number;
|
|
18
|
+
version: string;
|
|
19
|
+
lastSeen: number | null; // null = never existed
|
|
20
|
+
}
|
|
21
|
+
export interface HealthResponse {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
version: string;
|
|
24
|
+
uptime: number;
|
|
25
|
+
instanceId?: string;
|
|
26
|
+
cluster?: WorkerHealth[];
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function healthPlugin(
|
|
31
|
+
options: HealthPluginOptions = {},
|
|
32
|
+
): GrimoirePlugin {
|
|
33
|
+
const healthPath = options.path ?? "/health";
|
|
34
|
+
const startedAt = Date.now();
|
|
35
|
+
let names: string[] | undefined = options.names;
|
|
36
|
+
|
|
37
|
+
// set by onCoordinatorStart — stays null in worker context
|
|
38
|
+
let coordinatorCtx: CoordinatorContext | null = null;
|
|
39
|
+
|
|
40
|
+
// last successful contact per worker — lets a status page show
|
|
41
|
+
// "up but hasn't heartbeated recently" vs a fresh respawn
|
|
42
|
+
const lastSeen = new Map<string, number>();
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
name: "@sigil-dev/plugin-health",
|
|
46
|
+
|
|
47
|
+
onPluginsResolved(ctx) {
|
|
48
|
+
const pool = ctx.plugins.get("namepool") as any;
|
|
49
|
+
names ??= pool?.names;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
onCoordinatorStart(ctx) {
|
|
53
|
+
coordinatorCtx = ctx;
|
|
54
|
+
for (const w of ctx.workers) {
|
|
55
|
+
lastSeen.set(w.name ?? `${w.mode}-${w.index}`, Date.now());
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
onWorkerReady(worker) {
|
|
60
|
+
lastSeen.set(worker.name ?? `${worker.mode}-${worker.index}`, Date.now());
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async onRequest(req, next) {
|
|
64
|
+
const url = new URL(req.url);
|
|
65
|
+
if (url.pathname !== healthPath) return next();
|
|
66
|
+
|
|
67
|
+
// ── coordinator context: fan out to all workers ──
|
|
68
|
+
if (coordinatorCtx) {
|
|
69
|
+
const cluster: WorkerHealth[] = await Promise.all(
|
|
70
|
+
coordinatorCtx.workers.map(async (w): Promise<WorkerHealth> => {
|
|
71
|
+
const key = w.name ?? `${w.mode}-${w.index}`;
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(`${w.internalUrl}${healthPath}`, {
|
|
74
|
+
signal: AbortSignal.timeout(2_000),
|
|
75
|
+
headers: { "X-Grimoire-Internal": coordinatorCtx!.secret },
|
|
76
|
+
});
|
|
77
|
+
if (res.ok) {
|
|
78
|
+
lastSeen.set(key, Date.now());
|
|
79
|
+
const data = (await res.json()) as HealthResponse;
|
|
80
|
+
return {
|
|
81
|
+
name: key,
|
|
82
|
+
status: "up",
|
|
83
|
+
pid: w.pid,
|
|
84
|
+
version: data.version ?? "",
|
|
85
|
+
lastSeen: lastSeen.get(key)!,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
} catch {}
|
|
89
|
+
// didn't respond this poll — either mid-respawn (transient)
|
|
90
|
+
// or crash-looping (persistent). either way: down.
|
|
91
|
+
return {
|
|
92
|
+
name: key,
|
|
93
|
+
status: "down",
|
|
94
|
+
pid: w.pid,
|
|
95
|
+
version: "",
|
|
96
|
+
lastSeen: lastSeen.get(key) ?? 0,
|
|
97
|
+
};
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (names) {
|
|
102
|
+
const activeNames = new Set(cluster.map((w) => w.name));
|
|
103
|
+
for (const name of names) {
|
|
104
|
+
if (!activeNames.has(name)) {
|
|
105
|
+
cluster.push({
|
|
106
|
+
name,
|
|
107
|
+
status: "reserved",
|
|
108
|
+
version: "",
|
|
109
|
+
lastSeen: null,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const allOk = cluster.every(
|
|
115
|
+
(w) => w.status === "up" || w.status === "reserved",
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return Response.json(
|
|
119
|
+
{
|
|
120
|
+
ok: allOk,
|
|
121
|
+
version: process.env.npm_package_version ?? "0.0.0",
|
|
122
|
+
uptime: Math.floor((Date.now() - startedAt) / 1000),
|
|
123
|
+
cluster,
|
|
124
|
+
} satisfies HealthResponse,
|
|
125
|
+
{
|
|
126
|
+
status: allOk ? 200 : 503,
|
|
127
|
+
headers: { "Cache-Control": "no-store" },
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── worker context: report local status ──
|
|
133
|
+
const extra = options.extend ? await options.extend() : {};
|
|
134
|
+
return Response.json(
|
|
135
|
+
{
|
|
136
|
+
ok: true,
|
|
137
|
+
version: process.env.npm_package_version ?? "0.0.0",
|
|
138
|
+
uptime: Math.floor((Date.now() - startedAt) / 1000),
|
|
139
|
+
instanceId: process.env.GRIMOIRE_WORKER_NAME,
|
|
140
|
+
pid: process.pid,
|
|
141
|
+
...extra,
|
|
142
|
+
} satisfies HealthResponse,
|
|
143
|
+
{
|
|
144
|
+
status: 200,
|
|
145
|
+
headers: { "Cache-Control": "no-store" },
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@sigil-dev/grimoire": "0.9.
|
|
8
|
+
"@sigil-dev/grimoire": "0.9.3"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"@types/bun": "latest"
|
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
"peerDependencies": {
|
|
14
14
|
"typescript": "^5"
|
|
15
15
|
},
|
|
16
|
-
"version": "0.9.
|
|
16
|
+
"version": "0.9.3"
|
|
17
17
|
}
|