@muninmd/munin-sdk 0.0.0-stage → 1.1.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/LICENSE +21 -0
- package/README.md +315 -2
- package/pack-plugin.d.mts +4 -0
- package/pack-plugin.mjs +67 -0
- package/package.json +62 -4
- package/plugin-api.d.ts +779 -0
- package/plugin-package.d.ts +121 -0
- package/plugin-package.js +214 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aleksandr Eremin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,316 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @muninmd/munin-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Types and the packer for [Munin](https://munin.md) plugins — a local-first,
|
|
4
|
+
Markdown note-taking app for the desktop and Android.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install --save-dev @muninmd/munin-sdk
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import type { PluginContext } from '@muninmd/munin-sdk'
|
|
12
|
+
|
|
13
|
+
export default function activate(ctx: PluginContext): void {
|
|
14
|
+
ctx.commands.register({ id: 'hello', title: 'Say hello', run: () => ctx.ui.notice('Hello') })
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx munin-pack dist acme.hello-1.0.0.mnp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `@muninmd/munin-sdk` — the whole plugin API as TypeScript types (`PluginContext`
|
|
23
|
+
and everything it hands you). Types only: at run time the app passes the
|
|
24
|
+
context in.
|
|
25
|
+
- `@muninmd/munin-sdk/plugin-package` — the manifest and package rules, the same
|
|
26
|
+
code the app and the catalogue check packages with.
|
|
27
|
+
- `munin-pack` (also `@muninmd/munin-sdk/pack`) — packs a plugin folder into one
|
|
28
|
+
`.mnp` file and checks it.
|
|
29
|
+
|
|
30
|
+
The rest of this page is the guide to writing one.
|
|
31
|
+
|
|
32
|
+
# Writing a Munin plugin
|
|
33
|
+
|
|
34
|
+
A plugin is a folder with a `manifest.json` and a few text files. It is packed
|
|
35
|
+
into one file, `.mnp`, and installed from the marketplace, or from the file.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
my-plugin/
|
|
39
|
+
manifest.json
|
|
40
|
+
main.js the entry point — an ES module
|
|
41
|
+
styles.css optional
|
|
42
|
+
README.md optional; shown on the plugin's card
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## The manifest
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"id": "acme.word-count",
|
|
50
|
+
"name": "Word Count",
|
|
51
|
+
"version": "1.0.0",
|
|
52
|
+
"description": "Counts words in a note.",
|
|
53
|
+
"author": "acme",
|
|
54
|
+
"entry": "main.js",
|
|
55
|
+
"apiVersion": 1,
|
|
56
|
+
"minAppVersion": "0.4.1",
|
|
57
|
+
"platforms": ["desktop", "android"],
|
|
58
|
+
"permissions": ["vault:read"],
|
|
59
|
+
"tags": ["editor"]
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- `id` is `<author>.<name>`: lower case, digits, dashes. The part before the dot is
|
|
64
|
+
your **author handle**, which you register once (below), and it is the namespace
|
|
65
|
+
you own.
|
|
66
|
+
- `permissions` are `vault:read`, `vault:write`, `network`. They are shown to the
|
|
67
|
+
person before they switch the plugin on. **They disclose what you intend; they
|
|
68
|
+
are not a sandbox** — see "Trust".
|
|
69
|
+
- `platforms`: `desktop`, `android`, or both. One package runs on both.
|
|
70
|
+
- `dependencies` (optional): other plugins yours needs, with the oldest version of
|
|
71
|
+
each that will do — `{ "munin.daily-notes": "1.1.0" }`. They start before yours
|
|
72
|
+
and stop after it. When one is missing, off or too old, your plugin waits
|
|
73
|
+
(shown as such, with a button to fix it) instead of starting without it, and
|
|
74
|
+
the catalogue offers to install it along with yours.
|
|
75
|
+
|
|
76
|
+
## The code
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
export default function activate(ctx) {
|
|
80
|
+
ctx.commands.register({
|
|
81
|
+
id: 'hello',
|
|
82
|
+
title: 'Say hello',
|
|
83
|
+
run: () => ctx.ui.notice('Hello')
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function deactivate() {} // optional
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Everything you register through `ctx` is stamped with your plugin id and taken
|
|
91
|
+
back when the plugin is switched off. See `plugin-api.d.ts` in this package for the
|
|
92
|
+
whole surface, and `plugins/examples/todo-highlight` in the Munin repository for a
|
|
93
|
+
plugin that uses all of it. For the editor, build extensions from `ctx.editor.libs` — the app's own copy
|
|
94
|
+
of CodeMirror — and never from your own `@codemirror/*` import: two copies in one
|
|
95
|
+
window do not recognise each other's objects.
|
|
96
|
+
|
|
97
|
+
### Panels
|
|
98
|
+
|
|
99
|
+
`ctx.views.register({ id, title, icon, mount })` adds a panel; `mount(el,
|
|
100
|
+
instance)` fills `el` and may return a cleanup. `instance.state()` /
|
|
101
|
+
`instance.setState(value)` keep a JSON value for _that_ panel — two open panels
|
|
102
|
+
of your view keep two, and on a computer the value comes back with the layout.
|
|
103
|
+
On a phone a view needs `mobile: 'sheet'` (a sheet from a button in the library)
|
|
104
|
+
or `mobile: 'page'` (the whole screen, from the dock's menu) to be reachable at
|
|
105
|
+
all. `ctx.views.open(id)` shows one of your views, on either platform.
|
|
106
|
+
|
|
107
|
+
A view with `expandable: true` gets a control in its header that expands the panel
|
|
108
|
+
over the whole workspace on a computer; Escape, or the same control, puts the layout
|
|
109
|
+
back, and so does opening a note. It is never saved. `instance.expanded()` says
|
|
110
|
+
whether it is expanded, `instance.setExpanded(on)` does it from code, and
|
|
111
|
+
`instance.onExpandedChange(cb)` is called once the panel has its new size — to
|
|
112
|
+
change what is drawn; a `ResizeObserver` already reports the size itself.
|
|
113
|
+
|
|
114
|
+
### What notes mean
|
|
115
|
+
|
|
116
|
+
`ctx.metadata` reads the app's own index (needs `vault:read`): `note(path)` —
|
|
117
|
+
title, links, tags, aliases, headings, frontmatter properties — and
|
|
118
|
+
`backlinks`, `resolve`, `graph`, `vaultGraph`, `tags`, `notesWithTag`. Listen to
|
|
119
|
+
`metadata-updated` to know when to ask again. `plugins/bundled/graph` in the Munin repository is a whole
|
|
120
|
+
panel built on it.
|
|
121
|
+
|
|
122
|
+
### The open note
|
|
123
|
+
|
|
124
|
+
`ctx.editor.active()` is the note in the active pane, as its editor has it —
|
|
125
|
+
unsaved typing included — or null. `text()` and `selection()` read it
|
|
126
|
+
(`vault:read`); `insert(text)` at the caret, `replace(from, to, text)` and
|
|
127
|
+
`select(anchor, head)` change it through the editor, so the person can undo the
|
|
128
|
+
change and it is saved like typing (`vault:write`). Positions are offsets into
|
|
129
|
+
the text. In reading mode edits are refused.
|
|
130
|
+
|
|
131
|
+
### Input
|
|
132
|
+
|
|
133
|
+
- `ctx.keys.bind(commandId, 'Mod+Shift+D')` gives one of your commands a
|
|
134
|
+
shortcut (`Mod` is ⌘ on macOS, Ctrl elsewhere). A combination someone already
|
|
135
|
+
holds stays theirs, and your log says so.
|
|
136
|
+
- `await ctx.ui.pick({ items, placeholder })` shows a list with a search field
|
|
137
|
+
on either platform. Items are `{ label, detail? }` plus anything of yours; you
|
|
138
|
+
get back the item chosen, or null.
|
|
139
|
+
|
|
140
|
+
### Files
|
|
141
|
+
|
|
142
|
+
- Events `file-created`, `file-modified`, `file-renamed` (with `oldPath`) and
|
|
143
|
+
`file-deleted`, each with `path`, `folder` and `origin` (`app` or `external`).
|
|
144
|
+
- `vault.rename(path, newName)`, `vault.move(path, folder)`, `vault.delete(path)`
|
|
145
|
+
do what the person's own actions do — links are rewritten, tabs follow — and
|
|
146
|
+
throw when they cannot. `delete` asks nothing.
|
|
147
|
+
- `vault.readBinary(path)` → `{ data: Uint8Array, mime }`;
|
|
148
|
+
`vault.saveAttachment(name, data)` puts a file in the attachments folder and
|
|
149
|
+
returns its path.
|
|
150
|
+
|
|
151
|
+
### Menus and the status bar
|
|
152
|
+
|
|
153
|
+
- `ctx.menus.file.add({ id, title, when?, run })` — the file tree and a tab's
|
|
154
|
+
right-click menu on a computer, the long-press actions on a phone; `when` and
|
|
155
|
+
`run` get `{ path, isFolder }`.
|
|
156
|
+
- `ctx.menus.editor.add(…)` — a right click in the text (computer only); they get
|
|
157
|
+
`{ path, from, to, text }`.
|
|
158
|
+
- `ctx.statusBar.add({ id, text, title?, onClick? })` → `{ set(patch), remove() }`.
|
|
159
|
+
Computer only; on a phone it is accepted and shows nothing.
|
|
160
|
+
|
|
161
|
+
`when` runs while the menu is drawn: keep it quick, and synchronous.
|
|
162
|
+
|
|
163
|
+
### The network
|
|
164
|
+
|
|
165
|
+
Both need `network`, and both go out through the app rather than the window —
|
|
166
|
+
on a phone through the system — so a server's CORS rules do not apply, and
|
|
167
|
+
`http:` reaches machines on the local network (a model in Ollama or LM Studio)
|
|
168
|
+
on a computer and a phone alike.
|
|
169
|
+
|
|
170
|
+
- `await ctx.net.fetch(url, init?)` reads the answer whole (up to 10 MB):
|
|
171
|
+
`text()`, `json()`, `bytes()`.
|
|
172
|
+
- `await ctx.net.stream(url, init?)` resolves as soon as the status and headers
|
|
173
|
+
are in, and hands the body over as it arrives — what a language model's answer
|
|
174
|
+
is read with. Read it once, by one of `chunks()` (bytes), `lines()` (UTF-8
|
|
175
|
+
lines, joined across parts — for server-sent events and NDJSON) or `text()`.
|
|
176
|
+
Leaving a `for await` early closes the connection.
|
|
177
|
+
|
|
178
|
+
`init` is `{ method, headers, body, signal, timeoutMs }`. `signal` (an
|
|
179
|
+
`AbortController`'s) stops the request; waiting then fails with an error named
|
|
180
|
+
`AbortError`. `timeoutMs` is how long nothing may arrive — not how long the
|
|
181
|
+
whole answer may take: 30 s by default for `fetch`, 120 s for `stream`, and an
|
|
182
|
+
answer that keeps coming is never cut off; a quiet one fails as `TimeoutError`.
|
|
183
|
+
Requests still running when the plugin is switched off are stopped.
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
const res = await ctx.net.stream(`${base}/chat/completions`, {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${key}` },
|
|
189
|
+
body: JSON.stringify({ model, stream: true, messages }),
|
|
190
|
+
signal: controller.signal
|
|
191
|
+
})
|
|
192
|
+
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`)
|
|
193
|
+
for await (const line of res.lines()) {
|
|
194
|
+
if (!line.startsWith('data: ') || line === 'data: [DONE]') continue
|
|
195
|
+
write(JSON.parse(line.slice(6)).choices[0].delta.content ?? '')
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`plugins/examples/stream-chat` in the Munin repository is a whole plugin built this way.
|
|
200
|
+
|
|
201
|
+
### Secrets
|
|
202
|
+
|
|
203
|
+
`ctx.secrets.get(name)`, `set(name, value)`, `delete(name)` keep an access key or
|
|
204
|
+
token apart from everything else — never in the settings file, the plugin's data
|
|
205
|
+
or the vault. On a computer it is a file only the user can read (no keychain
|
|
206
|
+
prompt); on Android it is encrypted by the keystore, and where that is missing
|
|
207
|
+
`set` fails rather than keep the value in the clear. Plugins run with full
|
|
208
|
+
trust, so this keeps secrets off the disk's shared places, not from other
|
|
209
|
+
plugins.
|
|
210
|
+
|
|
211
|
+
A settings field `{ kind: 'secret', id: 'apiKey', label: 'API key' }` is the
|
|
212
|
+
person's way in: it shows whether the key is set, takes a new one and clears it,
|
|
213
|
+
and keeps it as the secret named by its `id`. `settings.get` answers `''` for it
|
|
214
|
+
and `onChange` is told `true` or `false`, never the value.
|
|
215
|
+
|
|
216
|
+
### Storage by key
|
|
217
|
+
|
|
218
|
+
`ctx.data` is one value, rewritten whole on every save. For many records that
|
|
219
|
+
change one at a time — sessions, a cache — use `ctx.store.get(key)`,
|
|
220
|
+
`set(key, value)`, `delete(key)` and `keys(prefix?)`: one file per key, beside
|
|
221
|
+
the data and removed with it. A key is 1–128 of `A–Z a–z 0–9 . _ -`; a value is
|
|
222
|
+
at most 5 MB written out; a save is whole — after a crash the key holds the old
|
|
223
|
+
value or the new one.
|
|
224
|
+
|
|
225
|
+
### Search
|
|
226
|
+
|
|
227
|
+
`await ctx.search.query('postgres index', { limit: 10 })` → `[{ path, title,
|
|
228
|
+
snippet }]`: the app's own search, with its operators and in its order
|
|
229
|
+
(`vault:read`). `limit` defaults to 20, at most 200.
|
|
230
|
+
|
|
231
|
+
### Settings you draw
|
|
232
|
+
|
|
233
|
+
A section with `mount(el)` instead of `fields` is drawn by the plugin — for what
|
|
234
|
+
is not a list of values, like a list of connections. As with a panel, return a
|
|
235
|
+
cleanup. Draw with the app's colours so it sits well in both themes:
|
|
236
|
+
`var(--text)`, `var(--text2)`, `var(--text3)`, `var(--island)`,
|
|
237
|
+
`var(--island2)`, `var(--border-soft)`, `var(--accent)`, `var(--danger)`.
|
|
238
|
+
|
|
239
|
+
`ctx.settings.open(sectionId?)` shows one of your sections — the first one
|
|
240
|
+
without an id: the settings window on a computer, the plugin's settings page on a
|
|
241
|
+
phone, where back returns to wherever you opened it from.
|
|
242
|
+
|
|
243
|
+
### Building on another plugin
|
|
244
|
+
|
|
245
|
+
A plugin publishes an API with `ctx.provide((client) => api)`; one that lists it
|
|
246
|
+
in `dependencies` gets it with `ctx.plugins.get(id)`. The factory runs once per
|
|
247
|
+
dependent, with `client.id`, `client.log(...)` (into the dependent's log) and
|
|
248
|
+
`client.register(disposer)` — what a dependent adds through your API is undone
|
|
249
|
+
when _it_ goes, so you never track its life yourself:
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
// provider
|
|
253
|
+
ctx.provide((client) => ({
|
|
254
|
+
addSource(source) {
|
|
255
|
+
sources.add(source)
|
|
256
|
+
const remove = () => sources.delete(source)
|
|
257
|
+
client.register(remove)
|
|
258
|
+
return remove
|
|
259
|
+
}
|
|
260
|
+
}))
|
|
261
|
+
|
|
262
|
+
// dependent — manifest: "dependencies": { "acme.provider": "1.0.0" }
|
|
263
|
+
ctx.plugins.get('acme.provider')?.addSource({ … })
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Daily notes publishes one: `addDayMarks({ id, label, marks(from, to) })` puts
|
|
267
|
+
marks on calendar days — `marks` answers `[{ date: 'YYYY-MM-DD', items: [{
|
|
268
|
+
title, time?, color? }] }]` for the month on screen, within three seconds — and
|
|
269
|
+
`refresh()` asks the calendar to fetch them again. `color` is one of `accent`,
|
|
270
|
+
`danger`, `red`, `orange`, `yellow`, `green`, `blue`, `purple`, `gray`, or `#rgb`
|
|
271
|
+
/ `#rrggbb`.
|
|
272
|
+
|
|
273
|
+
## Pack it
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
npx munin-pack my-plugin # → acme.word-count-1.0.0.mnp
|
|
277
|
+
npx munin-pack dist my-plugin.mnp # a built folder, a name of your own
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Bundle your code into the one `main.js` the manifest names (esbuild, Rollup…): a
|
|
281
|
+
package holds text files, and nothing is resolved from `node_modules` at run time.
|
|
282
|
+
The package is checked by the same rules the app and the registry use, so what
|
|
283
|
+
packs here installs there.
|
|
284
|
+
|
|
285
|
+
Try it before publishing: in the app, **Settings → Plugins → Install from a file**.
|
|
286
|
+
It is treated as unverified — you will get the same warnings a user would.
|
|
287
|
+
|
|
288
|
+
## Publish it
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
B=https://app.munin.md
|
|
292
|
+
|
|
293
|
+
# once. The token is shown only here; keep it.
|
|
294
|
+
curl -X POST -H 'Content-Type: application/json' -d '{"handle":"acme"}' $B/v1/authors
|
|
295
|
+
|
|
296
|
+
# each version. A published version is never overwritten: fix it with a new one.
|
|
297
|
+
curl -X PUT -H "Authorization: Bearer $TOKEN" --data-binary @acme.word-count-1.0.0.mnp \
|
|
298
|
+
$B/v1/plugins/acme.word-count/versions/1.0.0
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
The answer says `"state": "listed"` (it is in the catalogue now) or `"pending"`
|
|
302
|
+
(the catalogue reviews new versions first, and yours is waiting). Either way it is
|
|
303
|
+
**not verified** until the operator verifies it — people see "Not verified" and a
|
|
304
|
+
warning when they install it.
|
|
305
|
+
|
|
306
|
+
Handles containing `munin` (or look-alikes), and a few words like
|
|
307
|
+
`admin`, are reserved.
|
|
308
|
+
|
|
309
|
+
## Trust
|
|
310
|
+
|
|
311
|
+
A plugin runs **inside the app, with no isolation**. It can read and change all
|
|
312
|
+
your notes and reach whatever the app reaches. The permissions you declare are shown
|
|
313
|
+
to the person and the context refuses calls you did not declare — but that is a
|
|
314
|
+
guard against mistakes, not a wall, because nothing stops a plugin from going around
|
|
315
|
+
the context. Treat the person's trust as what it is. Plugins that misbehave can be
|
|
316
|
+
revoked, and the app switches revoked plugins off at its next start.
|
package/pack-plugin.mjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Pack a plugin folder into a .mnp file.
|
|
3
|
+
//
|
|
4
|
+
// node plugins/sdk/pack-plugin.mjs <folder> [out.mnp]
|
|
5
|
+
//
|
|
6
|
+
// The folder holds manifest.json and the plugin's text files (main.js, styles.css,
|
|
7
|
+
// README.md…). Every file other than the manifest goes into the package. The
|
|
8
|
+
// result is checked by the same code the app and the registry use, so a package
|
|
9
|
+
// that packs here installs there.
|
|
10
|
+
//
|
|
11
|
+
// The rules come from plugin-package.js, compiled from the app's own
|
|
12
|
+
// src/shared/plugin-package.ts (`npm run plugins:sdk`).
|
|
13
|
+
|
|
14
|
+
import { realpathSync } from 'node:fs'
|
|
15
|
+
import { readdir, readFile, writeFile } from 'node:fs/promises'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
import { join, relative, sep } from 'node:path'
|
|
18
|
+
import { parsePackage } from './plugin-package.js'
|
|
19
|
+
|
|
20
|
+
async function walk(dir, root, out = []) {
|
|
21
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
22
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
|
|
23
|
+
const full = join(dir, entry.name)
|
|
24
|
+
if (entry.isDirectory()) await walk(full, root, out)
|
|
25
|
+
else out.push(relative(root, full).split(sep).join('/'))
|
|
26
|
+
}
|
|
27
|
+
return out
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Read a folder and return the package text, or throw naming what is wrong. */
|
|
31
|
+
export async function packFolder(folder) {
|
|
32
|
+
let manifest
|
|
33
|
+
try {
|
|
34
|
+
manifest = JSON.parse(await readFile(join(folder, 'manifest.json'), 'utf8'))
|
|
35
|
+
} catch (err) {
|
|
36
|
+
throw new Error(`${folder}/manifest.json: ${err.message}`)
|
|
37
|
+
}
|
|
38
|
+
const files = {}
|
|
39
|
+
for (const path of await walk(folder, folder)) {
|
|
40
|
+
if (path === 'manifest.json') continue
|
|
41
|
+
files[path] = await readFile(join(folder, path), 'utf8')
|
|
42
|
+
}
|
|
43
|
+
const text = JSON.stringify({ manifest, files })
|
|
44
|
+
const parsed = parsePackage(text)
|
|
45
|
+
if (!parsed.ok) throw new Error(parsed.error.message)
|
|
46
|
+
return { text, manifest: parsed.pkg.manifest }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Run as a command — directly, or through the `munin-pack` link npm makes, whose
|
|
50
|
+
// path is not this file's until the link is followed.
|
|
51
|
+
const invoked = process.argv[1] ? realpathSync(process.argv[1]) : null
|
|
52
|
+
if (invoked === fileURLToPath(import.meta.url)) {
|
|
53
|
+
const [folder, out] = process.argv.slice(2)
|
|
54
|
+
if (!folder) {
|
|
55
|
+
console.error('usage: munin-pack <folder> [out.mnp]')
|
|
56
|
+
process.exit(2)
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const { text, manifest } = await packFolder(folder)
|
|
60
|
+
const target = out ?? `${manifest.id}-${manifest.version}.mnp`
|
|
61
|
+
await writeFile(target, text, 'utf8')
|
|
62
|
+
console.log(`${target} (${Buffer.byteLength(text)} bytes)`)
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(`not packed: ${err.message}`)
|
|
65
|
+
process.exit(1)
|
|
66
|
+
}
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muninmd/munin-sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "TypeScript types and the packer for Munin plugins.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"munin",
|
|
7
|
+
"plugin",
|
|
8
|
+
"sdk",
|
|
9
|
+
"notes",
|
|
10
|
+
"markdown",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://munin.md",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Aleksandr Eremin",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"types": "./plugin-api.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./plugin-api.d.ts"
|
|
21
|
+
},
|
|
22
|
+
"./plugin-api": {
|
|
23
|
+
"types": "./plugin-api.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"./plugin-package": {
|
|
26
|
+
"types": "./plugin-package.d.ts",
|
|
27
|
+
"default": "./plugin-package.js"
|
|
28
|
+
},
|
|
29
|
+
"./pack": {
|
|
30
|
+
"types": "./pack-plugin.d.mts",
|
|
31
|
+
"default": "./pack-plugin.mjs"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"bin": {
|
|
35
|
+
"munin-pack": "./pack-plugin.mjs"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"plugin-api.d.ts",
|
|
39
|
+
"plugin-package.d.ts",
|
|
40
|
+
"plugin-package.js",
|
|
41
|
+
"pack-plugin.mjs",
|
|
42
|
+
"pack-plugin.d.mts",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@codemirror/state": "^6.4.1",
|
|
51
|
+
"@codemirror/view": "^6.34.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@codemirror/state": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"@codemirror/view": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
}
|
|
64
|
+
}
|