mdpockla 0.1.1 → 0.2.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.
@@ -1,178 +1,327 @@
1
- import { promises as fs } from "node:fs";
2
- import path from "node:path";
3
- import { api } from "../lib/http.mjs";
4
- import { parseArgs } from "../lib/args.mjs";
5
-
6
- /**
7
- * Each command does its own argv parsing (via parseArgs) so the
8
- * --flag set is documented at the call site. The bodies are
9
- * intentionally one-liners over `api()` — the HTTP API is the source
10
- * of truth.
11
- *
12
- * Output convention: human-readable to stdout, share URL on the last
13
- * line so piping into another tool yields the URL alone.
14
- */
15
-
16
- export async function noteCreate(argv) {
17
- const args = parseArgs(argv, {
18
- title: { type: "string" },
19
- public: { type: "boolean" },
20
- tag: { type: "string", repeatable: true },
21
- json: { type: "boolean" },
22
- });
23
- const file = args._[0];
24
- if (!file) throw exit("Usage: mdpockla note create <file>");
25
- const abs = path.resolve(file);
26
- const content = await fs.readFile(abs, "utf8");
27
- const ext = path.extname(abs).toLowerCase();
28
- const content_type = ext === ".html" || ext === ".htm" ? "html" : "markdown";
29
-
30
- const note = await api("POST", "/api/v1/notes", {
31
- body: {
32
- title: args.title,
33
- content,
34
- content_type,
35
- visibility: args.public ? "public" : "private",
36
- tags: args.tag.length ? args.tag : undefined,
37
- },
38
- });
39
- if (args.json) process.stdout.write(JSON.stringify(note, null, 2) + "\n");
40
- else process.stdout.write(note.url + "\n");
41
- }
42
-
43
- export async function noteEdit(argv) {
44
- const args = parseArgs(argv, {
45
- content: { type: "string" },
46
- title: { type: "string" },
47
- visibility: { type: "string" },
48
- tag: { type: "string", repeatable: true },
49
- json: { type: "boolean" },
50
- });
51
- const target = args._[0];
52
- if (!target) throw exit("Usage: mdpockla note edit <id|url> [...]");
53
- const body = {};
54
- if (args.title) body.title = args.title;
55
- if (args.visibility) body.visibility = args.visibility;
56
- if (args.tag.length) body.tags = args.tag;
57
- if (args.content) {
58
- const abs = path.resolve(args.content);
59
- body.content = await fs.readFile(abs, "utf8");
60
- const ext = path.extname(abs).toLowerCase();
61
- body.content_type = ext === ".html" || ext === ".htm" ? "html" : "markdown";
62
- }
63
- if (Object.keys(body).length === 0) {
64
- throw exit("Nothing to update — pass at least one of --content, --title, --visibility, --tag");
65
- }
66
- const note = await api("PATCH", `/api/v1/notes/${encodeURIComponent(target)}`, {
67
- body,
68
- });
69
- if (args.json) process.stdout.write(JSON.stringify(note, null, 2) + "\n");
70
- else process.stdout.write(note.url + "\n");
71
- }
72
-
73
- export async function noteGet(argv) {
74
- const args = parseArgs(argv, {
75
- output: { type: "string" },
76
- json: { type: "boolean" },
77
- });
78
- const target = args._[0];
79
- if (!target) throw exit("Usage: mdpockla note get <id|url> [--output FILE]");
80
- const note = await api("GET", `/api/v1/notes/${encodeURIComponent(target)}`);
81
- if (args.json) {
82
- process.stdout.write(JSON.stringify(note, null, 2) + "\n");
83
- return;
84
- }
85
- if (args.output) {
86
- await fs.writeFile(path.resolve(args.output), note.content);
87
- process.stdout.write(`Wrote ${args.output}\n`);
88
- } else {
89
- process.stdout.write(note.content);
90
- if (!note.content.endsWith("\n")) process.stdout.write("\n");
91
- }
92
- }
93
-
94
- export async function noteList(argv) {
95
- const args = parseArgs(argv, {
96
- mine: { type: "boolean" },
97
- shared: { type: "boolean" },
98
- query: { type: "string" },
99
- limit: { type: "string" },
100
- json: { type: "boolean" },
101
- });
102
- const filter = args.mine ? "mine" : args.shared ? "shared" : "all";
103
- const limit = args.limit ? Number.parseInt(args.limit, 10) : undefined;
104
- const res = await api("GET", "/api/v1/notes", {
105
- query: { filter, query: args.query, limit },
106
- });
107
- if (args.json) {
108
- process.stdout.write(JSON.stringify(res, null, 2) + "\n");
109
- return;
110
- }
111
- if (res.notes.length === 0) {
112
- process.stdout.write("No notes.\n");
113
- return;
114
- }
115
- for (const n of res.notes) {
116
- process.stdout.write(
117
- `${n.url} ${n.visibility.padEnd(7)} ${truncate(n.title, 60)}\n`
118
- );
119
- }
120
- }
121
-
122
- export async function noteDelete(argv) {
123
- const target = argv[0];
124
- if (!target) throw exit("Usage: mdpockla note delete <id|url>");
125
- await api("DELETE", `/api/v1/notes/${encodeURIComponent(target)}`);
126
- process.stdout.write("Deleted.\n");
127
- }
128
-
129
- export async function noteShare(argv) {
130
- const args = parseArgs(argv, {
131
- visibility: { type: "string" },
132
- });
133
- const target = args._[0];
134
- if (!target || !args.visibility) {
135
- throw exit("Usage: mdpockla note share <id|url> --visibility public|private");
136
- }
137
- const note = await api("POST", `/api/v1/notes/${encodeURIComponent(target)}/share`, {
138
- body: { visibility: args.visibility },
139
- });
140
- process.stdout.write(note.url + "\n");
141
- }
142
-
143
- export async function noteCollaboratorAdd(argv) {
144
- const [target, email] = argv;
145
- if (!target || !email) {
146
- throw exit("Usage: mdpockla note collaborator add <id|url> <email>");
147
- }
148
- const note = await api(
149
- "POST",
150
- `/api/v1/notes/${encodeURIComponent(target)}/collaborators`,
151
- { body: { email } }
152
- );
153
- process.stdout.write(note.url + "\n");
154
- }
155
-
156
- export async function noteCollaboratorRemove(argv) {
157
- const [target, email] = argv;
158
- if (!target || !email) {
159
- throw exit("Usage: mdpockla note collaborator remove <id|url> <email>");
160
- }
161
- const note = await api(
162
- "DELETE",
163
- `/api/v1/notes/${encodeURIComponent(target)}/collaborators`,
164
- { query: { email } }
165
- );
166
- process.stdout.write(note.url + "\n");
167
- }
168
-
169
- function truncate(s, n) {
170
- if (!s) return "";
171
- return s.length > n ? s.slice(0, n - 1) + "…" : s;
172
- }
173
-
174
- function exit(message, code = 2) {
175
- const err = new Error(message);
176
- err.exitCode = code;
177
- return err;
178
- }
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { api } from "../lib/http.mjs";
4
+ import { parseArgs } from "../lib/args.mjs";
5
+
6
+ /**
7
+ * Each command does its own argv parsing (via parseArgs) so the
8
+ * --flag set is documented at the call site. The bodies are
9
+ * intentionally one-liners over `api()` — the HTTP API is the source
10
+ * of truth.
11
+ *
12
+ * Output convention: human-readable to stdout, share URL on the last
13
+ * line so piping into another tool yields the URL alone.
14
+ */
15
+
16
+ export async function noteCreate(argv) {
17
+ const args = parseArgs(argv, {
18
+ title: { type: "string" },
19
+ public: { type: "boolean" },
20
+ tag: { type: "string", repeatable: true },
21
+ json: { type: "boolean" },
22
+ });
23
+ const file = args._[0];
24
+ if (!file) throw exit("Usage: mdpockla note create <file>");
25
+ const abs = path.resolve(file);
26
+ const content = await fs.readFile(abs, "utf8");
27
+ const ext = path.extname(abs).toLowerCase();
28
+ const content_type = ext === ".html" || ext === ".htm" ? "html" : "markdown";
29
+
30
+ const note = await api("POST", "/api/v1/notes", {
31
+ body: {
32
+ title: args.title,
33
+ content,
34
+ content_type,
35
+ visibility: args.public ? "public" : "private",
36
+ tags: args.tag.length ? args.tag : undefined,
37
+ },
38
+ });
39
+ if (args.json) process.stdout.write(JSON.stringify(note, null, 2) + "\n");
40
+ else process.stdout.write(note.url + "\n");
41
+ }
42
+
43
+ export async function noteEdit(argv) {
44
+ const args = parseArgs(argv, {
45
+ content: { type: "string" },
46
+ title: { type: "string" },
47
+ visibility: { type: "string" },
48
+ tag: { type: "string", repeatable: true },
49
+ "save-version": { type: "boolean" },
50
+ label: { type: "string" },
51
+ json: { type: "boolean" },
52
+ });
53
+ const target = args._[0];
54
+ if (!target) throw exit("Usage: mdpockla note edit <id|url> [...]");
55
+ const body = {};
56
+ if (args.title) body.title = args.title;
57
+ if (args.visibility) body.visibility = args.visibility;
58
+ if (args.tag.length) body.tags = args.tag;
59
+ if (args.content) {
60
+ const abs = path.resolve(args.content);
61
+ body.content = await fs.readFile(abs, "utf8");
62
+ const ext = path.extname(abs).toLowerCase();
63
+ body.content_type = ext === ".html" || ext === ".htm" ? "html" : "markdown";
64
+ }
65
+ if (Object.keys(body).length === 0) {
66
+ throw exit("Nothing to update — pass at least one of --content, --title, --visibility, --tag");
67
+ }
68
+ const note = await api("PATCH", `/api/v1/notes/${encodeURIComponent(target)}`, {
69
+ body,
70
+ });
71
+ if (args["save-version"]) {
72
+ try {
73
+ await api(
74
+ "POST",
75
+ `/api/v1/notes/${encodeURIComponent(target)}/versions`,
76
+ { body: { label: args.label ?? null } }
77
+ );
78
+ } catch (err) {
79
+ // Snapshot failed but the edit landed surface the warning but
80
+ // exit 0 so the user can see the share URL.
81
+ process.stderr.write(
82
+ `warning: --save-version failed: ${err.message ?? err}\n`
83
+ );
84
+ }
85
+ }
86
+ if (args.json) process.stdout.write(JSON.stringify(note, null, 2) + "\n");
87
+ else process.stdout.write(note.url + "\n");
88
+ }
89
+
90
+ export async function noteGet(argv) {
91
+ const args = parseArgs(argv, {
92
+ output: { type: "string" },
93
+ version: { type: "string" },
94
+ json: { type: "boolean" },
95
+ });
96
+ const target = args._[0];
97
+ if (!target) throw exit("Usage: mdpockla note get <id|url> [--output FILE]");
98
+ const query = {};
99
+ if (args.version) {
100
+ const v = Number.parseInt(args.version, 10);
101
+ if (!Number.isFinite(v) || v <= 0) {
102
+ throw exit("--version must be a positive integer");
103
+ }
104
+ query.version = v;
105
+ }
106
+ const note = await api("GET", `/api/v1/notes/${encodeURIComponent(target)}`, {
107
+ query: Object.keys(query).length ? query : undefined,
108
+ });
109
+ if (args.json) {
110
+ process.stdout.write(JSON.stringify(note, null, 2) + "\n");
111
+ return;
112
+ }
113
+ if (args.output) {
114
+ await fs.writeFile(path.resolve(args.output), note.content);
115
+ process.stdout.write(`Wrote ${args.output}\n`);
116
+ } else {
117
+ process.stdout.write(note.content);
118
+ if (!note.content.endsWith("\n")) process.stdout.write("\n");
119
+ }
120
+ }
121
+
122
+ export async function noteList(argv) {
123
+ const args = parseArgs(argv, {
124
+ mine: { type: "boolean" },
125
+ shared: { type: "boolean" },
126
+ query: { type: "string" },
127
+ limit: { type: "string" },
128
+ json: { type: "boolean" },
129
+ });
130
+ const filter = args.mine ? "mine" : args.shared ? "shared" : "all";
131
+ const limit = args.limit ? Number.parseInt(args.limit, 10) : undefined;
132
+ const res = await api("GET", "/api/v1/notes", {
133
+ query: { filter, query: args.query, limit },
134
+ });
135
+ if (args.json) {
136
+ process.stdout.write(JSON.stringify(res, null, 2) + "\n");
137
+ return;
138
+ }
139
+ if (res.notes.length === 0) {
140
+ process.stdout.write("No notes.\n");
141
+ return;
142
+ }
143
+ for (const n of res.notes) {
144
+ process.stdout.write(
145
+ `${n.url} ${n.visibility.padEnd(7)} ${truncate(n.title, 60)}\n`
146
+ );
147
+ }
148
+ }
149
+
150
+ export async function noteDelete(argv) {
151
+ const target = argv[0];
152
+ if (!target) throw exit("Usage: mdpockla note delete <id|url>");
153
+ await api("DELETE", `/api/v1/notes/${encodeURIComponent(target)}`);
154
+ process.stdout.write("Deleted.\n");
155
+ }
156
+
157
+ export async function noteShare(argv) {
158
+ const args = parseArgs(argv, {
159
+ visibility: { type: "string" },
160
+ });
161
+ const target = args._[0];
162
+ if (!target || !args.visibility) {
163
+ throw exit("Usage: mdpockla note share <id|url> --visibility public|private");
164
+ }
165
+ const note = await api("POST", `/api/v1/notes/${encodeURIComponent(target)}/share`, {
166
+ body: { visibility: args.visibility },
167
+ });
168
+ process.stdout.write(note.url + "\n");
169
+ }
170
+
171
+ export async function noteCollaboratorAdd(argv) {
172
+ const [target, email] = argv;
173
+ if (!target || !email) {
174
+ throw exit("Usage: mdpockla note collaborator add <id|url> <email>");
175
+ }
176
+ const note = await api(
177
+ "POST",
178
+ `/api/v1/notes/${encodeURIComponent(target)}/collaborators`,
179
+ { body: { email } }
180
+ );
181
+ process.stdout.write(note.url + "\n");
182
+ }
183
+
184
+ export async function noteCollaboratorRemove(argv) {
185
+ const [target, email] = argv;
186
+ if (!target || !email) {
187
+ throw exit("Usage: mdpockla note collaborator remove <id|url> <email>");
188
+ }
189
+ const note = await api(
190
+ "DELETE",
191
+ `/api/v1/notes/${encodeURIComponent(target)}/collaborators`,
192
+ { query: { email } }
193
+ );
194
+ process.stdout.write(note.url + "\n");
195
+ }
196
+
197
+ // ------------------------------------------------------------
198
+ // note version <verb>
199
+ // ------------------------------------------------------------
200
+
201
+ export async function noteVersionSave(argv) {
202
+ const args = parseArgs(argv, {
203
+ label: { type: "string" },
204
+ json: { type: "boolean" },
205
+ });
206
+ const target = args._[0];
207
+ if (!target) throw exit("Usage: mdpockla note version save <id|url> [--label TEXT]");
208
+ const v = await api(
209
+ "POST",
210
+ `/api/v1/notes/${encodeURIComponent(target)}/versions`,
211
+ { body: { label: args.label ?? null } }
212
+ );
213
+ if (args.json) process.stdout.write(JSON.stringify(v, null, 2) + "\n");
214
+ else process.stdout.write(v.url + "\n");
215
+ }
216
+
217
+ export async function noteVersionList(argv) {
218
+ const args = parseArgs(argv, {
219
+ json: { type: "boolean" },
220
+ });
221
+ const target = args._[0];
222
+ if (!target) throw exit("Usage: mdpockla note version list <id|url>");
223
+ const res = await api(
224
+ "GET",
225
+ `/api/v1/notes/${encodeURIComponent(target)}/versions`
226
+ );
227
+ if (args.json) {
228
+ process.stdout.write(JSON.stringify(res, null, 2) + "\n");
229
+ return;
230
+ }
231
+ if (res.versions.length === 0) {
232
+ process.stdout.write("No saved versions.\n");
233
+ return;
234
+ }
235
+ for (const v of res.versions) {
236
+ const created = new Date(v.created_at).toLocaleDateString("en-US", {
237
+ month: "short",
238
+ day: "numeric",
239
+ year: "numeric",
240
+ });
241
+ const author = (v.author?.name || v.author?.email || "?").padEnd(16);
242
+ const label = v.label || (v.restored_from ? `restored from v${v.restored_from}` : "");
243
+ process.stdout.write(
244
+ `v${String(v.version).padEnd(3)} ${created.padEnd(14)} ${author} ${label}\n`
245
+ );
246
+ }
247
+ }
248
+
249
+ export async function noteVersionGet(argv) {
250
+ const args = parseArgs(argv, {
251
+ output: { type: "string" },
252
+ json: { type: "boolean" },
253
+ });
254
+ const target = args._[0];
255
+ const versionRaw = args._[1];
256
+ if (!target || !versionRaw) {
257
+ throw exit("Usage: mdpockla note version get <id|url> <version> [--output FILE]");
258
+ }
259
+ const version = Number.parseInt(versionRaw, 10);
260
+ if (!Number.isFinite(version) || version <= 0) {
261
+ throw exit("<version> must be a positive integer");
262
+ }
263
+ const v = await api(
264
+ "GET",
265
+ `/api/v1/notes/${encodeURIComponent(target)}/versions/${version}`
266
+ );
267
+ if (args.json) {
268
+ process.stdout.write(JSON.stringify(v, null, 2) + "\n");
269
+ return;
270
+ }
271
+ if (args.output) {
272
+ await fs.writeFile(path.resolve(args.output), v.content);
273
+ process.stdout.write(`Wrote ${args.output}\n`);
274
+ } else {
275
+ process.stdout.write(v.content);
276
+ if (!v.content.endsWith("\n")) process.stdout.write("\n");
277
+ }
278
+ }
279
+
280
+ export async function noteVersionRestore(argv) {
281
+ const args = parseArgs(argv, {
282
+ json: { type: "boolean" },
283
+ });
284
+ const target = args._[0];
285
+ const versionRaw = args._[1];
286
+ if (!target || !versionRaw) {
287
+ throw exit("Usage: mdpockla note version restore <id|url> <version>");
288
+ }
289
+ const version = Number.parseInt(versionRaw, 10);
290
+ if (!Number.isFinite(version) || version <= 0) {
291
+ throw exit("<version> must be a positive integer");
292
+ }
293
+ const v = await api(
294
+ "POST",
295
+ `/api/v1/notes/${encodeURIComponent(target)}/versions/${version}/restore`
296
+ );
297
+ if (args.json) process.stdout.write(JSON.stringify(v, null, 2) + "\n");
298
+ else process.stdout.write(v.url + "\n");
299
+ }
300
+
301
+ export async function noteVersionDelete(argv) {
302
+ const target = argv[0];
303
+ const versionRaw = argv[1];
304
+ if (!target || !versionRaw) {
305
+ throw exit("Usage: mdpockla note version delete <id|url> <version>");
306
+ }
307
+ const version = Number.parseInt(versionRaw, 10);
308
+ if (!Number.isFinite(version) || version <= 0) {
309
+ throw exit("<version> must be a positive integer");
310
+ }
311
+ await api(
312
+ "DELETE",
313
+ `/api/v1/notes/${encodeURIComponent(target)}/versions/${version}`
314
+ );
315
+ process.stdout.write(`Deleted v${version}.\n`);
316
+ }
317
+
318
+ function truncate(s, n) {
319
+ if (!s) return "";
320
+ return s.length > n ? s.slice(0, n - 1) + "…" : s;
321
+ }
322
+
323
+ function exit(message, code = 2) {
324
+ const err = new Error(message);
325
+ err.exitCode = code;
326
+ return err;
327
+ }