notefeed 0.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 +19 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +85 -0
- package/dist/client.d.ts +36 -0
- package/dist/client.js +87 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcel Bruckner
|
|
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
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# notefeed
|
|
2
|
+
|
|
3
|
+
Post markdown notes to a [notefeed](https://github.com/Marceltov/notefeed) server from Node or the command line. No dependencies; Node 20 or newer.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install notefeed
|
|
7
|
+
export NOTEFEED_URL=https://notes.example.com NOTEFEED_TOKEN=...
|
|
8
|
+
npx notefeed post "# Backup finished"
|
|
9
|
+
backup.sh 2>&1 | npx notefeed post -
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { Client } from "notefeed";
|
|
14
|
+
|
|
15
|
+
const note = await new Client().post("# Deploy done\nversion 1.4.2"); // reads NOTEFEED_URL / NOTEFEED_TOKEN
|
|
16
|
+
console.log(note.url);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Full documentation: https://marceltov.github.io/notefeed/clients/
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type Io = {
|
|
2
|
+
stdin: AsyncIterable<Buffer | string>;
|
|
3
|
+
stdout: {
|
|
4
|
+
write(s: string): unknown;
|
|
5
|
+
};
|
|
6
|
+
stderr: {
|
|
7
|
+
write(s: string): unknown;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
/** Returns the exit code: 0 ok, 1 server/network error, 2 usage/config error. */
|
|
11
|
+
export declare function main(argv: string[], io?: Io): Promise<number>;
|
|
12
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** `notefeed post ...` — post a note from the command line. */
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { Client, ConfigError, NotefeedError } from "./client.js";
|
|
6
|
+
class UsageError extends Error {
|
|
7
|
+
}
|
|
8
|
+
const USAGE = 'usage: notefeed post <text | - | --file PATH> [--url URL] [--token TOKEN]';
|
|
9
|
+
/** Returns the exit code: 0 ok, 1 server/network error, 2 usage/config error. */
|
|
10
|
+
export async function main(argv, io = process) {
|
|
11
|
+
try {
|
|
12
|
+
const { values, positionals } = parseArgs({
|
|
13
|
+
args: asPositionals(argv),
|
|
14
|
+
allowPositionals: true,
|
|
15
|
+
options: {
|
|
16
|
+
url: { type: "string" },
|
|
17
|
+
token: { type: "string" },
|
|
18
|
+
file: { type: "string" },
|
|
19
|
+
version: { type: "boolean" },
|
|
20
|
+
help: { type: "boolean", short: "h" },
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
if (values.help) {
|
|
24
|
+
io.stdout.write(`${USAGE}\n`);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
if (values.version) {
|
|
28
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
29
|
+
io.stdout.write(`notefeed ${pkg.version}\n`);
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
const [command, text, ...rest] = positionals;
|
|
33
|
+
if (command !== "post" || rest.length)
|
|
34
|
+
throw new UsageError(USAGE);
|
|
35
|
+
const markdown = await read(text, values.file, io);
|
|
36
|
+
const note = await new Client({ url: values.url, token: values.token }).post(markdown);
|
|
37
|
+
io.stdout.write(`${note.url}\n`);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
io.stderr.write(`notefeed: ${e.message.split("\n")[0]}\n`);
|
|
42
|
+
if (e instanceof ConfigError || e instanceof UsageError)
|
|
43
|
+
return 2;
|
|
44
|
+
if (e instanceof NotefeedError)
|
|
45
|
+
return 1;
|
|
46
|
+
return 2; // parseArgs rejects unknown options with a TypeError
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function read(text, file, io) {
|
|
50
|
+
if (file) {
|
|
51
|
+
try {
|
|
52
|
+
return utf8(await readFile(file), file);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
if (e instanceof UsageError)
|
|
56
|
+
throw e;
|
|
57
|
+
throw new UsageError(`cannot read ${file}: ${e.code ?? e.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (text === "-") {
|
|
61
|
+
const chunks = [];
|
|
62
|
+
for await (const c of io.stdin)
|
|
63
|
+
chunks.push(typeof c === "string" ? Buffer.from(c) : c);
|
|
64
|
+
return utf8(Buffer.concat(chunks), "stdin");
|
|
65
|
+
}
|
|
66
|
+
if (text === undefined)
|
|
67
|
+
throw new UsageError('give the note text, "-" for stdin, or --file PATH');
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
function utf8(bytes, source) {
|
|
71
|
+
try {
|
|
72
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new UsageError(`${source} is not UTF-8`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Note text like "- buy milk" starts with "-" but is not an option; argparse (Python CLI) treats an
|
|
79
|
+
// argument with whitespace as positional, so do the same by moving such arguments after "--".
|
|
80
|
+
function asPositionals(argv) {
|
|
81
|
+
if (argv.includes("--"))
|
|
82
|
+
return argv;
|
|
83
|
+
const text = argv.filter((a) => /^-.*\s/.test(a));
|
|
84
|
+
return [...argv.filter((a) => !/^-.*\s/.test(a)), "--", ...text];
|
|
85
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** A tiny client for notefeed's POST /api/notes. No dependencies; uses the global fetch. */
|
|
2
|
+
export type Note = {
|
|
3
|
+
id: string;
|
|
4
|
+
url: string;
|
|
5
|
+
};
|
|
6
|
+
export type ClientOptions = {
|
|
7
|
+
url?: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
};
|
|
11
|
+
/** Any failure talking to notefeed. `status` is the HTTP status, or null (network, config). */
|
|
12
|
+
export declare class NotefeedError extends Error {
|
|
13
|
+
readonly status: number | null;
|
|
14
|
+
constructor(message: string, status?: number | null);
|
|
15
|
+
}
|
|
16
|
+
/** URL or token missing. */
|
|
17
|
+
export declare class ConfigError extends NotefeedError {
|
|
18
|
+
}
|
|
19
|
+
/** The server rejected the note (400, 415): empty, not UTF-8, wrong type. */
|
|
20
|
+
export declare class InvalidNoteError extends NotefeedError {
|
|
21
|
+
}
|
|
22
|
+
/** Missing or wrong token (401). */
|
|
23
|
+
export declare class AuthError extends NotefeedError {
|
|
24
|
+
}
|
|
25
|
+
/** The note is over the server's size limit (413). */
|
|
26
|
+
export declare class NoteTooLargeError extends NotefeedError {
|
|
27
|
+
}
|
|
28
|
+
export declare class Client {
|
|
29
|
+
readonly url: string;
|
|
30
|
+
private readonly token;
|
|
31
|
+
private readonly timeoutMs;
|
|
32
|
+
constructor(options?: ClientOptions);
|
|
33
|
+
post(markdown: string): Promise<Note>;
|
|
34
|
+
}
|
|
35
|
+
/** Post one note. Options go to Client (url, token, timeoutMs). */
|
|
36
|
+
export declare function post(markdown: string, options?: ClientOptions): Promise<Note>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** A tiny client for notefeed's POST /api/notes. No dependencies; uses the global fetch. */
|
|
2
|
+
/** Any failure talking to notefeed. `status` is the HTTP status, or null (network, config). */
|
|
3
|
+
export class NotefeedError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
constructor(message, status = null) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.name = new.target.name;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** URL or token missing. */
|
|
12
|
+
export class ConfigError extends NotefeedError {
|
|
13
|
+
}
|
|
14
|
+
/** The server rejected the note (400, 415): empty, not UTF-8, wrong type. */
|
|
15
|
+
export class InvalidNoteError extends NotefeedError {
|
|
16
|
+
}
|
|
17
|
+
/** Missing or wrong token (401). */
|
|
18
|
+
export class AuthError extends NotefeedError {
|
|
19
|
+
}
|
|
20
|
+
/** The note is over the server's size limit (413). */
|
|
21
|
+
export class NoteTooLargeError extends NotefeedError {
|
|
22
|
+
}
|
|
23
|
+
const ERRORS = {
|
|
24
|
+
400: InvalidNoteError,
|
|
25
|
+
415: InvalidNoteError,
|
|
26
|
+
401: AuthError,
|
|
27
|
+
413: NoteTooLargeError,
|
|
28
|
+
};
|
|
29
|
+
function setting(value, env) {
|
|
30
|
+
const v = value || process.env[env] || "";
|
|
31
|
+
if (!v)
|
|
32
|
+
throw new ConfigError(`no ${env.split("_")[1].toLowerCase()} given; pass it or set ${env}`);
|
|
33
|
+
return v;
|
|
34
|
+
}
|
|
35
|
+
export class Client {
|
|
36
|
+
url;
|
|
37
|
+
token;
|
|
38
|
+
timeoutMs;
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
this.url = setting(options.url, "NOTEFEED_URL").replace(/\/+$/, "");
|
|
41
|
+
this.token = setting(options.token, "NOTEFEED_TOKEN").trim();
|
|
42
|
+
// Never echo the value: it would end up in terminals and CI logs.
|
|
43
|
+
if (/[\x00-\x1f\x7f]/.test(this.token))
|
|
44
|
+
throw new ConfigError("NOTEFEED_TOKEN contains invalid characters");
|
|
45
|
+
this.timeoutMs = options.timeoutMs ?? 10_000;
|
|
46
|
+
}
|
|
47
|
+
async post(markdown) {
|
|
48
|
+
let res;
|
|
49
|
+
try {
|
|
50
|
+
res = await fetch(`${this.url}/api/notes`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { Authorization: `Bearer ${this.token}`, "Content-Type": "text/markdown; charset=utf-8" },
|
|
53
|
+
body: new TextEncoder().encode(markdown),
|
|
54
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
const cause = e.cause;
|
|
59
|
+
const reason = cause?.code ?? cause?.message ?? e.message;
|
|
60
|
+
throw new NotefeedError(`could not reach ${this.url}: ${reason}`);
|
|
61
|
+
}
|
|
62
|
+
const text = await res.text();
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
let message;
|
|
65
|
+
try {
|
|
66
|
+
message = JSON.parse(text).error;
|
|
67
|
+
if (typeof message !== "string")
|
|
68
|
+
throw new Error();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
message = `HTTP ${res.status}: ${text.slice(0, 200).replace(/\s+/g, " ").trim()}`;
|
|
72
|
+
}
|
|
73
|
+
throw new (ERRORS[res.status] ?? NotefeedError)(message, res.status);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const { id, url } = JSON.parse(text);
|
|
77
|
+
if (typeof id === "string" && typeof url === "string")
|
|
78
|
+
return { id, url };
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
throw new NotefeedError(`unexpected response from ${this.url} (not a notefeed server?)`, res.status);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Post one note. Options go to Client (url, token, timeoutMs). */
|
|
85
|
+
export function post(markdown, options) {
|
|
86
|
+
return new Client(options).post(markdown);
|
|
87
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AuthError, Client, ConfigError, InvalidNoteError, NotefeedError, NoteTooLargeError, post } from "./client.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "notefeed",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Post markdown notes to a notefeed server, from Node or the command line.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"notefeed": "dist/bin.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/Marceltov/notefeed.git",
|
|
30
|
+
"directory": "packages/js"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://marceltov.github.io/notefeed/clients/",
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20",
|
|
35
|
+
"typescript": "^5.9",
|
|
36
|
+
"vitest": "^4.1"
|
|
37
|
+
}
|
|
38
|
+
}
|