freshjots 0.2.1 → 0.3.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 +41 -0
- package/bin/freshjots.js +4 -0
- package/cli.js +149 -0
- package/index.js +1 -1
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -41,6 +41,47 @@ The whole API is four methods: `notes()`, `note(filename)`,
|
|
|
41
41
|
`create()` return the note object directly (no `{ note: … }` wrapper);
|
|
42
42
|
`notes()` returns the array.
|
|
43
43
|
|
|
44
|
+
## CLI
|
|
45
|
+
|
|
46
|
+
Installing the package globally puts a `freshjots` command on your
|
|
47
|
+
PATH, so you can read and write notes straight from a terminal without
|
|
48
|
+
writing any JavaScript. This works in bash, zsh, fish, Windows
|
|
49
|
+
PowerShell, and CMD — npm generates a `.cmd` shim on Windows
|
|
50
|
+
automatically.
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npm install -g freshjots
|
|
54
|
+
export FRESHJOTS_TOKEN=mn_… # PowerShell: $env:FRESHJOTS_TOKEN = "mn_…"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The CLI mirrors the four API methods one-for-one:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
freshjots list # prints "<filename>\t<title>" per row
|
|
61
|
+
freshjots show cron-jobs-prod # prints the note's plain_body
|
|
62
|
+
freshjots create "Research 2026 Q2" # body comes from stdin or --body
|
|
63
|
+
freshjots append cron-jobs-prod "ok" # text may also be piped on stdin
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Both `create` and `append` read from stdin when the body or text isn't
|
|
67
|
+
passed as an argument, so the usual pipe patterns work:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
backup.sh && echo "backup ok $(date -Iseconds)" | freshjots append cron-jobs-prod
|
|
71
|
+
git log -1 --pretty=format:"%h %s" | freshjots append deploys
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The same patterns work in PowerShell:
|
|
75
|
+
|
|
76
|
+
```powershell
|
|
77
|
+
"backup ok $(Get-Date -Format o)" | freshjots append cron-jobs-prod
|
|
78
|
+
freshjots create "Deploy log" --body "Initial entry."
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Exit codes: `0` on success, `1` on runtime errors (missing token,
|
|
82
|
+
network failure, non-2xx API response — printed as `Error: HTTP <status>
|
|
83
|
+
<code>: <message>`), `2` on usage errors.
|
|
84
|
+
|
|
44
85
|
## TypeScript
|
|
45
86
|
|
|
46
87
|
Types ship with the package — no `@types/freshjots` needed, no `.d.ts`
|
package/bin/freshjots.js
ADDED
package/cli.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// CLI dispatcher for the `freshjots` command. The shebang entry
|
|
2
|
+
// (bin/freshjots.js) is a one-line wrapper around run() so this module
|
|
3
|
+
// stays import-safe and testable: parseArgs is pure, run takes its I/O
|
|
4
|
+
// surface as deps so tests can stub stdin/stdout/stderr and the Client.
|
|
5
|
+
|
|
6
|
+
import { Client, ApiError, VERSION } from "./index.js";
|
|
7
|
+
|
|
8
|
+
const USAGE = `freshjots — Fresh Jots CLI
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
freshjots list
|
|
12
|
+
freshjots show <filename>
|
|
13
|
+
freshjots create <title> [--body <text>]
|
|
14
|
+
freshjots append <filename> [<text>]
|
|
15
|
+
freshjots --help | --version
|
|
16
|
+
|
|
17
|
+
Notes:
|
|
18
|
+
- <text> for append and --body for create may also be piped on stdin.
|
|
19
|
+
- Auth: set FRESHJOTS_TOKEN. Mint one at
|
|
20
|
+
https://freshjots.com/settings/api_tokens.
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
export function parseArgs(argv) {
|
|
24
|
+
if (argv.length === 0) return { command: "help", exitCode: 2 };
|
|
25
|
+
const [first, ...rest] = argv;
|
|
26
|
+
|
|
27
|
+
if (first === "-h" || first === "--help" || first === "help") {
|
|
28
|
+
return { command: "help", exitCode: 0 };
|
|
29
|
+
}
|
|
30
|
+
if (first === "-v" || first === "--version" || first === "version") {
|
|
31
|
+
return { command: "version" };
|
|
32
|
+
}
|
|
33
|
+
if (first === "list") {
|
|
34
|
+
if (rest.length) return { command: "error", message: "list takes no arguments" };
|
|
35
|
+
return { command: "list" };
|
|
36
|
+
}
|
|
37
|
+
if (first === "show") {
|
|
38
|
+
if (rest.length !== 1) return { command: "error", message: "show requires exactly one <filename>" };
|
|
39
|
+
return { command: "show", filename: rest[0] };
|
|
40
|
+
}
|
|
41
|
+
if (first === "create") {
|
|
42
|
+
let body;
|
|
43
|
+
const positional = [];
|
|
44
|
+
for (let i = 0; i < rest.length; i++) {
|
|
45
|
+
const a = rest[i];
|
|
46
|
+
if (a === "--body" || a === "-b") {
|
|
47
|
+
if (i + 1 >= rest.length) return { command: "error", message: "--body requires a value" };
|
|
48
|
+
body = rest[++i];
|
|
49
|
+
} else if (a.startsWith("--body=")) {
|
|
50
|
+
body = a.slice("--body=".length);
|
|
51
|
+
} else {
|
|
52
|
+
positional.push(a);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (positional.length !== 1) return { command: "error", message: "create requires exactly one <title>" };
|
|
56
|
+
return { command: "create", title: positional[0], body };
|
|
57
|
+
}
|
|
58
|
+
if (first === "append") {
|
|
59
|
+
if (rest.length < 1 || rest.length > 2) {
|
|
60
|
+
return { command: "error", message: "append requires <filename> and optional <text>" };
|
|
61
|
+
}
|
|
62
|
+
return { command: "append", filename: rest[0], text: rest[1] };
|
|
63
|
+
}
|
|
64
|
+
return { command: "error", message: `unknown command: ${first}` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function readStdin(stdin) {
|
|
68
|
+
if (!stdin || stdin.isTTY) return "";
|
|
69
|
+
let buf = "";
|
|
70
|
+
for await (const chunk of stdin) {
|
|
71
|
+
buf += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
72
|
+
}
|
|
73
|
+
return buf;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function run(argv, deps = {}) {
|
|
77
|
+
const env = deps.env ?? process.env;
|
|
78
|
+
const stdout = deps.stdout ?? ((s) => process.stdout.write(s));
|
|
79
|
+
const stderr = deps.stderr ?? ((s) => process.stderr.write(s));
|
|
80
|
+
const stdin = deps.stdin ?? process.stdin;
|
|
81
|
+
const clientFactory = deps.clientFactory ?? ((token) => new Client({ token }));
|
|
82
|
+
|
|
83
|
+
const parsed = parseArgs(argv);
|
|
84
|
+
|
|
85
|
+
if (parsed.command === "help") {
|
|
86
|
+
(parsed.exitCode ? stderr : stdout)(USAGE);
|
|
87
|
+
return parsed.exitCode ?? 0;
|
|
88
|
+
}
|
|
89
|
+
if (parsed.command === "version") {
|
|
90
|
+
stdout(`freshjots ${VERSION}\n`);
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
if (parsed.command === "error") {
|
|
94
|
+
stderr(`Error: ${parsed.message}\n\n${USAGE}`);
|
|
95
|
+
return 2;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const token = env.FRESHJOTS_TOKEN;
|
|
99
|
+
if (!token) {
|
|
100
|
+
stderr("Error: FRESHJOTS_TOKEN is not set. Mint one at https://freshjots.com/settings/api_tokens\n");
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let client;
|
|
105
|
+
try {
|
|
106
|
+
client = clientFactory(token);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
stderr(`Error: ${e.message}\n`);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
if (parsed.command === "list") {
|
|
114
|
+
const notes = await client.notes();
|
|
115
|
+
for (const n of notes) stdout(`${n.filename}\t${n.title}\n`);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
if (parsed.command === "show") {
|
|
119
|
+
const note = await client.note(parsed.filename);
|
|
120
|
+
stdout(note.plain_body ?? "");
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
if (parsed.command === "create") {
|
|
124
|
+
let body = parsed.body;
|
|
125
|
+
if (body === undefined) body = await readStdin(stdin);
|
|
126
|
+
const created = await client.create({ title: parsed.title, body });
|
|
127
|
+
stdout(`${created.filename}\n`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
if (parsed.command === "append") {
|
|
131
|
+
let text = parsed.text;
|
|
132
|
+
if (text === undefined) text = await readStdin(stdin);
|
|
133
|
+
if (!text) {
|
|
134
|
+
stderr("Error: append requires text (as an argument or on stdin)\n");
|
|
135
|
+
return 2;
|
|
136
|
+
}
|
|
137
|
+
await client.append(parsed.filename, text);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
} catch (e) {
|
|
141
|
+
if (e instanceof ApiError) {
|
|
142
|
+
stderr(`Error: HTTP ${e.status} ${e.code}: ${e.message}\n`);
|
|
143
|
+
} else {
|
|
144
|
+
stderr(`Error: ${e.message}\n`);
|
|
145
|
+
}
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
package/index.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// payload ({ "notes": [...] }). show / show-by-filename / create return
|
|
19
19
|
// the note object at the TOP LEVEL — there is no { "note": ... } wrapper.
|
|
20
20
|
|
|
21
|
-
export const VERSION = "0.
|
|
21
|
+
export const VERSION = "0.3.0";
|
|
22
22
|
const DEFAULT_BASE_URL = "https://freshjots.com/api/v1";
|
|
23
23
|
|
|
24
24
|
export class ApiError extends Error {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freshjots",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Tiny JavaScript client for the Fresh Jots API. Append-only notebooks for cron jobs, deploy scripts, and bots.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -11,9 +11,14 @@
|
|
|
11
11
|
"default": "./index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"freshjots": "./bin/freshjots.js"
|
|
16
|
+
},
|
|
14
17
|
"files": [
|
|
15
18
|
"index.js",
|
|
16
19
|
"index.d.ts",
|
|
20
|
+
"cli.js",
|
|
21
|
+
"bin/",
|
|
17
22
|
"README.md",
|
|
18
23
|
"LICENSE"
|
|
19
24
|
],
|