@notatemd/cli 4.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 +264 -0
- package/dist/cli.js +2956 -0
- package/package.json +46 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2956 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { createRequire as createRequire2 } from "module";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/commands/auth.ts
|
|
8
|
+
import { text, password as passwordPrompt, isCancel, cancel, spinner } from "@clack/prompts";
|
|
9
|
+
|
|
10
|
+
// src/lib/api.ts
|
|
11
|
+
var ApiError = class extends Error {
|
|
12
|
+
constructor(message, status, code) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = "ApiError";
|
|
17
|
+
}
|
|
18
|
+
status;
|
|
19
|
+
code;
|
|
20
|
+
};
|
|
21
|
+
function buildUrl(server, path, query) {
|
|
22
|
+
const base = server.endsWith("/") ? server : `${server}/`;
|
|
23
|
+
const url = new URL(path.replace(/^\//, ""), base);
|
|
24
|
+
if (query) {
|
|
25
|
+
for (const [key, value] of Object.entries(query)) {
|
|
26
|
+
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return url.toString();
|
|
30
|
+
}
|
|
31
|
+
async function apiFetch(server, path, req = {}) {
|
|
32
|
+
const headers = { ...req.headers };
|
|
33
|
+
if (req.body !== void 0) headers["content-type"] = "application/json";
|
|
34
|
+
if (req.accessToken) headers.authorization = `Bearer ${req.accessToken}`;
|
|
35
|
+
const res = await fetch(buildUrl(server, path, req.query), {
|
|
36
|
+
method: req.method ?? "GET",
|
|
37
|
+
headers,
|
|
38
|
+
body: req.body !== void 0 ? JSON.stringify(req.body) : void 0
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
let code;
|
|
42
|
+
let message;
|
|
43
|
+
try {
|
|
44
|
+
const parsed = await res.clone().json();
|
|
45
|
+
code = parsed.code;
|
|
46
|
+
message = parsed.message;
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
throw new ApiError(message ?? `Request failed with status ${res.status}`, res.status, code);
|
|
50
|
+
}
|
|
51
|
+
if (res.status === 204) return void 0;
|
|
52
|
+
return await res.json();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ../shared/dist/crypto/masterKey.js
|
|
56
|
+
var ARGON2ID_MASTER_KEY_PARAMS = {
|
|
57
|
+
/** Memory cost in KiB (19 MiB). */
|
|
58
|
+
memoryKiB: 19 * 1024,
|
|
59
|
+
/** Time cost (passes). */
|
|
60
|
+
iterations: 2,
|
|
61
|
+
/** Lanes. */
|
|
62
|
+
parallelism: 1,
|
|
63
|
+
/** Output length in bytes — 256 bits, matches AES-256-GCM in envelope.ts. */
|
|
64
|
+
keyLength: 32
|
|
65
|
+
};
|
|
66
|
+
function hexToKeyBytes(hex, keyLength) {
|
|
67
|
+
const clean2 = hex.replace(/\s+/g, "").toLowerCase();
|
|
68
|
+
if (clean2.length !== keyLength * 2) {
|
|
69
|
+
throw new Error(`argon2id backend returned ${clean2.length / 2} bytes, expected ${keyLength}`);
|
|
70
|
+
}
|
|
71
|
+
const out = new Uint8Array(keyLength);
|
|
72
|
+
for (let i = 0; i < keyLength; i++) {
|
|
73
|
+
out[i] = parseInt(clean2.slice(i * 2, i * 2 + 2), 16);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
async function deriveMasterKey(password, userId, backend) {
|
|
78
|
+
const hex = await backend({
|
|
79
|
+
password,
|
|
80
|
+
userId,
|
|
81
|
+
memoryKiB: ARGON2ID_MASTER_KEY_PARAMS.memoryKiB,
|
|
82
|
+
iterations: ARGON2ID_MASTER_KEY_PARAMS.iterations,
|
|
83
|
+
parallelism: ARGON2ID_MASTER_KEY_PARAMS.parallelism,
|
|
84
|
+
keyLength: ARGON2ID_MASTER_KEY_PARAMS.keyLength
|
|
85
|
+
});
|
|
86
|
+
return hexToKeyBytes(hex, ARGON2ID_MASTER_KEY_PARAMS.keyLength);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ../shared/dist/crypto/envelope.js
|
|
90
|
+
var VERSION = 1;
|
|
91
|
+
var GCM_IV_LENGTH = 12;
|
|
92
|
+
var AES_KEY_LENGTH = 32;
|
|
93
|
+
function base64Encode(bytes) {
|
|
94
|
+
let binary = "";
|
|
95
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
96
|
+
binary += String.fromCharCode(bytes[i]);
|
|
97
|
+
}
|
|
98
|
+
return btoa(binary);
|
|
99
|
+
}
|
|
100
|
+
function base64Decode(b64) {
|
|
101
|
+
const binary = atob(b64);
|
|
102
|
+
const out = new Uint8Array(binary.length);
|
|
103
|
+
for (let i = 0; i < binary.length; i++) {
|
|
104
|
+
out[i] = binary.charCodeAt(i);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
function getRandomBytes(length) {
|
|
109
|
+
const out = new Uint8Array(length);
|
|
110
|
+
globalThis.crypto.getRandomValues(out);
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
async function importAesKey(rawKey) {
|
|
114
|
+
if (rawKey.byteLength !== AES_KEY_LENGTH) {
|
|
115
|
+
throw new Error(`AES key must be ${AES_KEY_LENGTH} bytes, got ${rawKey.byteLength}`);
|
|
116
|
+
}
|
|
117
|
+
return globalThis.crypto.subtle.importKey("raw", rawKey, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
118
|
+
}
|
|
119
|
+
async function aesGcmEncrypt(plaintext, key) {
|
|
120
|
+
const iv = getRandomBytes(GCM_IV_LENGTH);
|
|
121
|
+
const ct = new Uint8Array(await globalThis.crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext));
|
|
122
|
+
return {
|
|
123
|
+
v: VERSION,
|
|
124
|
+
iv: base64Encode(iv),
|
|
125
|
+
ct: base64Encode(ct)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function aesGcmDecrypt(payload, key) {
|
|
129
|
+
if (payload.v !== VERSION) {
|
|
130
|
+
throw new Error(`Unsupported envelope version: ${payload.v}`);
|
|
131
|
+
}
|
|
132
|
+
const iv = base64Decode(payload.iv);
|
|
133
|
+
const ct = base64Decode(payload.ct);
|
|
134
|
+
const plain = new Uint8Array(await globalThis.crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct));
|
|
135
|
+
return plain;
|
|
136
|
+
}
|
|
137
|
+
async function unwrapDEK(wrapped, mk) {
|
|
138
|
+
const mkKey = await importAesKey(mk);
|
|
139
|
+
return aesGcmDecrypt(wrapped, mkKey);
|
|
140
|
+
}
|
|
141
|
+
async function encryptBlob(plaintext, dek) {
|
|
142
|
+
const dekKey2 = await importAesKey(dek);
|
|
143
|
+
const bytes = new TextEncoder().encode(plaintext);
|
|
144
|
+
return aesGcmEncrypt(bytes, dekKey2);
|
|
145
|
+
}
|
|
146
|
+
async function decryptBlob(payload, dek) {
|
|
147
|
+
const dekKey2 = await importAesKey(dek);
|
|
148
|
+
const bytes = await aesGcmDecrypt(payload, dekKey2);
|
|
149
|
+
return new TextDecoder().decode(bytes);
|
|
150
|
+
}
|
|
151
|
+
function isEncryptedPayload(value) {
|
|
152
|
+
return typeof value === "object" && value !== null && "v" in value && "iv" in value && "ct" in value && typeof value.v === "number" && typeof value.iv === "string" && typeof value.ct === "string";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ../../node_modules/@noble/hashes/utils.js
|
|
156
|
+
function isBytes(a) {
|
|
157
|
+
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
158
|
+
}
|
|
159
|
+
function anumber(n, title = "") {
|
|
160
|
+
if (typeof n !== "number") {
|
|
161
|
+
const prefix = title && `"${title}" `;
|
|
162
|
+
throw new TypeError(`${prefix}expected number, got ${typeof n}`);
|
|
163
|
+
}
|
|
164
|
+
if (!Number.isSafeInteger(n) || n < 0) {
|
|
165
|
+
const prefix = title && `"${title}" `;
|
|
166
|
+
throw new RangeError(`${prefix}expected integer >= 0, got ${n}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function abytes(value, length, title = "") {
|
|
170
|
+
const bytes = isBytes(value);
|
|
171
|
+
const len = value?.length;
|
|
172
|
+
const needsLen = length !== void 0;
|
|
173
|
+
if (!bytes || needsLen && len !== length) {
|
|
174
|
+
const prefix = title && `"${title}" `;
|
|
175
|
+
const ofLen = needsLen ? ` of length ${length}` : "";
|
|
176
|
+
const got = bytes ? `length=${len}` : `type=${typeof value}`;
|
|
177
|
+
const message = prefix + "expected Uint8Array" + ofLen + ", got " + got;
|
|
178
|
+
if (!bytes)
|
|
179
|
+
throw new TypeError(message);
|
|
180
|
+
throw new RangeError(message);
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
function ahash(h) {
|
|
185
|
+
if (typeof h !== "function" || typeof h.create !== "function")
|
|
186
|
+
throw new TypeError("Hash must wrapped by utils.createHasher");
|
|
187
|
+
anumber(h.outputLen);
|
|
188
|
+
anumber(h.blockLen);
|
|
189
|
+
if (h.outputLen < 1)
|
|
190
|
+
throw new Error('"outputLen" must be >= 1');
|
|
191
|
+
if (h.blockLen < 1)
|
|
192
|
+
throw new Error('"blockLen" must be >= 1');
|
|
193
|
+
}
|
|
194
|
+
function aexists(instance, checkFinished = true) {
|
|
195
|
+
if (instance.destroyed)
|
|
196
|
+
throw new Error("Hash instance has been destroyed");
|
|
197
|
+
if (checkFinished && instance.finished)
|
|
198
|
+
throw new Error("Hash#digest() has already been called");
|
|
199
|
+
}
|
|
200
|
+
function aoutput(out, instance) {
|
|
201
|
+
abytes(out, void 0, "digestInto() output");
|
|
202
|
+
const min = instance.outputLen;
|
|
203
|
+
if (out.length < min) {
|
|
204
|
+
throw new RangeError('"digestInto() output" expected to be of length >=' + min);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function clean(...arrays) {
|
|
208
|
+
for (let i = 0; i < arrays.length; i++) {
|
|
209
|
+
arrays[i].fill(0);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function createView(arr) {
|
|
213
|
+
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
214
|
+
}
|
|
215
|
+
function rotr(word, shift) {
|
|
216
|
+
return word << 32 - shift | word >>> shift;
|
|
217
|
+
}
|
|
218
|
+
function createHasher(hashCons, info = {}) {
|
|
219
|
+
const hashC = (msg, opts) => hashCons(opts).update(msg).digest();
|
|
220
|
+
const tmp = hashCons(void 0);
|
|
221
|
+
hashC.outputLen = tmp.outputLen;
|
|
222
|
+
hashC.blockLen = tmp.blockLen;
|
|
223
|
+
hashC.canXOF = tmp.canXOF;
|
|
224
|
+
hashC.create = (opts) => hashCons(opts);
|
|
225
|
+
Object.assign(hashC, info);
|
|
226
|
+
return Object.freeze(hashC);
|
|
227
|
+
}
|
|
228
|
+
var oidNist = (suffix) => ({
|
|
229
|
+
// Current NIST hashAlgs suffixes used here fit in one DER subidentifier octet.
|
|
230
|
+
// Larger suffix values would need base-128 OID encoding and a different length byte.
|
|
231
|
+
oid: Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2, suffix])
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ../../node_modules/@noble/hashes/hmac.js
|
|
235
|
+
var _HMAC = class {
|
|
236
|
+
oHash;
|
|
237
|
+
iHash;
|
|
238
|
+
blockLen;
|
|
239
|
+
outputLen;
|
|
240
|
+
canXOF = false;
|
|
241
|
+
finished = false;
|
|
242
|
+
destroyed = false;
|
|
243
|
+
constructor(hash, key) {
|
|
244
|
+
ahash(hash);
|
|
245
|
+
abytes(key, void 0, "key");
|
|
246
|
+
this.iHash = hash.create();
|
|
247
|
+
if (typeof this.iHash.update !== "function")
|
|
248
|
+
throw new Error("Expected instance of class which extends utils.Hash");
|
|
249
|
+
this.blockLen = this.iHash.blockLen;
|
|
250
|
+
this.outputLen = this.iHash.outputLen;
|
|
251
|
+
const blockLen = this.blockLen;
|
|
252
|
+
const pad = new Uint8Array(blockLen);
|
|
253
|
+
pad.set(key.length > blockLen ? hash.create().update(key).digest() : key);
|
|
254
|
+
for (let i = 0; i < pad.length; i++)
|
|
255
|
+
pad[i] ^= 54;
|
|
256
|
+
this.iHash.update(pad);
|
|
257
|
+
this.oHash = hash.create();
|
|
258
|
+
for (let i = 0; i < pad.length; i++)
|
|
259
|
+
pad[i] ^= 54 ^ 92;
|
|
260
|
+
this.oHash.update(pad);
|
|
261
|
+
clean(pad);
|
|
262
|
+
}
|
|
263
|
+
update(buf) {
|
|
264
|
+
aexists(this);
|
|
265
|
+
this.iHash.update(buf);
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
digestInto(out) {
|
|
269
|
+
aexists(this);
|
|
270
|
+
aoutput(out, this);
|
|
271
|
+
this.finished = true;
|
|
272
|
+
const buf = out.subarray(0, this.outputLen);
|
|
273
|
+
this.iHash.digestInto(buf);
|
|
274
|
+
this.oHash.update(buf);
|
|
275
|
+
this.oHash.digestInto(buf);
|
|
276
|
+
this.destroy();
|
|
277
|
+
}
|
|
278
|
+
digest() {
|
|
279
|
+
const out = new Uint8Array(this.oHash.outputLen);
|
|
280
|
+
this.digestInto(out);
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
_cloneInto(to) {
|
|
284
|
+
to ||= Object.create(Object.getPrototypeOf(this), {});
|
|
285
|
+
const { oHash, iHash, finished, destroyed, blockLen, outputLen } = this;
|
|
286
|
+
to = to;
|
|
287
|
+
to.finished = finished;
|
|
288
|
+
to.destroyed = destroyed;
|
|
289
|
+
to.blockLen = blockLen;
|
|
290
|
+
to.outputLen = outputLen;
|
|
291
|
+
to.oHash = oHash._cloneInto(to.oHash);
|
|
292
|
+
to.iHash = iHash._cloneInto(to.iHash);
|
|
293
|
+
return to;
|
|
294
|
+
}
|
|
295
|
+
clone() {
|
|
296
|
+
return this._cloneInto();
|
|
297
|
+
}
|
|
298
|
+
destroy() {
|
|
299
|
+
this.destroyed = true;
|
|
300
|
+
this.oHash.destroy();
|
|
301
|
+
this.iHash.destroy();
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var hmac = /* @__PURE__ */ (() => {
|
|
305
|
+
const hmac_ = ((hash, key, message) => new _HMAC(hash, key).update(message).digest());
|
|
306
|
+
hmac_.create = (hash, key) => new _HMAC(hash, key);
|
|
307
|
+
return hmac_;
|
|
308
|
+
})();
|
|
309
|
+
|
|
310
|
+
// ../../node_modules/@noble/hashes/hkdf.js
|
|
311
|
+
function extract(hash, ikm, salt) {
|
|
312
|
+
ahash(hash);
|
|
313
|
+
if (salt === void 0)
|
|
314
|
+
salt = new Uint8Array(hash.outputLen);
|
|
315
|
+
return hmac(hash, salt, ikm);
|
|
316
|
+
}
|
|
317
|
+
var HKDF_COUNTER = /* @__PURE__ */ Uint8Array.of(0);
|
|
318
|
+
var EMPTY_BUFFER = /* @__PURE__ */ Uint8Array.of();
|
|
319
|
+
function expand(hash, prk, info, length = 32) {
|
|
320
|
+
ahash(hash);
|
|
321
|
+
anumber(length, "length");
|
|
322
|
+
abytes(prk, void 0, "prk");
|
|
323
|
+
const olen = hash.outputLen;
|
|
324
|
+
if (prk.length < olen)
|
|
325
|
+
throw new Error('"prk" must be at least HashLen octets');
|
|
326
|
+
if (length > 255 * olen)
|
|
327
|
+
throw new Error("Length must be <= 255*HashLen");
|
|
328
|
+
const blocks = Math.ceil(length / olen);
|
|
329
|
+
if (info === void 0)
|
|
330
|
+
info = EMPTY_BUFFER;
|
|
331
|
+
else
|
|
332
|
+
abytes(info, void 0, "info");
|
|
333
|
+
const okm = new Uint8Array(blocks * olen);
|
|
334
|
+
const HMAC = hmac.create(hash, prk);
|
|
335
|
+
const HMACTmp = HMAC._cloneInto();
|
|
336
|
+
const T = new Uint8Array(HMAC.outputLen);
|
|
337
|
+
for (let counter = 0; counter < blocks; counter++) {
|
|
338
|
+
HKDF_COUNTER[0] = counter + 1;
|
|
339
|
+
HMACTmp.update(counter === 0 ? EMPTY_BUFFER : T).update(info).update(HKDF_COUNTER).digestInto(T);
|
|
340
|
+
okm.set(T, olen * counter);
|
|
341
|
+
HMAC._cloneInto(HMACTmp);
|
|
342
|
+
}
|
|
343
|
+
HMAC.destroy();
|
|
344
|
+
HMACTmp.destroy();
|
|
345
|
+
clean(T, HKDF_COUNTER);
|
|
346
|
+
return okm.slice(0, length);
|
|
347
|
+
}
|
|
348
|
+
var hkdf = (hash, ikm, salt, info, length) => expand(hash, extract(hash, ikm, salt), info, length);
|
|
349
|
+
|
|
350
|
+
// ../../node_modules/@noble/hashes/_md.js
|
|
351
|
+
function Chi(a, b, c) {
|
|
352
|
+
return a & b ^ ~a & c;
|
|
353
|
+
}
|
|
354
|
+
function Maj(a, b, c) {
|
|
355
|
+
return a & b ^ a & c ^ b & c;
|
|
356
|
+
}
|
|
357
|
+
var HashMD = class {
|
|
358
|
+
blockLen;
|
|
359
|
+
outputLen;
|
|
360
|
+
canXOF = false;
|
|
361
|
+
padOffset;
|
|
362
|
+
isLE;
|
|
363
|
+
// For partial updates less than block size
|
|
364
|
+
buffer;
|
|
365
|
+
view;
|
|
366
|
+
finished = false;
|
|
367
|
+
length = 0;
|
|
368
|
+
pos = 0;
|
|
369
|
+
destroyed = false;
|
|
370
|
+
constructor(blockLen, outputLen, padOffset, isLE) {
|
|
371
|
+
this.blockLen = blockLen;
|
|
372
|
+
this.outputLen = outputLen;
|
|
373
|
+
this.padOffset = padOffset;
|
|
374
|
+
this.isLE = isLE;
|
|
375
|
+
this.buffer = new Uint8Array(blockLen);
|
|
376
|
+
this.view = createView(this.buffer);
|
|
377
|
+
}
|
|
378
|
+
update(data) {
|
|
379
|
+
aexists(this);
|
|
380
|
+
abytes(data);
|
|
381
|
+
const { view, buffer, blockLen } = this;
|
|
382
|
+
const len = data.length;
|
|
383
|
+
for (let pos = 0; pos < len; ) {
|
|
384
|
+
const take = Math.min(blockLen - this.pos, len - pos);
|
|
385
|
+
if (take === blockLen) {
|
|
386
|
+
const dataView = createView(data);
|
|
387
|
+
for (; blockLen <= len - pos; pos += blockLen)
|
|
388
|
+
this.process(dataView, pos);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
buffer.set(data.subarray(pos, pos + take), this.pos);
|
|
392
|
+
this.pos += take;
|
|
393
|
+
pos += take;
|
|
394
|
+
if (this.pos === blockLen) {
|
|
395
|
+
this.process(view, 0);
|
|
396
|
+
this.pos = 0;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
this.length += data.length;
|
|
400
|
+
this.roundClean();
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
digestInto(out) {
|
|
404
|
+
aexists(this);
|
|
405
|
+
aoutput(out, this);
|
|
406
|
+
this.finished = true;
|
|
407
|
+
const { buffer, view, blockLen, isLE } = this;
|
|
408
|
+
let { pos } = this;
|
|
409
|
+
buffer[pos++] = 128;
|
|
410
|
+
clean(this.buffer.subarray(pos));
|
|
411
|
+
if (this.padOffset > blockLen - pos) {
|
|
412
|
+
this.process(view, 0);
|
|
413
|
+
pos = 0;
|
|
414
|
+
}
|
|
415
|
+
for (let i = pos; i < blockLen; i++)
|
|
416
|
+
buffer[i] = 0;
|
|
417
|
+
view.setBigUint64(blockLen - 8, BigInt(this.length * 8), isLE);
|
|
418
|
+
this.process(view, 0);
|
|
419
|
+
const oview = createView(out);
|
|
420
|
+
const len = this.outputLen;
|
|
421
|
+
if (len % 4)
|
|
422
|
+
throw new Error("_sha2: outputLen must be aligned to 32bit");
|
|
423
|
+
const outLen = len / 4;
|
|
424
|
+
const state = this.get();
|
|
425
|
+
if (outLen > state.length)
|
|
426
|
+
throw new Error("_sha2: outputLen bigger than state");
|
|
427
|
+
for (let i = 0; i < outLen; i++)
|
|
428
|
+
oview.setUint32(4 * i, state[i], isLE);
|
|
429
|
+
}
|
|
430
|
+
digest() {
|
|
431
|
+
const { buffer, outputLen } = this;
|
|
432
|
+
this.digestInto(buffer);
|
|
433
|
+
const res = buffer.slice(0, outputLen);
|
|
434
|
+
this.destroy();
|
|
435
|
+
return res;
|
|
436
|
+
}
|
|
437
|
+
_cloneInto(to) {
|
|
438
|
+
to ||= new this.constructor();
|
|
439
|
+
to.set(...this.get());
|
|
440
|
+
const { blockLen, buffer, length, finished, destroyed, pos } = this;
|
|
441
|
+
to.destroyed = destroyed;
|
|
442
|
+
to.finished = finished;
|
|
443
|
+
to.length = length;
|
|
444
|
+
to.pos = pos;
|
|
445
|
+
if (length % blockLen)
|
|
446
|
+
to.buffer.set(buffer);
|
|
447
|
+
return to;
|
|
448
|
+
}
|
|
449
|
+
clone() {
|
|
450
|
+
return this._cloneInto();
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
var SHA256_IV = /* @__PURE__ */ Uint32Array.from([
|
|
454
|
+
1779033703,
|
|
455
|
+
3144134277,
|
|
456
|
+
1013904242,
|
|
457
|
+
2773480762,
|
|
458
|
+
1359893119,
|
|
459
|
+
2600822924,
|
|
460
|
+
528734635,
|
|
461
|
+
1541459225
|
|
462
|
+
]);
|
|
463
|
+
|
|
464
|
+
// ../../node_modules/@noble/hashes/sha2.js
|
|
465
|
+
var SHA256_K = /* @__PURE__ */ Uint32Array.from([
|
|
466
|
+
1116352408,
|
|
467
|
+
1899447441,
|
|
468
|
+
3049323471,
|
|
469
|
+
3921009573,
|
|
470
|
+
961987163,
|
|
471
|
+
1508970993,
|
|
472
|
+
2453635748,
|
|
473
|
+
2870763221,
|
|
474
|
+
3624381080,
|
|
475
|
+
310598401,
|
|
476
|
+
607225278,
|
|
477
|
+
1426881987,
|
|
478
|
+
1925078388,
|
|
479
|
+
2162078206,
|
|
480
|
+
2614888103,
|
|
481
|
+
3248222580,
|
|
482
|
+
3835390401,
|
|
483
|
+
4022224774,
|
|
484
|
+
264347078,
|
|
485
|
+
604807628,
|
|
486
|
+
770255983,
|
|
487
|
+
1249150122,
|
|
488
|
+
1555081692,
|
|
489
|
+
1996064986,
|
|
490
|
+
2554220882,
|
|
491
|
+
2821834349,
|
|
492
|
+
2952996808,
|
|
493
|
+
3210313671,
|
|
494
|
+
3336571891,
|
|
495
|
+
3584528711,
|
|
496
|
+
113926993,
|
|
497
|
+
338241895,
|
|
498
|
+
666307205,
|
|
499
|
+
773529912,
|
|
500
|
+
1294757372,
|
|
501
|
+
1396182291,
|
|
502
|
+
1695183700,
|
|
503
|
+
1986661051,
|
|
504
|
+
2177026350,
|
|
505
|
+
2456956037,
|
|
506
|
+
2730485921,
|
|
507
|
+
2820302411,
|
|
508
|
+
3259730800,
|
|
509
|
+
3345764771,
|
|
510
|
+
3516065817,
|
|
511
|
+
3600352804,
|
|
512
|
+
4094571909,
|
|
513
|
+
275423344,
|
|
514
|
+
430227734,
|
|
515
|
+
506948616,
|
|
516
|
+
659060556,
|
|
517
|
+
883997877,
|
|
518
|
+
958139571,
|
|
519
|
+
1322822218,
|
|
520
|
+
1537002063,
|
|
521
|
+
1747873779,
|
|
522
|
+
1955562222,
|
|
523
|
+
2024104815,
|
|
524
|
+
2227730452,
|
|
525
|
+
2361852424,
|
|
526
|
+
2428436474,
|
|
527
|
+
2756734187,
|
|
528
|
+
3204031479,
|
|
529
|
+
3329325298
|
|
530
|
+
]);
|
|
531
|
+
var SHA256_W = /* @__PURE__ */ new Uint32Array(64);
|
|
532
|
+
var SHA2_32B = class extends HashMD {
|
|
533
|
+
constructor(outputLen) {
|
|
534
|
+
super(64, outputLen, 8, false);
|
|
535
|
+
}
|
|
536
|
+
get() {
|
|
537
|
+
const { A, B, C, D, E, F, G, H } = this;
|
|
538
|
+
return [A, B, C, D, E, F, G, H];
|
|
539
|
+
}
|
|
540
|
+
// prettier-ignore
|
|
541
|
+
set(A, B, C, D, E, F, G, H) {
|
|
542
|
+
this.A = A | 0;
|
|
543
|
+
this.B = B | 0;
|
|
544
|
+
this.C = C | 0;
|
|
545
|
+
this.D = D | 0;
|
|
546
|
+
this.E = E | 0;
|
|
547
|
+
this.F = F | 0;
|
|
548
|
+
this.G = G | 0;
|
|
549
|
+
this.H = H | 0;
|
|
550
|
+
}
|
|
551
|
+
process(view, offset) {
|
|
552
|
+
for (let i = 0; i < 16; i++, offset += 4)
|
|
553
|
+
SHA256_W[i] = view.getUint32(offset, false);
|
|
554
|
+
for (let i = 16; i < 64; i++) {
|
|
555
|
+
const W15 = SHA256_W[i - 15];
|
|
556
|
+
const W2 = SHA256_W[i - 2];
|
|
557
|
+
const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ W15 >>> 3;
|
|
558
|
+
const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ W2 >>> 10;
|
|
559
|
+
SHA256_W[i] = s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16] | 0;
|
|
560
|
+
}
|
|
561
|
+
let { A, B, C, D, E, F, G, H } = this;
|
|
562
|
+
for (let i = 0; i < 64; i++) {
|
|
563
|
+
const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25);
|
|
564
|
+
const T1 = H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i] | 0;
|
|
565
|
+
const sigma0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22);
|
|
566
|
+
const T2 = sigma0 + Maj(A, B, C) | 0;
|
|
567
|
+
H = G;
|
|
568
|
+
G = F;
|
|
569
|
+
F = E;
|
|
570
|
+
E = D + T1 | 0;
|
|
571
|
+
D = C;
|
|
572
|
+
C = B;
|
|
573
|
+
B = A;
|
|
574
|
+
A = T1 + T2 | 0;
|
|
575
|
+
}
|
|
576
|
+
A = A + this.A | 0;
|
|
577
|
+
B = B + this.B | 0;
|
|
578
|
+
C = C + this.C | 0;
|
|
579
|
+
D = D + this.D | 0;
|
|
580
|
+
E = E + this.E | 0;
|
|
581
|
+
F = F + this.F | 0;
|
|
582
|
+
G = G + this.G | 0;
|
|
583
|
+
H = H + this.H | 0;
|
|
584
|
+
this.set(A, B, C, D, E, F, G, H);
|
|
585
|
+
}
|
|
586
|
+
roundClean() {
|
|
587
|
+
clean(SHA256_W);
|
|
588
|
+
}
|
|
589
|
+
destroy() {
|
|
590
|
+
this.destroyed = true;
|
|
591
|
+
this.set(0, 0, 0, 0, 0, 0, 0, 0);
|
|
592
|
+
clean(this.buffer);
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
var _SHA256 = class extends SHA2_32B {
|
|
596
|
+
// We cannot use array here since array allows indexing by variable
|
|
597
|
+
// which means optimizer/compiler cannot use registers.
|
|
598
|
+
A = SHA256_IV[0] | 0;
|
|
599
|
+
B = SHA256_IV[1] | 0;
|
|
600
|
+
C = SHA256_IV[2] | 0;
|
|
601
|
+
D = SHA256_IV[3] | 0;
|
|
602
|
+
E = SHA256_IV[4] | 0;
|
|
603
|
+
F = SHA256_IV[5] | 0;
|
|
604
|
+
G = SHA256_IV[6] | 0;
|
|
605
|
+
H = SHA256_IV[7] | 0;
|
|
606
|
+
constructor() {
|
|
607
|
+
super(32);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
var sha256 = /* @__PURE__ */ createHasher(
|
|
611
|
+
() => new _SHA256(),
|
|
612
|
+
/* @__PURE__ */ oidNist(1)
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
// ../shared/dist/crypto/kdf.js
|
|
616
|
+
var AES_KEY_LENGTH2 = 32;
|
|
617
|
+
function deriveKey32(ikm, info) {
|
|
618
|
+
return hkdf(sha256, ikm, void 0, new TextEncoder().encode(info), AES_KEY_LENGTH2);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ../shared/dist/crypto/contentKey.js
|
|
622
|
+
var NOTE_CONTENT_INFO = "notate/note-content/v1";
|
|
623
|
+
function deriveContentKey(dek) {
|
|
624
|
+
return deriveKey32(dek, NOTE_CONTENT_INFO);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ../shared/dist/crypto/noteFields.js
|
|
628
|
+
var ENCRYPTED_NOTE_STRING_FIELDS = [
|
|
629
|
+
"title",
|
|
630
|
+
"content",
|
|
631
|
+
"summary",
|
|
632
|
+
"transcript"
|
|
633
|
+
];
|
|
634
|
+
function parseEncryptedString(raw) {
|
|
635
|
+
try {
|
|
636
|
+
const parsed = JSON.parse(raw);
|
|
637
|
+
return isEncryptedPayload(parsed) ? parsed : null;
|
|
638
|
+
} catch {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async function encryptNoteFields(note, contentKey) {
|
|
643
|
+
const out = { ...note };
|
|
644
|
+
for (const field of ENCRYPTED_NOTE_STRING_FIELDS) {
|
|
645
|
+
const value = note[field];
|
|
646
|
+
if (typeof value === "string" && parseEncryptedString(value) === null) {
|
|
647
|
+
out[field] = JSON.stringify(await encryptBlob(value, contentKey));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (Array.isArray(note.tags)) {
|
|
651
|
+
out.tags = await encryptBlob(JSON.stringify(note.tags), contentKey);
|
|
652
|
+
}
|
|
653
|
+
return out;
|
|
654
|
+
}
|
|
655
|
+
async function decryptNoteFields(note, contentKey) {
|
|
656
|
+
const out = { ...note };
|
|
657
|
+
for (const field of ENCRYPTED_NOTE_STRING_FIELDS) {
|
|
658
|
+
const value = note[field];
|
|
659
|
+
if (typeof value === "string") {
|
|
660
|
+
const payload = parseEncryptedString(value);
|
|
661
|
+
if (payload) {
|
|
662
|
+
out[field] = await decryptBlob(payload, contentKey);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (isEncryptedPayload(note.tags)) {
|
|
667
|
+
out.tags = JSON.parse(await decryptBlob(note.tags, contentKey));
|
|
668
|
+
}
|
|
669
|
+
return out;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/lib/argon2.ts
|
|
673
|
+
import { hashRaw, Algorithm, Version } from "@node-rs/argon2";
|
|
674
|
+
var nodeArgon2Backend = async (input) => {
|
|
675
|
+
const raw = await hashRaw(input.password, {
|
|
676
|
+
algorithm: Algorithm.Argon2id,
|
|
677
|
+
version: Version.V0x13,
|
|
678
|
+
memoryCost: input.memoryKiB,
|
|
679
|
+
timeCost: input.iterations,
|
|
680
|
+
parallelism: input.parallelism,
|
|
681
|
+
outputLen: input.keyLength,
|
|
682
|
+
salt: new TextEncoder().encode(input.userId)
|
|
683
|
+
});
|
|
684
|
+
return Buffer.from(raw).toString("hex");
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
// src/lib/crypto.ts
|
|
688
|
+
async function unlockDEK(password, userId, wrappedDEK) {
|
|
689
|
+
const mk = await deriveMasterKey(password, userId, nodeArgon2Backend);
|
|
690
|
+
const parsed = JSON.parse(wrappedDEK);
|
|
691
|
+
if (!isEncryptedPayload(parsed)) {
|
|
692
|
+
throw new Error("wrappedDEK is not a valid encrypted payload");
|
|
693
|
+
}
|
|
694
|
+
return unwrapDEK(parsed, mk);
|
|
695
|
+
}
|
|
696
|
+
function encryptNote(note, contentKey) {
|
|
697
|
+
return encryptNoteFields(note, contentKey);
|
|
698
|
+
}
|
|
699
|
+
function decryptNote(note, contentKey) {
|
|
700
|
+
return decryptNoteFields(note, contentKey);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/lib/keychain.ts
|
|
704
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
|
|
705
|
+
import { homedir } from "os";
|
|
706
|
+
import { join } from "path";
|
|
707
|
+
import { createRequire } from "module";
|
|
708
|
+
var require2 = createRequire(import.meta.url);
|
|
709
|
+
var SERVICE = "notate-cli";
|
|
710
|
+
var KeychainStore = class {
|
|
711
|
+
get(key) {
|
|
712
|
+
const { Entry } = loadKeyring();
|
|
713
|
+
return new Entry(SERVICE, key).getPassword();
|
|
714
|
+
}
|
|
715
|
+
set(key, value) {
|
|
716
|
+
const { Entry } = loadKeyring();
|
|
717
|
+
new Entry(SERVICE, key).setPassword(value);
|
|
718
|
+
}
|
|
719
|
+
delete(key) {
|
|
720
|
+
const { Entry } = loadKeyring();
|
|
721
|
+
try {
|
|
722
|
+
new Entry(SERVICE, key).deleteCredential();
|
|
723
|
+
} catch {
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
var FileStore = class {
|
|
728
|
+
constructor(dir = defaultConfigDir()) {
|
|
729
|
+
this.dir = dir;
|
|
730
|
+
}
|
|
731
|
+
dir;
|
|
732
|
+
get file() {
|
|
733
|
+
return join(this.dir, "secrets.json");
|
|
734
|
+
}
|
|
735
|
+
read() {
|
|
736
|
+
if (!existsSync(this.file)) return {};
|
|
737
|
+
try {
|
|
738
|
+
return JSON.parse(readFileSync(this.file, "utf-8"));
|
|
739
|
+
} catch {
|
|
740
|
+
return {};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
write(data) {
|
|
744
|
+
mkdirSync(this.dir, { recursive: true });
|
|
745
|
+
writeFileSync(this.file, JSON.stringify(data, null, 2), { mode: 384 });
|
|
746
|
+
}
|
|
747
|
+
get(key) {
|
|
748
|
+
return this.read()[key] ?? null;
|
|
749
|
+
}
|
|
750
|
+
set(key, value) {
|
|
751
|
+
const data = this.read();
|
|
752
|
+
data[key] = value;
|
|
753
|
+
this.write(data);
|
|
754
|
+
}
|
|
755
|
+
delete(key) {
|
|
756
|
+
const data = this.read();
|
|
757
|
+
if (key in data) {
|
|
758
|
+
delete data[key];
|
|
759
|
+
this.write(data);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
var keyringModule = null;
|
|
764
|
+
function loadKeyring() {
|
|
765
|
+
if (!keyringModule) {
|
|
766
|
+
keyringModule = require2("@napi-rs/keyring");
|
|
767
|
+
}
|
|
768
|
+
return keyringModule;
|
|
769
|
+
}
|
|
770
|
+
function defaultConfigDir() {
|
|
771
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
772
|
+
return xdg ? join(xdg, "notate-cli") : join(homedir(), ".config", "notate-cli");
|
|
773
|
+
}
|
|
774
|
+
function resolveStore() {
|
|
775
|
+
const forced = process.env.NOTATE_CLI_STORE;
|
|
776
|
+
if (forced === "file") return new FileStore();
|
|
777
|
+
if (forced === "keychain") return new KeychainStore();
|
|
778
|
+
try {
|
|
779
|
+
const { Entry } = loadKeyring();
|
|
780
|
+
new Entry(SERVICE, "__probe__").getPassword();
|
|
781
|
+
return new KeychainStore();
|
|
782
|
+
} catch {
|
|
783
|
+
return new FileStore();
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
var SESSION_KEY = "session";
|
|
787
|
+
var dekKey = (userId) => `dek:${userId}`;
|
|
788
|
+
function saveSession(store, session) {
|
|
789
|
+
store.set(SESSION_KEY, JSON.stringify(session));
|
|
790
|
+
}
|
|
791
|
+
function loadSession(store) {
|
|
792
|
+
const raw = store.get(SESSION_KEY);
|
|
793
|
+
if (!raw) return null;
|
|
794
|
+
try {
|
|
795
|
+
return JSON.parse(raw);
|
|
796
|
+
} catch {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function clearSession(store) {
|
|
801
|
+
store.delete(SESSION_KEY);
|
|
802
|
+
}
|
|
803
|
+
function saveDEK(store, userId, dek) {
|
|
804
|
+
store.set(dekKey(userId), Buffer.from(dek).toString("base64"));
|
|
805
|
+
}
|
|
806
|
+
function loadDEK(store, userId) {
|
|
807
|
+
const raw = store.get(dekKey(userId));
|
|
808
|
+
if (!raw) return null;
|
|
809
|
+
const bytes = new Uint8Array(Buffer.from(raw, "base64"));
|
|
810
|
+
return bytes.byteLength === 32 ? bytes : null;
|
|
811
|
+
}
|
|
812
|
+
function clearDEK(store, userId) {
|
|
813
|
+
store.delete(dekKey(userId));
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/lib/auth.ts
|
|
817
|
+
function requireSession(store) {
|
|
818
|
+
const session = loadSession(store);
|
|
819
|
+
if (!session) {
|
|
820
|
+
throw new ApiError("Not logged in. Run `notate login`.", 401, "no_session");
|
|
821
|
+
}
|
|
822
|
+
return session;
|
|
823
|
+
}
|
|
824
|
+
function authContext(store) {
|
|
825
|
+
const session = requireSession(store);
|
|
826
|
+
return { authed: createAuthedClient(store), session };
|
|
827
|
+
}
|
|
828
|
+
function requestLogin(server, email, password) {
|
|
829
|
+
return apiFetch(server, "/auth/login", {
|
|
830
|
+
method: "POST",
|
|
831
|
+
body: { email, password }
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
function verifyTotp(server, totpToken, code) {
|
|
835
|
+
return apiFetch(server, "/auth/totp/verify", {
|
|
836
|
+
method: "POST",
|
|
837
|
+
body: { totpToken, code }
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
async function finalizeLogin(store, server, resp, password) {
|
|
841
|
+
if (!resp.refreshToken) {
|
|
842
|
+
throw new Error("Login response did not include a refresh token.");
|
|
843
|
+
}
|
|
844
|
+
const session = {
|
|
845
|
+
server,
|
|
846
|
+
userId: resp.user.id,
|
|
847
|
+
email: resp.user.email,
|
|
848
|
+
accessToken: resp.accessToken,
|
|
849
|
+
refreshToken: resp.refreshToken,
|
|
850
|
+
encryptionEnabled: resp.user.encryptionEnabled ?? false
|
|
851
|
+
};
|
|
852
|
+
saveSession(store, session);
|
|
853
|
+
if (resp.user.encryptionEnabled && resp.wrappedDEK) {
|
|
854
|
+
const dek = await unlockDEK(password, resp.user.id, resp.wrappedDEK);
|
|
855
|
+
saveDEK(store, resp.user.id, dek);
|
|
856
|
+
}
|
|
857
|
+
return session;
|
|
858
|
+
}
|
|
859
|
+
async function refreshSession(store, session) {
|
|
860
|
+
const resp = await apiFetch(session.server, "/auth/refresh", {
|
|
861
|
+
method: "POST",
|
|
862
|
+
body: { refreshToken: session.refreshToken },
|
|
863
|
+
headers: { "x-requested-with": "XMLHttpRequest" }
|
|
864
|
+
});
|
|
865
|
+
const updated = {
|
|
866
|
+
...session,
|
|
867
|
+
accessToken: resp.accessToken,
|
|
868
|
+
refreshToken: resp.refreshToken ?? session.refreshToken
|
|
869
|
+
};
|
|
870
|
+
saveSession(store, updated);
|
|
871
|
+
return updated;
|
|
872
|
+
}
|
|
873
|
+
async function logout(store, session) {
|
|
874
|
+
if (session) {
|
|
875
|
+
try {
|
|
876
|
+
await apiFetch(session.server, "/auth/logout", {
|
|
877
|
+
method: "POST",
|
|
878
|
+
body: { refreshToken: session.refreshToken }
|
|
879
|
+
});
|
|
880
|
+
} catch {
|
|
881
|
+
}
|
|
882
|
+
clearDEK(store, session.userId);
|
|
883
|
+
}
|
|
884
|
+
clearSession(store);
|
|
885
|
+
}
|
|
886
|
+
function createAuthedClient(store) {
|
|
887
|
+
return async function authed(path, req = {}) {
|
|
888
|
+
let session = loadSession(store);
|
|
889
|
+
if (!session) {
|
|
890
|
+
throw new ApiError("Not logged in. Run `notate login`.", 401, "no_session");
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
return await apiFetch(session.server, path, {
|
|
894
|
+
...req,
|
|
895
|
+
accessToken: session.accessToken
|
|
896
|
+
});
|
|
897
|
+
} catch (err) {
|
|
898
|
+
if (err instanceof ApiError && err.status === 401) {
|
|
899
|
+
session = await refreshSession(store, session);
|
|
900
|
+
return await apiFetch(session.server, path, {
|
|
901
|
+
...req,
|
|
902
|
+
accessToken: session.accessToken
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
throw err;
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/lib/config.ts
|
|
911
|
+
import Conf from "conf";
|
|
912
|
+
var CONFIG_DEFAULTS = {
|
|
913
|
+
server: "https://api.notate.md",
|
|
914
|
+
workspace: null,
|
|
915
|
+
defaultMode: "memo",
|
|
916
|
+
defaultSort: "updatedAt",
|
|
917
|
+
pageSize: 10
|
|
918
|
+
};
|
|
919
|
+
function createConfig(options = {}) {
|
|
920
|
+
return new Conf({
|
|
921
|
+
projectName: "notate-cli",
|
|
922
|
+
defaults: CONFIG_DEFAULTS,
|
|
923
|
+
...options.cwd ? { cwd: options.cwd } : {}
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
function resolveServer(flag, env, configured) {
|
|
927
|
+
return flag ?? env ?? configured;
|
|
928
|
+
}
|
|
929
|
+
var CONFIG_KEYS = [
|
|
930
|
+
"server",
|
|
931
|
+
"workspace",
|
|
932
|
+
"defaultMode",
|
|
933
|
+
"defaultSort",
|
|
934
|
+
"pageSize"
|
|
935
|
+
];
|
|
936
|
+
function isConfigKey(key) {
|
|
937
|
+
return CONFIG_KEYS.includes(key);
|
|
938
|
+
}
|
|
939
|
+
function coerceConfigValue(key, value) {
|
|
940
|
+
if (key === "pageSize") {
|
|
941
|
+
const n = Number(value);
|
|
942
|
+
if (!Number.isFinite(n) || n <= 0) throw new Error("pageSize must be a positive number");
|
|
943
|
+
return Math.floor(n);
|
|
944
|
+
}
|
|
945
|
+
if (key === "workspace") return value === "" ? null : value;
|
|
946
|
+
return value;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/lib/output.ts
|
|
950
|
+
import Table from "cli-table3";
|
|
951
|
+
function renderJson(data) {
|
|
952
|
+
return JSON.stringify(data, null, 2);
|
|
953
|
+
}
|
|
954
|
+
function renderTable(head, rows) {
|
|
955
|
+
const table = new Table({ head });
|
|
956
|
+
for (const row of rows) {
|
|
957
|
+
table.push(row.map((cell) => String(cell)));
|
|
958
|
+
}
|
|
959
|
+
return table.toString();
|
|
960
|
+
}
|
|
961
|
+
function emit(data, opts, human, writer = console.log) {
|
|
962
|
+
if (opts.quiet) return;
|
|
963
|
+
writer(opts.json ? renderJson(data) : human(data));
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/types.ts
|
|
967
|
+
var EXIT = {
|
|
968
|
+
ok: 0,
|
|
969
|
+
error: 1,
|
|
970
|
+
usage: 2,
|
|
971
|
+
auth: 3
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
// src/commands/auth.ts
|
|
975
|
+
function bail(message, code) {
|
|
976
|
+
cancel(message);
|
|
977
|
+
process.exit(code);
|
|
978
|
+
}
|
|
979
|
+
function fail(err) {
|
|
980
|
+
if (err instanceof ApiError) {
|
|
981
|
+
console.error(err.message);
|
|
982
|
+
process.exit(err.status === 401 || err.status === 403 ? EXIT.auth : EXIT.error);
|
|
983
|
+
}
|
|
984
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
985
|
+
process.exit(EXIT.error);
|
|
986
|
+
}
|
|
987
|
+
async function loginAction(opts) {
|
|
988
|
+
const store = resolveStore();
|
|
989
|
+
const config = createConfig();
|
|
990
|
+
const server = resolveServer(opts.server, process.env.NOTATE_SERVER, config.get("server"));
|
|
991
|
+
let email = opts.email;
|
|
992
|
+
if (!email) {
|
|
993
|
+
const answer = await text({
|
|
994
|
+
message: "Email",
|
|
995
|
+
validate: (value) => value.includes("@") ? void 0 : "Enter a valid email address"
|
|
996
|
+
});
|
|
997
|
+
if (isCancel(answer)) bail("Cancelled.", EXIT.usage);
|
|
998
|
+
email = answer;
|
|
999
|
+
}
|
|
1000
|
+
const pw = await passwordPrompt({ message: "Password" });
|
|
1001
|
+
if (isCancel(pw)) bail("Cancelled.", EXIT.usage);
|
|
1002
|
+
const progress = spinner();
|
|
1003
|
+
progress.start("Signing in");
|
|
1004
|
+
try {
|
|
1005
|
+
let resp = await requestLogin(server, email, pw);
|
|
1006
|
+
if (resp.requiresTotp && resp.totpToken) {
|
|
1007
|
+
progress.stop("Two-factor authentication required");
|
|
1008
|
+
const code = await text({ message: "Authentication code (or backup code)" });
|
|
1009
|
+
if (isCancel(code)) bail("Cancelled.", EXIT.usage);
|
|
1010
|
+
progress.start("Verifying code");
|
|
1011
|
+
resp = await verifyTotp(server, resp.totpToken, code);
|
|
1012
|
+
}
|
|
1013
|
+
const session = await finalizeLogin(store, server, resp, pw);
|
|
1014
|
+
progress.stop(`Logged in as ${session.email}`);
|
|
1015
|
+
if (opts.json) {
|
|
1016
|
+
console.log(renderJson({ email: session.email, server, userId: session.userId }));
|
|
1017
|
+
}
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
progress.stop("Login failed");
|
|
1020
|
+
fail(err);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function whoamiAction(opts) {
|
|
1024
|
+
const store = resolveStore();
|
|
1025
|
+
const session = loadSession(store);
|
|
1026
|
+
if (!session) {
|
|
1027
|
+
if (!opts.quiet) console.error("Not logged in. Run `notate login`.");
|
|
1028
|
+
process.exit(EXIT.auth);
|
|
1029
|
+
}
|
|
1030
|
+
emit(
|
|
1031
|
+
{ email: session.email, server: session.server, userId: session.userId },
|
|
1032
|
+
opts,
|
|
1033
|
+
() => `${session.email}
|
|
1034
|
+
${session.server}`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
async function logoutAction(opts) {
|
|
1038
|
+
const store = resolveStore();
|
|
1039
|
+
const session = loadSession(store);
|
|
1040
|
+
await logout(store, session);
|
|
1041
|
+
if (!opts.quiet) console.log("Logged out.");
|
|
1042
|
+
}
|
|
1043
|
+
function registerAuthCommands(program2) {
|
|
1044
|
+
program2.command("login").description("Log in to a Notate account").option("--email <email>", "account email (otherwise prompted)").option("--server <url>", "API server URL (default: configured server)").option("--json", "print session info as JSON on success").option("--quiet", "suppress output").action(loginAction);
|
|
1045
|
+
program2.command("logout").description("Log out and clear stored credentials").option("--quiet", "suppress output").action(logoutAction);
|
|
1046
|
+
program2.command("whoami").description("Show the current user and server").option("--json", "output JSON").option("--quiet", "suppress output").action(whoamiAction);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// src/commands/notes.ts
|
|
1050
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1051
|
+
import { confirm, isCancel as isCancel2, multiselect, cancel as cancel2 } from "@clack/prompts";
|
|
1052
|
+
|
|
1053
|
+
// src/lib/notes.ts
|
|
1054
|
+
async function refreshEncryptionState(store) {
|
|
1055
|
+
const session = requireSession(store);
|
|
1056
|
+
const authed = createAuthedClient(store);
|
|
1057
|
+
const status = await authed("/auth/encryption/status");
|
|
1058
|
+
const enabled = Boolean(status.enabled);
|
|
1059
|
+
const fresh = enabled === session.encryptionEnabled ? session : { ...session, encryptionEnabled: enabled };
|
|
1060
|
+
if (fresh !== session) saveSession(store, fresh);
|
|
1061
|
+
return { authed, session: fresh, enabled };
|
|
1062
|
+
}
|
|
1063
|
+
async function buildNotesContext(store) {
|
|
1064
|
+
const { authed, session, enabled } = await refreshEncryptionState(store);
|
|
1065
|
+
if (!enabled) {
|
|
1066
|
+
return { authed, encryptionEnabled: false };
|
|
1067
|
+
}
|
|
1068
|
+
const dek = loadDEK(store, session.userId);
|
|
1069
|
+
if (!dek) {
|
|
1070
|
+
throw new ApiError(
|
|
1071
|
+
"This account is encrypted but not unlocked on this machine. Run `notate login`.",
|
|
1072
|
+
401,
|
|
1073
|
+
"e2e_locked"
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
return { authed, contentKey: deriveContentKey(dek), encryptionEnabled: true };
|
|
1077
|
+
}
|
|
1078
|
+
async function decrypt(ctx, note) {
|
|
1079
|
+
return ctx.contentKey ? decryptNote(note, ctx.contentKey) : note;
|
|
1080
|
+
}
|
|
1081
|
+
async function listNotes(ctx, params = {}) {
|
|
1082
|
+
const res = await ctx.authed("/notes", {
|
|
1083
|
+
query: {
|
|
1084
|
+
workspaceId: params.workspaceId,
|
|
1085
|
+
folder: params.folder,
|
|
1086
|
+
folderId: params.folderId,
|
|
1087
|
+
tags: params.tags?.length ? params.tags.join(",") : void 0,
|
|
1088
|
+
favorite: params.favorite ? "true" : void 0,
|
|
1089
|
+
search: params.search,
|
|
1090
|
+
searchMode: params.searchMode,
|
|
1091
|
+
sortBy: params.sortBy,
|
|
1092
|
+
sortOrder: params.sortOrder,
|
|
1093
|
+
page: params.page,
|
|
1094
|
+
pageSize: params.pageSize
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
const notes = await Promise.all(res.notes.map((n) => decrypt(ctx, n)));
|
|
1098
|
+
return { notes, total: res.total };
|
|
1099
|
+
}
|
|
1100
|
+
async function searchNotes(ctx, query, params = {}) {
|
|
1101
|
+
const res = await ctx.authed("/notes", {
|
|
1102
|
+
query: {
|
|
1103
|
+
search: query,
|
|
1104
|
+
searchMode: params.searchMode ?? "keyword",
|
|
1105
|
+
workspaceId: params.workspaceId,
|
|
1106
|
+
folder: params.folder,
|
|
1107
|
+
tags: params.tags?.length ? params.tags.join(",") : void 0,
|
|
1108
|
+
pageSize: params.pageSize
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
const notes = await Promise.all(res.notes.map((n) => decrypt(ctx, n)));
|
|
1112
|
+
return { notes, total: res.total };
|
|
1113
|
+
}
|
|
1114
|
+
async function getNote(ctx, id) {
|
|
1115
|
+
const res = await ctx.authed(`/notes/${id}`);
|
|
1116
|
+
return decrypt(ctx, res.note);
|
|
1117
|
+
}
|
|
1118
|
+
async function createNote(ctx, input) {
|
|
1119
|
+
const body = ctx.contentKey ? await encryptNote(input, ctx.contentKey) : input;
|
|
1120
|
+
const res = await ctx.authed("/notes", { method: "POST", body });
|
|
1121
|
+
return decrypt(ctx, res.note);
|
|
1122
|
+
}
|
|
1123
|
+
async function updateNote(ctx, id, patch) {
|
|
1124
|
+
const body = ctx.contentKey ? await encryptNote(patch, ctx.contentKey) : patch;
|
|
1125
|
+
const res = await ctx.authed(`/notes/${id}`, { method: "PATCH", body });
|
|
1126
|
+
return decrypt(ctx, res.note);
|
|
1127
|
+
}
|
|
1128
|
+
async function deleteNote(ctx, id) {
|
|
1129
|
+
await ctx.authed(`/notes/${id}`, { method: "DELETE" });
|
|
1130
|
+
}
|
|
1131
|
+
async function permanentDeleteNote(ctx, id) {
|
|
1132
|
+
await ctx.authed(`/notes/${id}/permanent`, { method: "DELETE" });
|
|
1133
|
+
}
|
|
1134
|
+
function listNoteVersions(ctx, noteId, params = {}) {
|
|
1135
|
+
return ctx.authed(`/notes/${noteId}/versions`, {
|
|
1136
|
+
query: { page: params.page, pageSize: params.pageSize }
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
async function restoreNoteVersion(ctx, noteId, versionId) {
|
|
1140
|
+
const res = await ctx.authed(
|
|
1141
|
+
`/notes/${noteId}/versions/${versionId}/restore`,
|
|
1142
|
+
{ method: "POST" }
|
|
1143
|
+
);
|
|
1144
|
+
return decrypt(ctx, res.note);
|
|
1145
|
+
}
|
|
1146
|
+
async function restoreNote(ctx, id) {
|
|
1147
|
+
const res = await ctx.authed(`/notes/${id}/restore`, { method: "PATCH" });
|
|
1148
|
+
return decrypt(ctx, res.note);
|
|
1149
|
+
}
|
|
1150
|
+
async function emptyTrash(ctx, workspaceId) {
|
|
1151
|
+
return ctx.authed("/notes/trash", {
|
|
1152
|
+
method: "DELETE",
|
|
1153
|
+
body: workspaceId ? { workspaceId } : void 0
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
async function listTrashedNotes(ctx, params = {}) {
|
|
1157
|
+
const res = await ctx.authed("/notes/trash", {
|
|
1158
|
+
query: {
|
|
1159
|
+
workspaceId: params.workspaceId,
|
|
1160
|
+
page: params.page,
|
|
1161
|
+
pageSize: params.pageSize
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
const notes = await Promise.all(res.notes.map((n) => decrypt(ctx, n)));
|
|
1165
|
+
return { notes, total: res.total };
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/lib/workspace.ts
|
|
1169
|
+
async function listWorkspaces(authed) {
|
|
1170
|
+
const res = await authed("/workspaces");
|
|
1171
|
+
return res.workspaces;
|
|
1172
|
+
}
|
|
1173
|
+
var AmbiguousWorkspaceError = class extends Error {
|
|
1174
|
+
matches;
|
|
1175
|
+
constructor(workspaceName, matches) {
|
|
1176
|
+
const lines = matches.map(
|
|
1177
|
+
(w) => ` ${w.id} ${w.name} (${w.folderCount} folders \xB7 ${w.noteCount ?? 0} notes)`
|
|
1178
|
+
);
|
|
1179
|
+
super(
|
|
1180
|
+
`Multiple workspaces named "${workspaceName}":
|
|
1181
|
+
${lines.join("\n")}
|
|
1182
|
+
Re-run with the id.`
|
|
1183
|
+
);
|
|
1184
|
+
this.name = "AmbiguousWorkspaceError";
|
|
1185
|
+
this.matches = matches;
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
function resolveWorkspace(workspaces, candidate) {
|
|
1189
|
+
if (workspaces.length === 0) {
|
|
1190
|
+
throw new Error("No workspaces found on this account.");
|
|
1191
|
+
}
|
|
1192
|
+
if (!candidate) {
|
|
1193
|
+
return [...workspaces].sort(
|
|
1194
|
+
(a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" })
|
|
1195
|
+
)[0];
|
|
1196
|
+
}
|
|
1197
|
+
const byId = workspaces.find((w) => w.id === candidate);
|
|
1198
|
+
if (byId) return byId;
|
|
1199
|
+
const byName = workspaces.filter((w) => w.name.toLowerCase() === candidate.toLowerCase());
|
|
1200
|
+
if (byName.length === 1) return byName[0];
|
|
1201
|
+
if (byName.length === 0) throw new Error(`Workspace not found: ${candidate}`);
|
|
1202
|
+
throw new AmbiguousWorkspaceError(candidate, byName);
|
|
1203
|
+
}
|
|
1204
|
+
function matchWorkspaces(workspaces, candidate) {
|
|
1205
|
+
const byId = workspaces.find((w) => w.id === candidate);
|
|
1206
|
+
if (byId) return [byId];
|
|
1207
|
+
return workspaces.filter((w) => w.name.toLowerCase() === candidate.toLowerCase());
|
|
1208
|
+
}
|
|
1209
|
+
async function resolveActiveWorkspace(authed, flag, configured) {
|
|
1210
|
+
const workspaces = await listWorkspaces(authed);
|
|
1211
|
+
return resolveWorkspace(workspaces, flag ?? configured);
|
|
1212
|
+
}
|
|
1213
|
+
function createWorkspace(authed, name, opts = {}) {
|
|
1214
|
+
return authed("/workspaces", {
|
|
1215
|
+
method: "POST",
|
|
1216
|
+
body: { name, ...opts }
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
function renameWorkspace(authed, id, name) {
|
|
1220
|
+
return authed(`/workspaces/${id}`, {
|
|
1221
|
+
method: "PATCH",
|
|
1222
|
+
body: { name }
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
function deleteWorkspace(authed, id) {
|
|
1226
|
+
return authed(`/workspaces/${id}`, { method: "DELETE" });
|
|
1227
|
+
}
|
|
1228
|
+
function setWorkspaceDefaultFolder(authed, id, defaultFolderId) {
|
|
1229
|
+
return authed(`/workspaces/${id}`, {
|
|
1230
|
+
method: "PATCH",
|
|
1231
|
+
body: { defaultFolderId }
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
function addFolderToWorkspace(authed, workspaceId, folderId) {
|
|
1235
|
+
return authed(`/workspaces/${workspaceId}/folders`, {
|
|
1236
|
+
method: "POST",
|
|
1237
|
+
body: { folderId }
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
function removeFolderFromWorkspace(authed, workspaceId, folderId) {
|
|
1241
|
+
return authed(`/workspaces/${workspaceId}/folders/${folderId}`, { method: "DELETE" });
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// src/lib/folders.ts
|
|
1245
|
+
async function listFolders(authed, workspaceId) {
|
|
1246
|
+
const res = await authed("/notes/folders", { query: { workspaceId } });
|
|
1247
|
+
return flattenFolders(res.folders);
|
|
1248
|
+
}
|
|
1249
|
+
async function listTrashedFolders(authed, workspaceId) {
|
|
1250
|
+
const res = await authed("/notes/folders/trash", { query: { workspaceId } });
|
|
1251
|
+
return flattenFolders(res.folders);
|
|
1252
|
+
}
|
|
1253
|
+
function flattenFolders(folders) {
|
|
1254
|
+
const out = [];
|
|
1255
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1256
|
+
const walk = (list) => {
|
|
1257
|
+
for (const f of list) {
|
|
1258
|
+
if (!seen.has(f.id)) {
|
|
1259
|
+
seen.add(f.id);
|
|
1260
|
+
out.push(f);
|
|
1261
|
+
}
|
|
1262
|
+
if (f.children?.length) walk(f.children);
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
walk(folders);
|
|
1266
|
+
return out;
|
|
1267
|
+
}
|
|
1268
|
+
function orderFolderTree(flat) {
|
|
1269
|
+
const ids = new Set(flat.map((f) => f.id));
|
|
1270
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
1271
|
+
for (const f of flat) {
|
|
1272
|
+
if (f.parentId && ids.has(f.parentId)) {
|
|
1273
|
+
const arr = childrenOf.get(f.parentId) ?? [];
|
|
1274
|
+
arr.push(f);
|
|
1275
|
+
childrenOf.set(f.parentId, arr);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const byName = (a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" });
|
|
1279
|
+
const out = [];
|
|
1280
|
+
const emit2 = (f, depth) => {
|
|
1281
|
+
out.push({ ...f, depth });
|
|
1282
|
+
for (const child of (childrenOf.get(f.id) ?? []).sort(byName)) emit2(child, depth + 1);
|
|
1283
|
+
};
|
|
1284
|
+
const roots = flat.filter((f) => !f.parentId || !ids.has(f.parentId));
|
|
1285
|
+
for (const root of roots.sort(byName)) emit2(root, 0);
|
|
1286
|
+
return out;
|
|
1287
|
+
}
|
|
1288
|
+
function firstRootId(folders) {
|
|
1289
|
+
const roots = folders.filter((f) => f.parentId === null);
|
|
1290
|
+
return (roots.find((f) => f.isRoot) ?? roots[0])?.id ?? null;
|
|
1291
|
+
}
|
|
1292
|
+
function resolveNewNoteFolderId(defaultFolderId, folders) {
|
|
1293
|
+
if (defaultFolderId && folders.some((f) => f.id === defaultFolderId)) {
|
|
1294
|
+
return defaultFolderId;
|
|
1295
|
+
}
|
|
1296
|
+
return firstRootId(folders);
|
|
1297
|
+
}
|
|
1298
|
+
function folderTreeLines(flat, label) {
|
|
1299
|
+
const ids = new Set(flat.map((f) => f.id));
|
|
1300
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
1301
|
+
for (const f of flat) {
|
|
1302
|
+
if (f.parentId && ids.has(f.parentId)) {
|
|
1303
|
+
const arr = childrenOf.get(f.parentId) ?? [];
|
|
1304
|
+
arr.push(f);
|
|
1305
|
+
childrenOf.set(f.parentId, arr);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
const byName = (a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" });
|
|
1309
|
+
const lines = [];
|
|
1310
|
+
const walk = (nodes, ancestorHasNext) => {
|
|
1311
|
+
const sorted = [...nodes].sort(byName);
|
|
1312
|
+
sorted.forEach((f, i) => {
|
|
1313
|
+
const isLast = i === sorted.length - 1;
|
|
1314
|
+
const prefix = ancestorHasNext.map((hasNext) => hasNext ? "\u2502 " : " ").join("");
|
|
1315
|
+
lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label(f)}`);
|
|
1316
|
+
const kids = childrenOf.get(f.id);
|
|
1317
|
+
if (kids?.length) walk(kids, [...ancestorHasNext, !isLast]);
|
|
1318
|
+
});
|
|
1319
|
+
};
|
|
1320
|
+
walk(
|
|
1321
|
+
flat.filter((f) => !f.parentId || !ids.has(f.parentId)),
|
|
1322
|
+
[]
|
|
1323
|
+
);
|
|
1324
|
+
return lines;
|
|
1325
|
+
}
|
|
1326
|
+
var AmbiguousFolderError = class extends Error {
|
|
1327
|
+
constructor(folderName, matches) {
|
|
1328
|
+
super(`Multiple folders named "${folderName}".`);
|
|
1329
|
+
this.folderName = folderName;
|
|
1330
|
+
this.matches = matches;
|
|
1331
|
+
this.name = "AmbiguousFolderError";
|
|
1332
|
+
}
|
|
1333
|
+
folderName;
|
|
1334
|
+
matches;
|
|
1335
|
+
};
|
|
1336
|
+
function resolveFolder(flat, nameOrId) {
|
|
1337
|
+
const byId = flat.find((f) => f.id === nameOrId);
|
|
1338
|
+
if (byId) return byId;
|
|
1339
|
+
const matches = flat.filter((f) => f.name === nameOrId);
|
|
1340
|
+
if (matches.length === 1) return matches[0];
|
|
1341
|
+
if (matches.length === 0) throw new ApiError(`No folder named "${nameOrId}".`, 1, "not_found");
|
|
1342
|
+
throw new AmbiguousFolderError(nameOrId, matches);
|
|
1343
|
+
}
|
|
1344
|
+
function createFolder(authed, req) {
|
|
1345
|
+
return authed(
|
|
1346
|
+
"/notes/folders",
|
|
1347
|
+
{ method: "POST", body: req }
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
function renameFolder(authed, id, newName) {
|
|
1351
|
+
return authed(`/notes/folders/${id}`, {
|
|
1352
|
+
method: "PATCH",
|
|
1353
|
+
body: { newName }
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
function deleteFolder(authed, id, mode) {
|
|
1357
|
+
return authed(`/notes/folders/${id}`, {
|
|
1358
|
+
method: "DELETE",
|
|
1359
|
+
query: { mode }
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
function restoreFolder(authed, id) {
|
|
1363
|
+
return authed(`/notes/folders/${id}/restore`, { method: "PATCH" });
|
|
1364
|
+
}
|
|
1365
|
+
function permanentDeleteFolder(authed, id) {
|
|
1366
|
+
return authed(`/notes/folders/${id}/permanent`, { method: "DELETE" });
|
|
1367
|
+
}
|
|
1368
|
+
function setFolderFavorite(authed, id, favorite) {
|
|
1369
|
+
return authed(`/notes/folders/${id}/favorite`, {
|
|
1370
|
+
method: "PATCH",
|
|
1371
|
+
body: { favorite }
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
function moveFolder(authed, id, parentId, opts = {}) {
|
|
1375
|
+
return authed(
|
|
1376
|
+
`/notes/folders/${id}/move`,
|
|
1377
|
+
{
|
|
1378
|
+
method: "PATCH",
|
|
1379
|
+
body: { parentId },
|
|
1380
|
+
query: opts.confirmCrossBoundary ? { confirmCrossBoundary: 1 } : void 0
|
|
1381
|
+
}
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// src/lib/resolve.ts
|
|
1386
|
+
var RESOLVE_SCAN_LIMIT = 500;
|
|
1387
|
+
var AmbiguousTitleError = class extends Error {
|
|
1388
|
+
constructor(title, matches) {
|
|
1389
|
+
super(`Multiple notes match "${title}".`);
|
|
1390
|
+
this.title = title;
|
|
1391
|
+
this.matches = matches;
|
|
1392
|
+
this.name = "AmbiguousTitleError";
|
|
1393
|
+
}
|
|
1394
|
+
title;
|
|
1395
|
+
matches;
|
|
1396
|
+
};
|
|
1397
|
+
async function resolveNoteTarget(opts, lister) {
|
|
1398
|
+
if (opts.id) return { id: opts.id };
|
|
1399
|
+
const title = opts.title;
|
|
1400
|
+
if (!title) {
|
|
1401
|
+
throw new ApiError("Provide a note title or --id.", 2, "usage");
|
|
1402
|
+
}
|
|
1403
|
+
const { notes } = await lister({
|
|
1404
|
+
workspaceId: opts.workspaceId,
|
|
1405
|
+
search: opts.useSearch ? title : void 0,
|
|
1406
|
+
pageSize: RESOLVE_SCAN_LIMIT
|
|
1407
|
+
});
|
|
1408
|
+
const matches = notes.filter((n) => n.title === title);
|
|
1409
|
+
if (matches.length === 1) return { id: matches[0].id, note: matches[0] };
|
|
1410
|
+
if (matches.length === 0) {
|
|
1411
|
+
throw new ApiError(`No note titled "${title}".`, 1, "not_found");
|
|
1412
|
+
}
|
|
1413
|
+
throw new AmbiguousTitleError(title, matches);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/commands/notes.ts
|
|
1417
|
+
function handleError(err) {
|
|
1418
|
+
if (err instanceof AmbiguousTitleError) {
|
|
1419
|
+
console.error(err.message);
|
|
1420
|
+
console.error(
|
|
1421
|
+
renderTable(
|
|
1422
|
+
["ID", "Title", "Folder"],
|
|
1423
|
+
err.matches.map((n) => [n.id, n.title, n.folder ?? ""])
|
|
1424
|
+
)
|
|
1425
|
+
);
|
|
1426
|
+
console.error("Re-run with --id <id>.");
|
|
1427
|
+
process.exit(EXIT.error);
|
|
1428
|
+
}
|
|
1429
|
+
if (err instanceof ApiError) {
|
|
1430
|
+
console.error(err.message);
|
|
1431
|
+
process.exit(err.status === 401 || err.status === 403 ? EXIT.auth : EXIT.error);
|
|
1432
|
+
}
|
|
1433
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1434
|
+
process.exit(EXIT.error);
|
|
1435
|
+
}
|
|
1436
|
+
async function setup(opts) {
|
|
1437
|
+
const store = resolveStore();
|
|
1438
|
+
const config = createConfig();
|
|
1439
|
+
const ctx = await buildNotesContext(store);
|
|
1440
|
+
const workspace = await resolveActiveWorkspace(
|
|
1441
|
+
ctx.authed,
|
|
1442
|
+
opts.workspace,
|
|
1443
|
+
config.get("workspace")
|
|
1444
|
+
);
|
|
1445
|
+
return { ctx, workspaceId: workspace.id, defaultFolderId: workspace.defaultFolderId ?? null };
|
|
1446
|
+
}
|
|
1447
|
+
function readContentInput(opts) {
|
|
1448
|
+
if (opts.stdin) return readStdin();
|
|
1449
|
+
if (opts.file) return Promise.resolve(readFileSync2(opts.file, "utf-8"));
|
|
1450
|
+
return Promise.resolve(opts.content);
|
|
1451
|
+
}
|
|
1452
|
+
async function readStdin() {
|
|
1453
|
+
const chunks = [];
|
|
1454
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1455
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
1456
|
+
}
|
|
1457
|
+
function splitCsv(value) {
|
|
1458
|
+
if (!value) return void 0;
|
|
1459
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1460
|
+
}
|
|
1461
|
+
async function resolveTarget(ctx, workspaceId, opts, source = "active") {
|
|
1462
|
+
const lister = source === "trash" ? (params) => listTrashedNotes(ctx, params) : (params) => listNotes(ctx, params);
|
|
1463
|
+
return resolveNoteTarget(
|
|
1464
|
+
{
|
|
1465
|
+
id: opts.id,
|
|
1466
|
+
title: opts.title,
|
|
1467
|
+
workspaceId,
|
|
1468
|
+
// Server search can't match encrypted titles; only narrow for non-E2E.
|
|
1469
|
+
useSearch: source === "active" && !ctx.contentKey
|
|
1470
|
+
},
|
|
1471
|
+
lister
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
function noteSummary(n) {
|
|
1475
|
+
return { id: n.id, title: n.title, folder: n.folder, tags: n.tags, updatedAt: n.updatedAt };
|
|
1476
|
+
}
|
|
1477
|
+
async function listAction(opts) {
|
|
1478
|
+
try {
|
|
1479
|
+
const { ctx, workspaceId } = await setup(opts);
|
|
1480
|
+
const { notes } = await listNotes(ctx, {
|
|
1481
|
+
workspaceId,
|
|
1482
|
+
folder: opts.folder,
|
|
1483
|
+
tags: opts.tag ? [opts.tag] : void 0,
|
|
1484
|
+
favorite: opts.favorite,
|
|
1485
|
+
sortBy: opts.sort ?? "updatedAt",
|
|
1486
|
+
sortOrder: "desc",
|
|
1487
|
+
pageSize: opts.limit ? Number(opts.limit) : 10
|
|
1488
|
+
});
|
|
1489
|
+
emit(
|
|
1490
|
+
notes.map(noteSummary),
|
|
1491
|
+
opts,
|
|
1492
|
+
() => renderTable(
|
|
1493
|
+
["Title", "Folder", "Tags", "Updated"],
|
|
1494
|
+
notes.map((n) => [n.title, n.folder ?? "", n.tags.join(", "), n.updatedAt.slice(0, 10)])
|
|
1495
|
+
)
|
|
1496
|
+
);
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
handleError(err);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
async function searchAction(query, opts) {
|
|
1502
|
+
try {
|
|
1503
|
+
const store = resolveStore();
|
|
1504
|
+
if ((await refreshEncryptionState(store)).enabled) {
|
|
1505
|
+
console.error(
|
|
1506
|
+
"Search isn't available on encrypted accounts. (The server can't read your encrypted notes. Use the app to search.)"
|
|
1507
|
+
);
|
|
1508
|
+
process.exit(EXIT.error);
|
|
1509
|
+
}
|
|
1510
|
+
const { ctx, workspaceId } = await setup(opts);
|
|
1511
|
+
const { notes } = await searchNotes(ctx, query, {
|
|
1512
|
+
searchMode: opts.mode,
|
|
1513
|
+
workspaceId,
|
|
1514
|
+
folder: opts.folder,
|
|
1515
|
+
tags: opts.tag ? [opts.tag] : void 0,
|
|
1516
|
+
pageSize: opts.limit ? Number(opts.limit) : 10
|
|
1517
|
+
});
|
|
1518
|
+
emit(
|
|
1519
|
+
notes.map((n) => ({
|
|
1520
|
+
id: n.id,
|
|
1521
|
+
title: n.title,
|
|
1522
|
+
folder: n.folder,
|
|
1523
|
+
headline: n.headline,
|
|
1524
|
+
updatedAt: n.updatedAt
|
|
1525
|
+
})),
|
|
1526
|
+
opts,
|
|
1527
|
+
() => renderTable(
|
|
1528
|
+
["Title", "Folder", "Snippet", "Updated"],
|
|
1529
|
+
notes.map((n) => [
|
|
1530
|
+
n.title,
|
|
1531
|
+
n.folder ?? "",
|
|
1532
|
+
(n.headline ?? "").replace(/\s+/g, " ").trim().slice(0, 60),
|
|
1533
|
+
n.updatedAt.slice(0, 10)
|
|
1534
|
+
])
|
|
1535
|
+
)
|
|
1536
|
+
);
|
|
1537
|
+
} catch (err) {
|
|
1538
|
+
handleError(err);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
async function getAction(title, opts) {
|
|
1542
|
+
try {
|
|
1543
|
+
const { ctx, workspaceId } = await setup(opts);
|
|
1544
|
+
const target = await resolveTarget(ctx, workspaceId, { id: opts.id, title });
|
|
1545
|
+
const note = target.note ?? await getNote(ctx, target.id);
|
|
1546
|
+
emit(note, opts, () => note.content);
|
|
1547
|
+
} catch (err) {
|
|
1548
|
+
handleError(err);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
async function createAction(title, opts) {
|
|
1552
|
+
try {
|
|
1553
|
+
const { ctx, workspaceId, defaultFolderId } = await setup(opts);
|
|
1554
|
+
const content = await readContentInput(opts);
|
|
1555
|
+
const folders = await listFolders(ctx.authed, workspaceId);
|
|
1556
|
+
let dest;
|
|
1557
|
+
if (opts.folder) {
|
|
1558
|
+
const f = resolveFolder(folders, opts.folder);
|
|
1559
|
+
dest = { id: f.id, name: f.name };
|
|
1560
|
+
} else {
|
|
1561
|
+
const folderId = resolveNewNoteFolderId(defaultFolderId, folders);
|
|
1562
|
+
if (!folderId) {
|
|
1563
|
+
throw new ApiError(
|
|
1564
|
+
"This workspace has no folders. Create one first (e.g. `notate folders create Notes`), or pass --folder.",
|
|
1565
|
+
2,
|
|
1566
|
+
"no_root"
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
dest = { id: folderId, name: folders.find((x) => x.id === folderId)?.name ?? "" };
|
|
1570
|
+
}
|
|
1571
|
+
const note = await createNote(ctx, {
|
|
1572
|
+
title,
|
|
1573
|
+
content,
|
|
1574
|
+
folderId: dest.id,
|
|
1575
|
+
folder: dest.name,
|
|
1576
|
+
tags: splitCsv(opts.tags),
|
|
1577
|
+
workspaceId
|
|
1578
|
+
});
|
|
1579
|
+
emit(noteSummary(note), opts, () => `Created "${note.title}" (${note.id}).`);
|
|
1580
|
+
} catch (err) {
|
|
1581
|
+
handleError(err);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
async function editAction(title, opts) {
|
|
1585
|
+
try {
|
|
1586
|
+
const { ctx, workspaceId } = await setup(opts);
|
|
1587
|
+
const target = await resolveTarget(ctx, workspaceId, { id: opts.id, title });
|
|
1588
|
+
let content;
|
|
1589
|
+
if (opts.append !== void 0) {
|
|
1590
|
+
const current = target.note ?? await getNote(ctx, target.id);
|
|
1591
|
+
content = `${current.content}
|
|
1592
|
+
${opts.append}`;
|
|
1593
|
+
} else {
|
|
1594
|
+
content = await readContentInput(opts);
|
|
1595
|
+
}
|
|
1596
|
+
if (content === void 0) {
|
|
1597
|
+
throw new ApiError("Nothing to write. Use --content, --stdin, --file, or --append.", 2, "usage");
|
|
1598
|
+
}
|
|
1599
|
+
const note = await updateNote(ctx, target.id, { content });
|
|
1600
|
+
emit(noteSummary(note), opts, () => `Updated "${note.title}".`);
|
|
1601
|
+
} catch (err) {
|
|
1602
|
+
handleError(err);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
async function favoriteAction(title, opts) {
|
|
1606
|
+
try {
|
|
1607
|
+
const { ctx, workspaceId } = await setup(opts);
|
|
1608
|
+
const target = await resolveTarget(ctx, workspaceId, { id: opts.id, title });
|
|
1609
|
+
const favorite = !opts.off;
|
|
1610
|
+
const note = await updateNote(ctx, target.id, { favorite });
|
|
1611
|
+
emit(
|
|
1612
|
+
{ id: note.id, title: note.title, favorite: note.favorite },
|
|
1613
|
+
opts,
|
|
1614
|
+
() => `${favorite ? "Favorited" : "Unfavorited"} "${note.title}".`
|
|
1615
|
+
);
|
|
1616
|
+
} catch (err) {
|
|
1617
|
+
handleError(err);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
async function deleteAction(title, opts) {
|
|
1621
|
+
try {
|
|
1622
|
+
const { ctx, workspaceId } = await setup(opts);
|
|
1623
|
+
const target = await resolveTarget(ctx, workspaceId, { id: opts.id, title });
|
|
1624
|
+
if (!opts.confirm) {
|
|
1625
|
+
const ok = await confirm({ message: `Move "${title ?? target.id}" to Trash?` });
|
|
1626
|
+
if (isCancel2(ok) || !ok) {
|
|
1627
|
+
if (!opts.quiet) console.error("Cancelled.");
|
|
1628
|
+
process.exit(EXIT.usage);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
await deleteNote(ctx, target.id);
|
|
1632
|
+
if (!opts.quiet) console.log("Moved to Trash.");
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
handleError(err);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
async function trashContext(opts) {
|
|
1638
|
+
const store = resolveStore();
|
|
1639
|
+
const ctx = await buildNotesContext(store);
|
|
1640
|
+
let workspaceId;
|
|
1641
|
+
if (opts.workspace) {
|
|
1642
|
+
const config = createConfig();
|
|
1643
|
+
workspaceId = (await resolveActiveWorkspace(ctx.authed, opts.workspace, config.get("workspace"))).id;
|
|
1644
|
+
}
|
|
1645
|
+
return { ctx, workspaceId };
|
|
1646
|
+
}
|
|
1647
|
+
async function trashListAction(opts) {
|
|
1648
|
+
try {
|
|
1649
|
+
const { ctx, workspaceId } = await trashContext(opts);
|
|
1650
|
+
const { notes } = await listTrashedNotes(ctx, { workspaceId, pageSize: 500 });
|
|
1651
|
+
emit(
|
|
1652
|
+
notes.map((n) => ({ id: n.id, title: n.title, folder: n.folder, deletedAt: n.deletedAt })),
|
|
1653
|
+
opts,
|
|
1654
|
+
() => notes.length ? renderTable(
|
|
1655
|
+
["Title", "Folder", "Deleted", "ID"],
|
|
1656
|
+
notes.map((n) => [n.title, n.folder ?? "", (n.deletedAt ?? "").slice(0, 10), n.id])
|
|
1657
|
+
) : "Trash is empty."
|
|
1658
|
+
);
|
|
1659
|
+
} catch (err) {
|
|
1660
|
+
handleError(err);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
async function trashDeleteAction(title, opts) {
|
|
1664
|
+
try {
|
|
1665
|
+
const { ctx, workspaceId } = await trashContext(opts);
|
|
1666
|
+
if (title || opts.id) {
|
|
1667
|
+
const target = await resolveTarget(ctx, workspaceId, { id: opts.id, title }, "trash");
|
|
1668
|
+
await permanentDeleteNote(ctx, target.id);
|
|
1669
|
+
if (!opts.quiet) console.log(`Permanently deleted "${title ?? target.id}".`);
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
const { notes } = await listTrashedNotes(ctx, { workspaceId, pageSize: 500 });
|
|
1673
|
+
if (notes.length === 0) {
|
|
1674
|
+
if (!opts.quiet) console.log("Trash is empty.");
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (!process.stdin.isTTY) {
|
|
1678
|
+
throw new ApiError("Specify a note title or --id (or run in a terminal to pick).", 2, "usage");
|
|
1679
|
+
}
|
|
1680
|
+
const chosen = await multiselect({
|
|
1681
|
+
message: "Select trashed notes to permanently delete (space to toggle, enter to confirm)",
|
|
1682
|
+
options: notes.map((n) => ({ value: n.id, label: n.title, hint: (n.deletedAt ?? "").slice(0, 10) })),
|
|
1683
|
+
required: false
|
|
1684
|
+
});
|
|
1685
|
+
if (isCancel2(chosen)) {
|
|
1686
|
+
cancel2("Cancelled.");
|
|
1687
|
+
process.exit(EXIT.usage);
|
|
1688
|
+
}
|
|
1689
|
+
const ids = chosen;
|
|
1690
|
+
if (ids.length === 0) {
|
|
1691
|
+
if (!opts.quiet) console.log("Nothing selected.");
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
for (const id of ids) await permanentDeleteNote(ctx, id);
|
|
1695
|
+
if (!opts.quiet) console.log(`Permanently deleted ${ids.length} note(s).`);
|
|
1696
|
+
} catch (err) {
|
|
1697
|
+
handleError(err);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
async function emptyTrashAction(opts) {
|
|
1701
|
+
try {
|
|
1702
|
+
const { ctx, workspaceId } = await trashContext(opts);
|
|
1703
|
+
if (!opts.confirm) {
|
|
1704
|
+
const target = opts.workspace ? `the "${opts.workspace}" workspace` : "all workspaces";
|
|
1705
|
+
const ok = await confirm({
|
|
1706
|
+
message: `Permanently delete all trashed notes in ${target}? This can't be undone.`
|
|
1707
|
+
});
|
|
1708
|
+
if (isCancel2(ok) || !ok) {
|
|
1709
|
+
if (!opts.quiet) console.error("Cancelled.");
|
|
1710
|
+
process.exit(EXIT.usage);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
const res = await emptyTrash(ctx, workspaceId);
|
|
1714
|
+
if (!opts.quiet) console.log(`Permanently deleted ${res.deleted} note(s) from Trash.`);
|
|
1715
|
+
} catch (err) {
|
|
1716
|
+
handleError(err);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
async function restoreAction(title, opts) {
|
|
1720
|
+
try {
|
|
1721
|
+
const { ctx, workspaceId } = await setup(opts);
|
|
1722
|
+
const target = await resolveTarget(ctx, workspaceId, { id: opts.id, title }, "trash");
|
|
1723
|
+
const note = await restoreNote(ctx, target.id);
|
|
1724
|
+
emit(noteSummary(note), opts, () => `Restored "${note.title}".`);
|
|
1725
|
+
} catch (err) {
|
|
1726
|
+
handleError(err);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
async function versionsAction(title, opts) {
|
|
1730
|
+
try {
|
|
1731
|
+
const { ctx, workspaceId } = await setup(opts);
|
|
1732
|
+
const target = await resolveTarget(ctx, workspaceId, { id: opts.id, title });
|
|
1733
|
+
if (opts.restore) {
|
|
1734
|
+
const note = await restoreNoteVersion(ctx, target.id, opts.restore);
|
|
1735
|
+
emit(noteSummary(note), opts, () => `Restored "${note.title}" to version ${opts.restore}.`);
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
const { versions } = await listNoteVersions(ctx, target.id, { pageSize: 50 });
|
|
1739
|
+
emit(
|
|
1740
|
+
versions.map((v) => ({ id: v.id, origin: v.origin, createdAt: v.createdAt })),
|
|
1741
|
+
opts,
|
|
1742
|
+
() => versions.length ? renderTable(
|
|
1743
|
+
["Version ID", "Origin", "Saved"],
|
|
1744
|
+
versions.map((v) => [v.id, v.origin, v.createdAt.slice(0, 19).replace("T", " ")])
|
|
1745
|
+
) : "No version history yet."
|
|
1746
|
+
);
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
handleError(err);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
async function moveAction(title, opts) {
|
|
1752
|
+
try {
|
|
1753
|
+
const { ctx, workspaceId, defaultFolderId } = await setup(opts);
|
|
1754
|
+
const target = await resolveTarget(ctx, workspaceId, { id: opts.id, title });
|
|
1755
|
+
const folders = await listFolders(ctx.authed, workspaceId);
|
|
1756
|
+
let dest;
|
|
1757
|
+
if (opts.toDefault) {
|
|
1758
|
+
if (!defaultFolderId) {
|
|
1759
|
+
throw new ApiError("This workspace has no default folder to move into.", 2, "no_default");
|
|
1760
|
+
}
|
|
1761
|
+
const f = folders.find((x) => x.id === defaultFolderId);
|
|
1762
|
+
dest = { id: defaultFolderId, name: f?.name ?? "" };
|
|
1763
|
+
} else if (opts.to) {
|
|
1764
|
+
const f = resolveFolder(folders, opts.to);
|
|
1765
|
+
dest = { id: f.id, name: f.name };
|
|
1766
|
+
} else {
|
|
1767
|
+
throw new ApiError("Provide --to <folder> or --to-default.", 2, "usage");
|
|
1768
|
+
}
|
|
1769
|
+
const note = await updateNote(ctx, target.id, { folderId: dest.id, folder: dest.name });
|
|
1770
|
+
emit(noteSummary(note), opts, () => `Moved "${note.title}" to "${dest.name}".`);
|
|
1771
|
+
} catch (err) {
|
|
1772
|
+
handleError(err);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
async function tagAction(title, opts) {
|
|
1776
|
+
try {
|
|
1777
|
+
const { ctx, workspaceId } = await setup(opts);
|
|
1778
|
+
const target = await resolveTarget(ctx, workspaceId, { id: opts.id, title });
|
|
1779
|
+
let tags;
|
|
1780
|
+
if (opts.set !== void 0) {
|
|
1781
|
+
tags = splitCsv(opts.set) ?? [];
|
|
1782
|
+
} else {
|
|
1783
|
+
const current = target.note ?? await getNote(ctx, target.id);
|
|
1784
|
+
const set = new Set(current.tags);
|
|
1785
|
+
for (const t of splitCsv(opts.add) ?? []) set.add(t);
|
|
1786
|
+
for (const t of splitCsv(opts.remove) ?? []) set.delete(t);
|
|
1787
|
+
tags = [...set];
|
|
1788
|
+
}
|
|
1789
|
+
const note = await updateNote(ctx, target.id, { tags });
|
|
1790
|
+
emit(noteSummary(note), opts, () => `Tags: ${note.tags.join(", ") || "(none)"}`);
|
|
1791
|
+
} catch (err) {
|
|
1792
|
+
handleError(err);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
function registerNotesCommands(program2) {
|
|
1796
|
+
const notes = program2.command("notes").description("Create, read, update, and delete notes");
|
|
1797
|
+
notes.command("list").description("List notes").option("--folder <name>", "filter by folder").option("--tag <name>", "filter by tag").option("--favorite", "favorites only").option("--sort <field>", "sort by title|createdAt|updatedAt").option("--limit <n>", "max results (default 10)").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").option("--quiet", "suppress output").action(listAction);
|
|
1798
|
+
notes.command("search <query>").description("Search notes (keyword/semantic/hybrid). Unavailable on encrypted accounts.").option("--mode <mode>", "keyword | semantic | hybrid (default keyword)").option("--folder <name>", "restrict to a folder").option("--tag <name>", "restrict to a tag").option("--limit <n>", "max results (default 10)").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(searchAction);
|
|
1799
|
+
notes.command("get [title]").description("Print a note (markdown to stdout, or --json)").option("--id <id>", "address by id instead of title").option("--workspace <name|id>", "workspace scope").option("--json", "output the full note as JSON").action(getAction);
|
|
1800
|
+
notes.command("create <title>").description("Create a note (defaults to the workspace's default folder)").option("--content <text>", "note content").option("--stdin", "read content from stdin").option("--file <path>", "read content from a file").option("--folder <name>", "place in a folder").option("--tags <csv>", "comma-separated tags").option("--workspace <name|id>", "workspace scope").option("--json", "output the created note as JSON").action(createAction);
|
|
1801
|
+
notes.command("edit [title]").description("Replace or append note content").option("--id <id>", "address by id instead of title").option("--content <text>", "new content").option("--stdin", "read content from stdin").option("--file <path>", "read content from a file").option("--append <text>", "append to existing content").option("--workspace <name|id>", "workspace scope").option("--json", "output the updated note as JSON").action(editAction);
|
|
1802
|
+
notes.command("delete [title]").description("Move a note to Trash").option("--id <id>", "address by id instead of title").option("--confirm", "skip the confirmation prompt").option("--workspace <name|id>", "workspace scope").option("--quiet", "suppress output").action(deleteAction);
|
|
1803
|
+
const trash = notes.command("trash").description("List and manage trashed notes").option("--workspace <name|id>", "scope to a workspace (default: all)").option("--json", "output JSON").action(trashListAction);
|
|
1804
|
+
trash.command("list").description("List trashed notes").option("--workspace <name|id>", "scope to a workspace (default: all)").option("--json", "output JSON").action(trashListAction);
|
|
1805
|
+
trash.command("delete [title]").description("Permanently delete a trashed note by name or --id, or pick interactively").option("--id <id>", "address by id instead of name").option("--workspace <name|id>", "scope to a workspace (default: all)").option("--quiet", "suppress output").action(trashDeleteAction);
|
|
1806
|
+
trash.command("empty").description("Permanently delete all trashed notes").option("--workspace <name|id>", "only this workspace's trash").option("--confirm", "skip the confirmation prompt").option("--quiet", "suppress output").action(emptyTrashAction);
|
|
1807
|
+
notes.command("restore [title]").description("Restore a note from Trash").option("--id <id>", "address by id instead of title").option("--workspace <name|id>", "workspace scope").option("--json", "output the restored note as JSON").action(restoreAction);
|
|
1808
|
+
notes.command("move [title]").description("Move a note to a folder").option("--id <id>", "address by id instead of title").option("--to <folder>", "destination folder name").option("--to-default", "move to the workspace's default folder").option("--workspace <name|id>", "workspace scope").option("--json", "output the updated note as JSON").action(moveAction);
|
|
1809
|
+
notes.command("tag [title]").description("Add, remove, or replace a note's tags").option("--id <id>", "address by id instead of title").option("--add <csv>", "tags to add").option("--remove <csv>", "tags to remove").option("--set <csv>", "replace all tags").option("--workspace <name|id>", "workspace scope").option("--json", "output the updated note as JSON").action(tagAction);
|
|
1810
|
+
notes.command("favorite [title]").description("Mark a note as favorite (--off to unfavorite)").option("--id <id>", "address by id instead of title").option("--off", "remove from favorites").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(favoriteAction);
|
|
1811
|
+
notes.command("versions [title]").description("List a note's version history (restore one with --restore <version-id>)").option("--restore <version-id>", "restore the note to this version (id from the list)").option("--id <id>", "address the note by id instead of title").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(versionsAction);
|
|
1812
|
+
return notes;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// src/commands/folders.ts
|
|
1816
|
+
import { confirm as confirm2, isCancel as isCancel3 } from "@clack/prompts";
|
|
1817
|
+
|
|
1818
|
+
// src/lib/cli-error.ts
|
|
1819
|
+
function exitWithError(err) {
|
|
1820
|
+
if (err instanceof ApiError) {
|
|
1821
|
+
console.error(err.message);
|
|
1822
|
+
process.exit(err.status === 401 || err.status === 403 ? EXIT.auth : EXIT.error);
|
|
1823
|
+
}
|
|
1824
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1825
|
+
process.exit(EXIT.error);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// src/commands/folders.ts
|
|
1829
|
+
function fail2(err) {
|
|
1830
|
+
if (err instanceof AmbiguousFolderError) {
|
|
1831
|
+
console.error(err.message);
|
|
1832
|
+
console.error(
|
|
1833
|
+
renderTable(
|
|
1834
|
+
["ID", "Name", "Parent"],
|
|
1835
|
+
err.matches.map((f) => [f.id, f.name, f.parentId ?? ""])
|
|
1836
|
+
)
|
|
1837
|
+
);
|
|
1838
|
+
console.error("Re-run with --id <id>.");
|
|
1839
|
+
process.exit(EXIT.error);
|
|
1840
|
+
}
|
|
1841
|
+
exitWithError(err);
|
|
1842
|
+
}
|
|
1843
|
+
async function scope(opts) {
|
|
1844
|
+
const store = resolveStore();
|
|
1845
|
+
const { authed } = authContext(store);
|
|
1846
|
+
const config = createConfig();
|
|
1847
|
+
const workspace = await resolveActiveWorkspace(authed, opts.workspace, config.get("workspace"));
|
|
1848
|
+
return { authed, workspaceId: workspace.id };
|
|
1849
|
+
}
|
|
1850
|
+
async function listAction2(opts) {
|
|
1851
|
+
try {
|
|
1852
|
+
const { authed, workspaceId } = await scope(opts);
|
|
1853
|
+
const folders = await listFolders(authed, workspaceId);
|
|
1854
|
+
emit(
|
|
1855
|
+
orderFolderTree(folders).map((n) => ({
|
|
1856
|
+
id: n.id,
|
|
1857
|
+
name: n.name,
|
|
1858
|
+
parentId: n.parentId,
|
|
1859
|
+
// Direct note count (like web/desktop/mobile), not the subtree total.
|
|
1860
|
+
notes: n.count
|
|
1861
|
+
})),
|
|
1862
|
+
opts,
|
|
1863
|
+
() => folderTreeLines(
|
|
1864
|
+
folders,
|
|
1865
|
+
(f) => `${f.name} (${f.count} note${f.count === 1 ? "" : "s"})`
|
|
1866
|
+
).join("\n") || "(no folders)"
|
|
1867
|
+
);
|
|
1868
|
+
} catch (err) {
|
|
1869
|
+
fail2(err);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
async function createAction2(name, opts) {
|
|
1873
|
+
try {
|
|
1874
|
+
const { authed, workspaceId } = await scope(opts);
|
|
1875
|
+
let parentId;
|
|
1876
|
+
if (opts.parent) {
|
|
1877
|
+
const folders = await listFolders(authed, workspaceId);
|
|
1878
|
+
parentId = resolveFolder(folders, opts.parent).id;
|
|
1879
|
+
}
|
|
1880
|
+
const folder = await createFolder(authed, { name, parentId });
|
|
1881
|
+
if (!parentId) {
|
|
1882
|
+
await addFolderToWorkspace(authed, workspaceId, folder.id);
|
|
1883
|
+
}
|
|
1884
|
+
emit(folder, opts, () => `Created folder "${folder.name}" (${folder.id}).`);
|
|
1885
|
+
} catch (err) {
|
|
1886
|
+
fail2(err);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
async function renameAction(name, newName, opts) {
|
|
1890
|
+
try {
|
|
1891
|
+
const { authed, workspaceId } = await scope(opts);
|
|
1892
|
+
const id = opts.id ?? resolveFolder(await listFolders(authed, workspaceId), name).id;
|
|
1893
|
+
const folder = await renameFolder(authed, id, newName);
|
|
1894
|
+
emit(folder, opts, () => `Renamed to "${folder.name}".`);
|
|
1895
|
+
} catch (err) {
|
|
1896
|
+
fail2(err);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
async function deleteAction2(name, opts) {
|
|
1900
|
+
try {
|
|
1901
|
+
const { authed, workspaceId } = await scope(opts);
|
|
1902
|
+
const id = opts.id ?? resolveFolder(await listFolders(authed, workspaceId), name).id;
|
|
1903
|
+
const mode = opts.recursive ? "recursive" : "move-up";
|
|
1904
|
+
if (!opts.confirm) {
|
|
1905
|
+
const message = opts.recursive ? `Move "${name}" and everything inside it to Trash?` : `Move "${name}" to Trash (its notes rise to the parent)?`;
|
|
1906
|
+
const ok = await confirm2({ message });
|
|
1907
|
+
if (isCancel3(ok) || !ok) {
|
|
1908
|
+
if (!opts.quiet) console.error("Cancelled.");
|
|
1909
|
+
process.exit(EXIT.usage);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
await deleteFolder(authed, id, mode);
|
|
1913
|
+
if (!opts.quiet) console.log("Moved to Trash.");
|
|
1914
|
+
} catch (err) {
|
|
1915
|
+
fail2(err);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
async function restoreAction2(name, opts) {
|
|
1919
|
+
try {
|
|
1920
|
+
const { authed, workspaceId } = await scope(opts);
|
|
1921
|
+
const id = opts.id ?? resolveFolder(await listTrashedFolders(authed, workspaceId), name).id;
|
|
1922
|
+
await restoreFolder(authed, id);
|
|
1923
|
+
if (!opts.quiet) console.log(`Restored "${name}".`);
|
|
1924
|
+
} catch (err) {
|
|
1925
|
+
fail2(err);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
async function favoriteAction2(name, opts) {
|
|
1929
|
+
try {
|
|
1930
|
+
const { authed, workspaceId } = await scope(opts);
|
|
1931
|
+
const id = opts.id ?? resolveFolder(await listFolders(authed, workspaceId), name).id;
|
|
1932
|
+
const favorite = !opts.off;
|
|
1933
|
+
const res = await setFolderFavorite(authed, id, favorite);
|
|
1934
|
+
emit(res, opts, () => `${favorite ? "Favorited" : "Unfavorited"} folder "${name}".`);
|
|
1935
|
+
} catch (err) {
|
|
1936
|
+
fail2(err);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
async function moveAction2(name, opts) {
|
|
1940
|
+
try {
|
|
1941
|
+
const { authed, workspaceId } = await scope(opts);
|
|
1942
|
+
const folders = await listFolders(authed, workspaceId);
|
|
1943
|
+
const folder = resolveFolder(folders, opts.id ?? name);
|
|
1944
|
+
let parentId;
|
|
1945
|
+
if (opts.toRoot) {
|
|
1946
|
+
parentId = null;
|
|
1947
|
+
} else if (opts.to) {
|
|
1948
|
+
parentId = resolveFolder(folders, opts.to).id;
|
|
1949
|
+
} else {
|
|
1950
|
+
throw new ApiError("Provide --to <parent> or --to-root.", 2, "usage");
|
|
1951
|
+
}
|
|
1952
|
+
let res;
|
|
1953
|
+
try {
|
|
1954
|
+
res = await moveFolder(authed, folder.id, parentId);
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
if (err instanceof ApiError && err.status === 409) {
|
|
1957
|
+
res = await moveFolder(authed, folder.id, parentId, { confirmCrossBoundary: true });
|
|
1958
|
+
} else {
|
|
1959
|
+
throw err;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
if (parentId === null) {
|
|
1963
|
+
await addFolderToWorkspace(authed, workspaceId, folder.id);
|
|
1964
|
+
}
|
|
1965
|
+
emit(res, opts, () => `Moved "${folder.name}"${parentId ? "" : " to the top level"}.`);
|
|
1966
|
+
} catch (err) {
|
|
1967
|
+
fail2(err);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
async function trashListAction2(opts) {
|
|
1971
|
+
try {
|
|
1972
|
+
const { authed, workspaceId } = await scope(opts);
|
|
1973
|
+
const folders = await listTrashedFolders(authed, workspaceId);
|
|
1974
|
+
emit(
|
|
1975
|
+
folders.map((f) => ({ id: f.id, name: f.name, parentId: f.parentId })),
|
|
1976
|
+
opts,
|
|
1977
|
+
() => folders.length ? renderTable(["Name", "ID"], folders.map((f) => [f.name, f.id])) : "Folder trash is empty."
|
|
1978
|
+
);
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
fail2(err);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
async function trashDeleteAction2(name, opts) {
|
|
1984
|
+
try {
|
|
1985
|
+
const { authed, workspaceId } = await scope(opts);
|
|
1986
|
+
const folder = resolveFolder(await listTrashedFolders(authed, workspaceId), opts.id ?? name);
|
|
1987
|
+
if (!opts.confirm) {
|
|
1988
|
+
const ok = await confirm2({
|
|
1989
|
+
message: `Permanently delete folder "${folder.name}" and its subtree? This can't be undone.`
|
|
1990
|
+
});
|
|
1991
|
+
if (isCancel3(ok) || !ok) {
|
|
1992
|
+
if (!opts.quiet) console.error("Cancelled.");
|
|
1993
|
+
process.exit(EXIT.usage);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
await permanentDeleteFolder(authed, folder.id);
|
|
1997
|
+
if (!opts.quiet) console.log(`Permanently deleted folder "${folder.name}".`);
|
|
1998
|
+
} catch (err) {
|
|
1999
|
+
fail2(err);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
function registerFoldersCommands(program2) {
|
|
2003
|
+
const folders = program2.command("folders").description("Manage folders");
|
|
2004
|
+
folders.command("list").description("Show the folder tree with note counts").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(listAction2);
|
|
2005
|
+
folders.command("create <name>").description("Create a folder").option("--parent <name|id>", "parent folder (otherwise top-level)").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(createAction2);
|
|
2006
|
+
folders.command("rename <name> <newName>").description("Rename a folder").option("--id <id>", "address by id instead of name").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(renameAction);
|
|
2007
|
+
folders.command("delete <name>").description("Move a folder to Trash").option("--recursive", "also trash the folder's subtree (default: children rise to parent)").option("--id <id>", "address by id instead of name").option("--confirm", "skip the confirmation prompt").option("--workspace <name|id>", "workspace scope").option("--quiet", "suppress output").action(deleteAction2);
|
|
2008
|
+
folders.command("restore <name>").description("Restore a folder from Trash").option("--id <id>", "address by id instead of name").option("--workspace <name|id>", "workspace scope").option("--quiet", "suppress output").action(restoreAction2);
|
|
2009
|
+
folders.command("move <name>").description("Move a folder under a new parent (or --to-root for top-level)").option("--to <parent>", "destination parent folder").option("--to-root", "move to the top level (no parent)").option("--id <id>", "address by id instead of name").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(moveAction2);
|
|
2010
|
+
folders.command("favorite <name>").description("Mark a folder as favorite (--off to unfavorite)").option("--off", "remove from favorites").option("--id <id>", "address by id instead of name").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(favoriteAction2);
|
|
2011
|
+
const trash = folders.command("trash").description("List and manage trashed folders").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(trashListAction2);
|
|
2012
|
+
trash.command("list").description("List trashed folders").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(trashListAction2);
|
|
2013
|
+
trash.command("delete <name>").description("Permanently delete a trashed folder + its subtree").option("--id <id>", "address by id instead of name").option("--confirm", "skip the confirmation prompt").option("--workspace <name|id>", "workspace scope").option("--quiet", "suppress output").action(trashDeleteAction2);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// src/commands/tags.ts
|
|
2017
|
+
import { confirm as confirm3, isCancel as isCancel4 } from "@clack/prompts";
|
|
2018
|
+
|
|
2019
|
+
// src/lib/tags.ts
|
|
2020
|
+
async function listTags(authed, workspaceId) {
|
|
2021
|
+
const res = await authed("/notes/tags", { query: { workspaceId } });
|
|
2022
|
+
return res.tags;
|
|
2023
|
+
}
|
|
2024
|
+
function renameTag(authed, name, newName) {
|
|
2025
|
+
return authed(`/notes/tags/${encodeURIComponent(name)}`, {
|
|
2026
|
+
method: "PATCH",
|
|
2027
|
+
body: { newName }
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
function deleteTag(authed, name) {
|
|
2031
|
+
return authed(`/notes/tags/${encodeURIComponent(name)}`, {
|
|
2032
|
+
method: "DELETE"
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// src/commands/tags.ts
|
|
2037
|
+
async function scope2(opts) {
|
|
2038
|
+
const store = resolveStore();
|
|
2039
|
+
const { authed, enabled } = await refreshEncryptionState(store);
|
|
2040
|
+
if (enabled) {
|
|
2041
|
+
console.error(
|
|
2042
|
+
"Tags aren't available on encrypted accounts. (The server can't read your encrypted tags.)"
|
|
2043
|
+
);
|
|
2044
|
+
process.exit(EXIT.error);
|
|
2045
|
+
}
|
|
2046
|
+
const config = createConfig();
|
|
2047
|
+
const workspace = await resolveActiveWorkspace(authed, opts.workspace, config.get("workspace"));
|
|
2048
|
+
return { authed, workspaceId: workspace.id };
|
|
2049
|
+
}
|
|
2050
|
+
async function listAction3(opts) {
|
|
2051
|
+
try {
|
|
2052
|
+
const { authed, workspaceId } = await scope2(opts);
|
|
2053
|
+
const tags = await listTags(authed, workspaceId);
|
|
2054
|
+
if (opts.sort === "count") tags.sort((a, b) => b.count - a.count);
|
|
2055
|
+
emit(
|
|
2056
|
+
tags,
|
|
2057
|
+
opts,
|
|
2058
|
+
() => renderTable(
|
|
2059
|
+
["Tag", "Count"],
|
|
2060
|
+
tags.map((t) => [t.name, t.count])
|
|
2061
|
+
)
|
|
2062
|
+
);
|
|
2063
|
+
} catch (err) {
|
|
2064
|
+
exitWithError(err);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
async function renameAction2(oldName, newName, opts) {
|
|
2068
|
+
try {
|
|
2069
|
+
const { authed } = await scope2(opts);
|
|
2070
|
+
const res = await renameTag(authed, oldName, newName);
|
|
2071
|
+
emit(res, opts, () => `Renamed tag on ${res.updated} note(s).`);
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
exitWithError(err);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
async function deleteAction3(name, opts) {
|
|
2077
|
+
try {
|
|
2078
|
+
const { authed } = await scope2(opts);
|
|
2079
|
+
if (!opts.confirm) {
|
|
2080
|
+
const ok = await confirm3({ message: `Remove tag "${name}" from all notes?` });
|
|
2081
|
+
if (isCancel4(ok) || !ok) {
|
|
2082
|
+
if (!opts.quiet) console.error("Cancelled.");
|
|
2083
|
+
process.exit(EXIT.usage);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
const res = await deleteTag(authed, name);
|
|
2087
|
+
if (!opts.quiet) console.log(`Removed tag from ${res.updated} note(s).`);
|
|
2088
|
+
} catch (err) {
|
|
2089
|
+
exitWithError(err);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
function registerTagsCommands(program2) {
|
|
2093
|
+
const tags = program2.command("tags").description("Manage tags");
|
|
2094
|
+
tags.command("list").description("List tags with usage counts").option("--sort <field>", "sort by count").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(listAction3);
|
|
2095
|
+
tags.command("rename <oldName> <newName>").description("Rename a tag across all notes").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(renameAction2);
|
|
2096
|
+
tags.command("delete <name>").description("Remove a tag from all notes").option("--confirm", "skip the confirmation prompt").option("--workspace <name|id>", "workspace scope").option("--quiet", "suppress output").action(deleteAction3);
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// src/lib/stats.ts
|
|
2100
|
+
async function gatherStats(authed, workspaceId, encryptionEnabled) {
|
|
2101
|
+
const [all, favs, folders, dashboard] = await Promise.all([
|
|
2102
|
+
authed("/notes", { query: { workspaceId, pageSize: 1 } }),
|
|
2103
|
+
authed("/notes", { query: { workspaceId, favorite: "true", pageSize: 1 } }),
|
|
2104
|
+
authed("/notes/folders", { query: { workspaceId } }),
|
|
2105
|
+
authed("/notes/dashboard", {
|
|
2106
|
+
query: { workspaceId }
|
|
2107
|
+
})
|
|
2108
|
+
]);
|
|
2109
|
+
let tags = null;
|
|
2110
|
+
if (!encryptionEnabled) {
|
|
2111
|
+
tags = (await authed("/notes/tags", { query: { workspaceId } })).tags.length;
|
|
2112
|
+
}
|
|
2113
|
+
return {
|
|
2114
|
+
notes: all.total,
|
|
2115
|
+
favorites: favs.total,
|
|
2116
|
+
folders: flattenFolders(folders.folders).length,
|
|
2117
|
+
tags,
|
|
2118
|
+
audio: dashboard.audioNotes.length
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// src/commands/stats.ts
|
|
2123
|
+
async function statsAction(opts) {
|
|
2124
|
+
try {
|
|
2125
|
+
const store = resolveStore();
|
|
2126
|
+
const { authed, enabled } = await refreshEncryptionState(store);
|
|
2127
|
+
const config = createConfig();
|
|
2128
|
+
const workspace = await resolveActiveWorkspace(authed, opts.workspace, config.get("workspace"));
|
|
2129
|
+
const stats = await gatherStats(authed, workspace.id, enabled);
|
|
2130
|
+
emit(stats, opts, () => {
|
|
2131
|
+
const tags = stats.tags === null ? "n/a (encrypted)" : String(stats.tags);
|
|
2132
|
+
return [
|
|
2133
|
+
`Notes: ${stats.notes}`,
|
|
2134
|
+
`Favorites: ${stats.favorites}`,
|
|
2135
|
+
`Folders: ${stats.folders}`,
|
|
2136
|
+
`Tags: ${tags}`,
|
|
2137
|
+
`Audio: ${stats.audio}`
|
|
2138
|
+
].join("\n");
|
|
2139
|
+
});
|
|
2140
|
+
} catch (err) {
|
|
2141
|
+
exitWithError(err);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
function registerStatsCommand(program2) {
|
|
2145
|
+
program2.command("stats").description("Show summary statistics for the active workspace").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(statsAction);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// src/commands/workspaces.ts
|
|
2149
|
+
import { confirm as confirm4, select, isCancel as isCancel5, cancel as cancel3 } from "@clack/prompts";
|
|
2150
|
+
function scope3() {
|
|
2151
|
+
const store = resolveStore();
|
|
2152
|
+
const { authed } = authContext(store);
|
|
2153
|
+
return { authed, config: createConfig() };
|
|
2154
|
+
}
|
|
2155
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
2156
|
+
function workspaceOptionLabel(w) {
|
|
2157
|
+
return `${w.name} ${plural(w.folderCount, "folder")} \xB7 ${plural(w.noteCount ?? 0, "note")} ${w.id}`;
|
|
2158
|
+
}
|
|
2159
|
+
async function chooseWorkspace(matches, name, opts) {
|
|
2160
|
+
if (!process.stdin.isTTY || opts.quiet) {
|
|
2161
|
+
throw new AmbiguousWorkspaceError(name, matches);
|
|
2162
|
+
}
|
|
2163
|
+
const choice = await select({
|
|
2164
|
+
message: `Multiple workspaces named "${name}". Which one?`,
|
|
2165
|
+
options: matches.map((w) => ({ value: w.id, label: workspaceOptionLabel(w) }))
|
|
2166
|
+
});
|
|
2167
|
+
if (isCancel5(choice)) {
|
|
2168
|
+
cancel3("Cancelled.");
|
|
2169
|
+
process.exit(EXIT.usage);
|
|
2170
|
+
}
|
|
2171
|
+
return matches.find((w) => w.id === choice);
|
|
2172
|
+
}
|
|
2173
|
+
async function listAction4(opts) {
|
|
2174
|
+
try {
|
|
2175
|
+
const { authed, config } = scope3();
|
|
2176
|
+
const workspaces = await listWorkspaces(authed);
|
|
2177
|
+
let activeId = null;
|
|
2178
|
+
try {
|
|
2179
|
+
activeId = resolveWorkspace(workspaces, config.get("workspace")).id;
|
|
2180
|
+
} catch {
|
|
2181
|
+
activeId = null;
|
|
2182
|
+
}
|
|
2183
|
+
const sorted = [...workspaces].sort(
|
|
2184
|
+
(a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" })
|
|
2185
|
+
);
|
|
2186
|
+
emit(
|
|
2187
|
+
sorted.map((w) => ({
|
|
2188
|
+
id: w.id,
|
|
2189
|
+
name: w.name,
|
|
2190
|
+
folders: w.folderCount,
|
|
2191
|
+
notes: w.noteCount ?? 0,
|
|
2192
|
+
active: w.id === activeId
|
|
2193
|
+
})),
|
|
2194
|
+
opts,
|
|
2195
|
+
() => renderTable(
|
|
2196
|
+
["", "Name", "ID", "Folders", "Notes"],
|
|
2197
|
+
sorted.map((w) => [
|
|
2198
|
+
w.id === activeId ? "\u2605" : "",
|
|
2199
|
+
w.name,
|
|
2200
|
+
w.id,
|
|
2201
|
+
w.folderCount,
|
|
2202
|
+
w.noteCount ?? 0
|
|
2203
|
+
])
|
|
2204
|
+
)
|
|
2205
|
+
);
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
exitWithError(err);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
async function useAction(nameOrId, opts) {
|
|
2211
|
+
try {
|
|
2212
|
+
const { authed, config } = scope3();
|
|
2213
|
+
const matches = matchWorkspaces(await listWorkspaces(authed), nameOrId);
|
|
2214
|
+
if (matches.length === 0) throw new Error(`Workspace not found: ${nameOrId}`);
|
|
2215
|
+
const ws = matches.length === 1 ? matches[0] : await chooseWorkspace(matches, nameOrId, opts);
|
|
2216
|
+
config.set("workspace", ws.id);
|
|
2217
|
+
if (!opts.quiet) console.log(`Active workspace: ${ws.name}`);
|
|
2218
|
+
} catch (err) {
|
|
2219
|
+
exitWithError(err);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
async function createAction3(name, opts) {
|
|
2223
|
+
try {
|
|
2224
|
+
const { authed } = scope3();
|
|
2225
|
+
const { workspace } = await createWorkspace(authed, name, { color: opts.color, icon: opts.icon });
|
|
2226
|
+
emit(workspace, opts, () => `Created workspace "${workspace.name}" (${workspace.id}).`);
|
|
2227
|
+
} catch (err) {
|
|
2228
|
+
exitWithError(err);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
async function renameAction3(name, newName, opts) {
|
|
2232
|
+
try {
|
|
2233
|
+
const { authed } = scope3();
|
|
2234
|
+
const ws = resolveWorkspace(await listWorkspaces(authed), name);
|
|
2235
|
+
const { workspace } = await renameWorkspace(authed, ws.id, newName);
|
|
2236
|
+
emit(workspace, opts, () => `Renamed workspace to "${workspace.name}".`);
|
|
2237
|
+
} catch (err) {
|
|
2238
|
+
exitWithError(err);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
async function deleteAction4(name, opts) {
|
|
2242
|
+
try {
|
|
2243
|
+
const { authed, config } = scope3();
|
|
2244
|
+
const matches = matchWorkspaces(await listWorkspaces(authed), name);
|
|
2245
|
+
if (matches.length === 0) throw new Error(`Workspace not found: ${name}`);
|
|
2246
|
+
const ws = matches.length === 1 ? matches[0] : await chooseWorkspace(matches, name, opts);
|
|
2247
|
+
if (!opts.confirm) {
|
|
2248
|
+
const ok = await confirm4({ message: `Delete workspace "${ws.name}"? (folders/notes stay; they're just unscoped)` });
|
|
2249
|
+
if (isCancel5(ok) || !ok) {
|
|
2250
|
+
if (!opts.quiet) console.error("Cancelled.");
|
|
2251
|
+
process.exit(EXIT.usage);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
await deleteWorkspace(authed, ws.id);
|
|
2255
|
+
if (config.get("workspace") === ws.name || config.get("workspace") === ws.id) {
|
|
2256
|
+
config.set("workspace", null);
|
|
2257
|
+
}
|
|
2258
|
+
if (!opts.quiet) console.log(`Deleted workspace "${ws.name}".`);
|
|
2259
|
+
} catch (err) {
|
|
2260
|
+
exitWithError(err);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
async function setDefaultFolderAction(folderName, opts) {
|
|
2264
|
+
try {
|
|
2265
|
+
const { authed, config } = scope3();
|
|
2266
|
+
const ws = resolveWorkspace(await listWorkspaces(authed), opts.workspace ?? config.get("workspace"));
|
|
2267
|
+
const folder = resolveFolder(await listFolders(authed, ws.id), folderName);
|
|
2268
|
+
const { workspace } = await setWorkspaceDefaultFolder(authed, ws.id, folder.id);
|
|
2269
|
+
emit(workspace, opts, () => `Default folder for "${workspace.name}" set to "${folder.name}".`);
|
|
2270
|
+
} catch (err) {
|
|
2271
|
+
exitWithError(err);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
async function removeFolderAction(folderName, opts) {
|
|
2275
|
+
try {
|
|
2276
|
+
const { authed, config } = scope3();
|
|
2277
|
+
const ws = resolveWorkspace(await listWorkspaces(authed), opts.workspace ?? config.get("workspace"));
|
|
2278
|
+
const folder = resolveFolder(await listFolders(authed, ws.id), folderName);
|
|
2279
|
+
await removeFolderFromWorkspace(authed, ws.id, folder.id);
|
|
2280
|
+
if (!opts.quiet) console.log(`Removed folder "${folder.name}" from workspace "${ws.name}".`);
|
|
2281
|
+
} catch (err) {
|
|
2282
|
+
exitWithError(err);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
async function addFolderAction(folderName, opts) {
|
|
2286
|
+
try {
|
|
2287
|
+
const { authed, config } = scope3();
|
|
2288
|
+
const ws = resolveWorkspace(await listWorkspaces(authed), opts.workspace ?? config.get("workspace"));
|
|
2289
|
+
const folder = resolveFolder(await listFolders(authed), folderName);
|
|
2290
|
+
await addFolderToWorkspace(authed, ws.id, folder.id);
|
|
2291
|
+
if (!opts.quiet) console.log(`Added folder "${folder.name}" to workspace "${ws.name}".`);
|
|
2292
|
+
} catch (err) {
|
|
2293
|
+
exitWithError(err);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
function registerWorkspacesCommands(program2) {
|
|
2297
|
+
const workspaces = program2.command("workspaces").description("Manage workspaces");
|
|
2298
|
+
workspaces.command("list").description("List workspaces (the active one is marked \u2605)").option("--json", "output JSON").action(listAction4);
|
|
2299
|
+
workspaces.command("use <name|id>").description("Set the active (default) workspace").option("--quiet", "suppress output").action(useAction);
|
|
2300
|
+
workspaces.command("create <name>").description("Create a workspace").option("--color <color>", "workspace color").option("--icon <icon>", "workspace icon").option("--json", "output JSON").action(createAction3);
|
|
2301
|
+
workspaces.command("rename <name> <newName>").description("Rename a workspace").option("--json", "output JSON").action(renameAction3);
|
|
2302
|
+
workspaces.command("delete <name>").description("Delete a workspace (its folders/notes are kept, just unscoped)").option("--confirm", "skip the confirmation prompt").option("--quiet", "suppress output").action(deleteAction4);
|
|
2303
|
+
workspaces.command("set-default-folder <folder>").description("Set the folder new notes default into for a workspace").option("--workspace <name|id>", "target workspace (else the active one)").option("--json", "output JSON").action(setDefaultFolderAction);
|
|
2304
|
+
workspaces.command("add-folder <folder>").description("Link an existing folder (root) into a workspace (multi-workspace membership)").option("--workspace <name|id>", "target workspace (else the active one)").option("--quiet", "suppress output").action(addFolderAction);
|
|
2305
|
+
workspaces.command("remove-folder <folder>").description("Unlink a folder (root) from a workspace (the folder is not deleted)").option("--workspace <name|id>", "target workspace (else the active one)").option("--quiet", "suppress output").action(removeFolderAction);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// src/commands/ai.ts
|
|
2309
|
+
import pc from "picocolors";
|
|
2310
|
+
|
|
2311
|
+
// src/lib/ai.ts
|
|
2312
|
+
async function parseSseStream(stream, handlers) {
|
|
2313
|
+
const reader = stream.getReader();
|
|
2314
|
+
const decoder = new TextDecoder();
|
|
2315
|
+
let buffer = "";
|
|
2316
|
+
for (; ; ) {
|
|
2317
|
+
const { done, value } = await reader.read();
|
|
2318
|
+
if (done) break;
|
|
2319
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2320
|
+
let sep;
|
|
2321
|
+
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
2322
|
+
const block = buffer.slice(0, sep);
|
|
2323
|
+
buffer = buffer.slice(sep + 2);
|
|
2324
|
+
const dataLine = block.split("\n").find((l) => l.startsWith("data:"));
|
|
2325
|
+
if (!dataLine) continue;
|
|
2326
|
+
const data = dataLine.slice(5).trim();
|
|
2327
|
+
if (data === "[DONE]") return;
|
|
2328
|
+
let event;
|
|
2329
|
+
try {
|
|
2330
|
+
event = JSON.parse(data);
|
|
2331
|
+
} catch {
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
dispatch(event, handlers);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
function dispatch(event, handlers) {
|
|
2339
|
+
if (typeof event.text === "string") handlers.onText?.(event.text);
|
|
2340
|
+
else if (event.tool && typeof event.tool === "object") {
|
|
2341
|
+
const t = event.tool;
|
|
2342
|
+
handlers.onTool?.(t.name ?? "", t.description ?? "");
|
|
2343
|
+
} else if (Array.isArray(event.noteCards)) handlers.onNoteCards?.(event.noteCards);
|
|
2344
|
+
else if (event.confirmation) handlers.onConfirmation?.(event.confirmation);
|
|
2345
|
+
else if (typeof event.error === "string") handlers.onError?.(event.error);
|
|
2346
|
+
}
|
|
2347
|
+
async function streamAsk(store, question, handlers) {
|
|
2348
|
+
let session = requireSession(store);
|
|
2349
|
+
const post = (token) => fetch(buildUrl(session.server, "/ai/ask"), {
|
|
2350
|
+
method: "POST",
|
|
2351
|
+
headers: {
|
|
2352
|
+
"content-type": "application/json",
|
|
2353
|
+
accept: "text/event-stream",
|
|
2354
|
+
authorization: `Bearer ${token}`
|
|
2355
|
+
},
|
|
2356
|
+
body: JSON.stringify({ question })
|
|
2357
|
+
});
|
|
2358
|
+
let res = await post(session.accessToken);
|
|
2359
|
+
if (res.status === 401) {
|
|
2360
|
+
session = await refreshSession(store, session);
|
|
2361
|
+
res = await post(session.accessToken);
|
|
2362
|
+
}
|
|
2363
|
+
if (!res.ok || !res.body) {
|
|
2364
|
+
let code;
|
|
2365
|
+
try {
|
|
2366
|
+
code = (await res.clone().json()).code;
|
|
2367
|
+
} catch {
|
|
2368
|
+
}
|
|
2369
|
+
throw new ApiError(`AI request failed with status ${res.status}`, res.status, code);
|
|
2370
|
+
}
|
|
2371
|
+
await parseSseStream(res.body, handlers);
|
|
2372
|
+
}
|
|
2373
|
+
async function summarizeNote(authed, noteId) {
|
|
2374
|
+
const res = await authed("/ai/summarize", {
|
|
2375
|
+
method: "POST",
|
|
2376
|
+
body: { noteId }
|
|
2377
|
+
});
|
|
2378
|
+
return res.summary;
|
|
2379
|
+
}
|
|
2380
|
+
async function suggestTags(authed, noteId) {
|
|
2381
|
+
const res = await authed("/ai/tags", { method: "POST", body: { noteId } });
|
|
2382
|
+
return res.tags;
|
|
2383
|
+
}
|
|
2384
|
+
async function rewriteText(authed, text2, action) {
|
|
2385
|
+
const res = await authed("/ai/rewrite", {
|
|
2386
|
+
method: "POST",
|
|
2387
|
+
body: { text: text2, action }
|
|
2388
|
+
});
|
|
2389
|
+
return res.text;
|
|
2390
|
+
}
|
|
2391
|
+
function aiErrorMessage(err) {
|
|
2392
|
+
if (err instanceof ApiError) {
|
|
2393
|
+
if (err.code === "encryption_no_ai") {
|
|
2394
|
+
return "AI features are unavailable while end-to-end encryption is on.";
|
|
2395
|
+
}
|
|
2396
|
+
if (err.status === 403) {
|
|
2397
|
+
return "AI is not available on your plan.";
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
return err instanceof Error ? err.message : String(err);
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// src/commands/ai.ts
|
|
2404
|
+
async function guardE2E(store) {
|
|
2405
|
+
if ((await refreshEncryptionState(store)).enabled) {
|
|
2406
|
+
console.error("AI features are unavailable while end-to-end encryption is on.");
|
|
2407
|
+
process.exit(EXIT.error);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
function failAi(err) {
|
|
2411
|
+
console.error(aiErrorMessage(err));
|
|
2412
|
+
const code = err instanceof ApiError && (err.status === 401 || err.status === 403) ? EXIT.auth : EXIT.error;
|
|
2413
|
+
process.exit(code);
|
|
2414
|
+
}
|
|
2415
|
+
async function resolveNote(opts, title) {
|
|
2416
|
+
const store = resolveStore();
|
|
2417
|
+
await guardE2E(store);
|
|
2418
|
+
const ctx = await buildNotesContext(store);
|
|
2419
|
+
const config = createConfig();
|
|
2420
|
+
const workspace = await resolveActiveWorkspace(ctx.authed, opts.workspace, config.get("workspace"));
|
|
2421
|
+
const target = await resolveNoteTarget(
|
|
2422
|
+
{ id: opts.id, title, workspaceId: workspace.id, useSearch: !ctx.contentKey },
|
|
2423
|
+
(params) => listNotes(ctx, params)
|
|
2424
|
+
);
|
|
2425
|
+
return { ctx, target };
|
|
2426
|
+
}
|
|
2427
|
+
async function askAction(question, opts) {
|
|
2428
|
+
const store = resolveStore();
|
|
2429
|
+
await guardE2E(store);
|
|
2430
|
+
try {
|
|
2431
|
+
await streamAsk(store, question, {
|
|
2432
|
+
onText: (t) => process.stdout.write(t),
|
|
2433
|
+
onTool: (name, description) => {
|
|
2434
|
+
if (!opts.quietTools) process.stderr.write(pc.dim(`
|
|
2435
|
+
${description || name}\u2026
|
|
2436
|
+
`));
|
|
2437
|
+
},
|
|
2438
|
+
onNoteCards: (cards) => {
|
|
2439
|
+
if (opts.quietTools) return;
|
|
2440
|
+
for (const card of cards) {
|
|
2441
|
+
const title = card.title;
|
|
2442
|
+
if (title) process.stderr.write(pc.dim(` \xB7 ${title}
|
|
2443
|
+
`));
|
|
2444
|
+
}
|
|
2445
|
+
},
|
|
2446
|
+
onError: (message) => process.stderr.write(`
|
|
2447
|
+
${message}
|
|
2448
|
+
`)
|
|
2449
|
+
});
|
|
2450
|
+
process.stdout.write("\n");
|
|
2451
|
+
} catch (err) {
|
|
2452
|
+
failAi(err);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
async function summarizeAction(title, opts) {
|
|
2456
|
+
try {
|
|
2457
|
+
const { ctx, target } = await resolveNote(opts, title);
|
|
2458
|
+
const summary = await summarizeNote(ctx.authed, target.id);
|
|
2459
|
+
emit({ summary }, opts, () => summary);
|
|
2460
|
+
} catch (err) {
|
|
2461
|
+
failAi(err);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
async function gentagsAction(title, opts) {
|
|
2465
|
+
try {
|
|
2466
|
+
const { ctx, target } = await resolveNote(opts, title);
|
|
2467
|
+
const tags = await suggestTags(ctx.authed, target.id);
|
|
2468
|
+
if (opts.apply) {
|
|
2469
|
+
const note = target.note ?? await getNote(ctx, target.id);
|
|
2470
|
+
const merged = [.../* @__PURE__ */ new Set([...note.tags, ...tags])];
|
|
2471
|
+
await updateNote(ctx, target.id, { tags: merged });
|
|
2472
|
+
}
|
|
2473
|
+
emit(
|
|
2474
|
+
{ tags, applied: Boolean(opts.apply) },
|
|
2475
|
+
opts,
|
|
2476
|
+
() => `${opts.apply ? "Applied" : "Suggested"} tags: ${tags.join(", ") || "(none)"}`
|
|
2477
|
+
);
|
|
2478
|
+
} catch (err) {
|
|
2479
|
+
failAi(err);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
async function readStdin2() {
|
|
2483
|
+
const chunks = [];
|
|
2484
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
2485
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
2486
|
+
}
|
|
2487
|
+
async function rewriteAction(text2, opts) {
|
|
2488
|
+
const store = resolveStore();
|
|
2489
|
+
await guardE2E(store);
|
|
2490
|
+
try {
|
|
2491
|
+
const input = opts.stdin ? await readStdin2() : text2;
|
|
2492
|
+
if (!input || !input.trim()) {
|
|
2493
|
+
console.error("Provide text to rewrite (an argument or --stdin).");
|
|
2494
|
+
process.exit(EXIT.usage);
|
|
2495
|
+
}
|
|
2496
|
+
const authed = createAuthedClient(store);
|
|
2497
|
+
const result = await rewriteText(authed, input, opts.action ?? "rewrite");
|
|
2498
|
+
process.stdout.write(result.endsWith("\n") ? result : `${result}
|
|
2499
|
+
`);
|
|
2500
|
+
} catch (err) {
|
|
2501
|
+
failAi(err);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
function registerAiCommands(program2) {
|
|
2505
|
+
program2.command("ask <question>").description("Ask the AI about your notes (streams the answer)").option("--quiet-tools", "hide tool-activity lines").action(askAction);
|
|
2506
|
+
program2.command("summarize [title]").description("Generate and save a note's summary").option("--id <id>", "address by id instead of title").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(summarizeAction);
|
|
2507
|
+
program2.command("gentags [title]").description("Suggest tags for a note (use --apply to add them)").option("--apply", "apply the suggested tags to the note").option("--id <id>", "address by id instead of title").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").action(gentagsAction);
|
|
2508
|
+
program2.command("rewrite [text]").description("Rewrite text with AI (prints the result to stdout)").option(
|
|
2509
|
+
"--action <action>",
|
|
2510
|
+
"rewrite | concise | fix-grammar | to-list | expand | summarize",
|
|
2511
|
+
"rewrite"
|
|
2512
|
+
).option("--stdin", "read text from stdin").action(rewriteAction);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// src/commands/transcribe.ts
|
|
2516
|
+
import ora from "ora";
|
|
2517
|
+
|
|
2518
|
+
// src/lib/transcribe.ts
|
|
2519
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2520
|
+
import { basename, extname } from "path";
|
|
2521
|
+
var MIME_BY_EXT = {
|
|
2522
|
+
".wav": "audio/wav",
|
|
2523
|
+
".mp3": "audio/mpeg",
|
|
2524
|
+
".m4a": "audio/mp4",
|
|
2525
|
+
".mp4": "audio/mp4",
|
|
2526
|
+
".webm": "audio/webm",
|
|
2527
|
+
".ogg": "audio/ogg"
|
|
2528
|
+
};
|
|
2529
|
+
function audioMimeForFile(path) {
|
|
2530
|
+
const ext = extname(path).toLowerCase();
|
|
2531
|
+
const mime = MIME_BY_EXT[ext];
|
|
2532
|
+
if (!mime) {
|
|
2533
|
+
throw new Error(
|
|
2534
|
+
`Unsupported audio type: ${ext || "(no extension)"}. Supported: wav, mp3, m4a/mp4, webm, ogg.`
|
|
2535
|
+
);
|
|
2536
|
+
}
|
|
2537
|
+
return mime;
|
|
2538
|
+
}
|
|
2539
|
+
async function transcribeFile(store, path, opts) {
|
|
2540
|
+
const buffer = readFileSync3(path);
|
|
2541
|
+
const mime = audioMimeForFile(path);
|
|
2542
|
+
let session = requireSession(store);
|
|
2543
|
+
const send = (token) => {
|
|
2544
|
+
const form = new FormData();
|
|
2545
|
+
form.append("mode", opts.mode);
|
|
2546
|
+
if (opts.folderId) form.append("folderId", opts.folderId);
|
|
2547
|
+
form.append("file", new Blob([buffer], { type: mime }), basename(path));
|
|
2548
|
+
return fetch(buildUrl(session.server, "/ai/transcribe"), {
|
|
2549
|
+
method: "POST",
|
|
2550
|
+
headers: { authorization: `Bearer ${token}` },
|
|
2551
|
+
body: form
|
|
2552
|
+
});
|
|
2553
|
+
};
|
|
2554
|
+
let res = await send(session.accessToken);
|
|
2555
|
+
if (res.status === 401) {
|
|
2556
|
+
session = await refreshSession(store, session);
|
|
2557
|
+
res = await send(session.accessToken);
|
|
2558
|
+
}
|
|
2559
|
+
if (!res.ok) {
|
|
2560
|
+
let code;
|
|
2561
|
+
try {
|
|
2562
|
+
code = (await res.clone().json()).code;
|
|
2563
|
+
} catch {
|
|
2564
|
+
}
|
|
2565
|
+
throw new ApiError(`Transcription failed with status ${res.status}`, res.status, code);
|
|
2566
|
+
}
|
|
2567
|
+
return await res.json();
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// src/commands/transcribe.ts
|
|
2571
|
+
var VALID_MODES = ["meeting", "lecture", "memo", "verbatim"];
|
|
2572
|
+
function fail3(err) {
|
|
2573
|
+
console.error(aiErrorMessage(err));
|
|
2574
|
+
const code = err instanceof ApiError && (err.status === 401 || err.status === 403) ? EXIT.auth : EXIT.error;
|
|
2575
|
+
process.exit(code);
|
|
2576
|
+
}
|
|
2577
|
+
async function transcribeAction(file, opts) {
|
|
2578
|
+
const store = resolveStore();
|
|
2579
|
+
if ((await refreshEncryptionState(store)).enabled) {
|
|
2580
|
+
console.error("AI features are unavailable while end-to-end encryption is on.");
|
|
2581
|
+
process.exit(EXIT.error);
|
|
2582
|
+
}
|
|
2583
|
+
const mode = opts.mode ?? "memo";
|
|
2584
|
+
if (!VALID_MODES.includes(mode)) {
|
|
2585
|
+
console.error(`Invalid --mode "${opts.mode}". Use: ${VALID_MODES.join(", ")}.`);
|
|
2586
|
+
process.exit(EXIT.usage);
|
|
2587
|
+
}
|
|
2588
|
+
try {
|
|
2589
|
+
const ctx = await buildNotesContext(store);
|
|
2590
|
+
const config = createConfig();
|
|
2591
|
+
const workspace = await resolveActiveWorkspace(ctx.authed, opts.workspace, config.get("workspace"));
|
|
2592
|
+
const folders = await listFolders(ctx.authed, workspace.id);
|
|
2593
|
+
let folderId;
|
|
2594
|
+
if (opts.folder) {
|
|
2595
|
+
folderId = resolveFolder(folders, opts.folder).id;
|
|
2596
|
+
} else {
|
|
2597
|
+
const target = resolveNewNoteFolderId(workspace.defaultFolderId ?? null, folders);
|
|
2598
|
+
if (!target) {
|
|
2599
|
+
throw new ApiError(
|
|
2600
|
+
"This workspace has no folders. Create one first (e.g. `notate folders create Notes`), or pass --folder.",
|
|
2601
|
+
2,
|
|
2602
|
+
"no_root"
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
folderId = target;
|
|
2606
|
+
}
|
|
2607
|
+
const spinner2 = opts.quiet || opts.json ? null : ora(`Transcribing ${file}`).start();
|
|
2608
|
+
try {
|
|
2609
|
+
const result = await transcribeFile(store, file, { mode, folderId });
|
|
2610
|
+
let note = result.note;
|
|
2611
|
+
const patch = {};
|
|
2612
|
+
if (opts.title) patch.title = opts.title;
|
|
2613
|
+
if (opts.tags) {
|
|
2614
|
+
patch.tags = [.../* @__PURE__ */ new Set([...note.tags, ...opts.tags.split(",").map((t) => t.trim()).filter(Boolean)])];
|
|
2615
|
+
}
|
|
2616
|
+
if (patch.title || patch.tags) note = await updateNote(ctx, note.id, patch);
|
|
2617
|
+
spinner2?.succeed(`Created "${note.title}"`);
|
|
2618
|
+
emit(
|
|
2619
|
+
{ id: note.id, title: note.title, folder: note.folder, tags: note.tags },
|
|
2620
|
+
opts,
|
|
2621
|
+
() => `Created "${note.title}"
|
|
2622
|
+
Folder: ${note.folder ?? "(default)"}
|
|
2623
|
+
Tags: ${note.tags.join(", ") || "(none)"}`
|
|
2624
|
+
);
|
|
2625
|
+
} catch (err) {
|
|
2626
|
+
spinner2?.fail("Transcription failed");
|
|
2627
|
+
throw err;
|
|
2628
|
+
}
|
|
2629
|
+
} catch (err) {
|
|
2630
|
+
fail3(err);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
function registerTranscribeCommand(program2) {
|
|
2634
|
+
program2.command("transcribe <file>").description("Transcribe an audio file into a structured note").option("--mode <mode>", "meeting | lecture | memo | verbatim (default memo)").option("--folder <name>", "place the note in a folder").option("--tags <csv>", "add tags").option("--title <title>", "override the generated title").option("--workspace <name|id>", "workspace scope").option("--json", "output JSON").option("--quiet", "suppress output").action(transcribeAction);
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// src/commands/importExport.ts
|
|
2638
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
2639
|
+
import { join as join3 } from "path";
|
|
2640
|
+
|
|
2641
|
+
// src/lib/importExport.ts
|
|
2642
|
+
import { readdirSync, statSync } from "fs";
|
|
2643
|
+
import { basename as basename2, extname as extname2, join as join2 } from "path";
|
|
2644
|
+
function deriveTitle(content, filename) {
|
|
2645
|
+
const heading = content.match(/^#\s+(.+?)\s*$/m);
|
|
2646
|
+
if (heading) return heading[1].trim();
|
|
2647
|
+
return basename2(filename).replace(/\.md$/i, "");
|
|
2648
|
+
}
|
|
2649
|
+
function sanitizeFilename(title) {
|
|
2650
|
+
const cleaned = title.replace(/[/\\:*?"<>|]/g, "-").replace(/\s+/g, " ").trim().slice(0, 100);
|
|
2651
|
+
return cleaned || "untitled";
|
|
2652
|
+
}
|
|
2653
|
+
function collectMarkdownFiles(path, recursive) {
|
|
2654
|
+
const stat = statSync(path);
|
|
2655
|
+
if (stat.isFile()) return extname2(path).toLowerCase() === ".md" ? [path] : [];
|
|
2656
|
+
const out = [];
|
|
2657
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
2658
|
+
const full = join2(path, entry.name);
|
|
2659
|
+
if (entry.isDirectory()) {
|
|
2660
|
+
if (recursive) out.push(...collectMarkdownFiles(full, recursive));
|
|
2661
|
+
} else if (extname2(entry.name).toLowerCase() === ".md") {
|
|
2662
|
+
out.push(full);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
return out.sort();
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// src/commands/importExport.ts
|
|
2669
|
+
var EXPORT_BATCH_CAP = 1e3;
|
|
2670
|
+
function fail4(err) {
|
|
2671
|
+
if (err instanceof AmbiguousTitleError) {
|
|
2672
|
+
console.error(err.message);
|
|
2673
|
+
console.error(
|
|
2674
|
+
renderTable(
|
|
2675
|
+
["ID", "Title", "Folder"],
|
|
2676
|
+
err.matches.map((n) => [n.id, n.title, n.folder ?? ""])
|
|
2677
|
+
)
|
|
2678
|
+
);
|
|
2679
|
+
console.error("Re-run with --id <id>.");
|
|
2680
|
+
process.exit(EXIT.error);
|
|
2681
|
+
}
|
|
2682
|
+
if (err instanceof ApiError) {
|
|
2683
|
+
console.error(err.message);
|
|
2684
|
+
process.exit(err.status === 401 || err.status === 403 ? EXIT.auth : EXIT.error);
|
|
2685
|
+
}
|
|
2686
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
2687
|
+
process.exit(EXIT.error);
|
|
2688
|
+
}
|
|
2689
|
+
async function exportAction(title, opts) {
|
|
2690
|
+
try {
|
|
2691
|
+
const store = resolveStore();
|
|
2692
|
+
const ctx = await buildNotesContext(store);
|
|
2693
|
+
const config = createConfig();
|
|
2694
|
+
const workspace = await resolveActiveWorkspace(ctx.authed, opts.workspace, config.get("workspace"));
|
|
2695
|
+
if (opts.all || opts.folder) {
|
|
2696
|
+
if (!opts.out) throw new ApiError("Batch export needs -o <dir>.", 2, "usage");
|
|
2697
|
+
const { notes, total } = await listNotes(ctx, {
|
|
2698
|
+
workspaceId: workspace.id,
|
|
2699
|
+
folder: opts.folder,
|
|
2700
|
+
pageSize: EXPORT_BATCH_CAP
|
|
2701
|
+
});
|
|
2702
|
+
mkdirSync2(opts.out, { recursive: true });
|
|
2703
|
+
const used = /* @__PURE__ */ new Set();
|
|
2704
|
+
for (const note2 of notes) {
|
|
2705
|
+
let name = sanitizeFilename(note2.title);
|
|
2706
|
+
if (used.has(name.toLowerCase())) name = `${name}-${note2.id.slice(0, 8)}`;
|
|
2707
|
+
used.add(name.toLowerCase());
|
|
2708
|
+
writeFileSync2(join3(opts.out, `${name}.md`), note2.content);
|
|
2709
|
+
}
|
|
2710
|
+
let msg = `Exported ${notes.length} note(s) to ${opts.out}`;
|
|
2711
|
+
if (total > notes.length) msg += ` (capped at ${EXPORT_BATCH_CAP} of ${total})`;
|
|
2712
|
+
if (!opts.quiet) console.log(`${msg}.`);
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
const target = await resolveNoteTarget(
|
|
2716
|
+
{ id: opts.id, title, workspaceId: workspace.id, useSearch: !ctx.contentKey },
|
|
2717
|
+
(params) => listNotes(ctx, params)
|
|
2718
|
+
);
|
|
2719
|
+
const note = target.note ?? await getNote(ctx, target.id);
|
|
2720
|
+
const payload = opts.format === "json" ? JSON.stringify(note, null, 2) : note.content;
|
|
2721
|
+
if (opts.out) {
|
|
2722
|
+
writeFileSync2(opts.out, payload);
|
|
2723
|
+
if (!opts.quiet) console.error(`Wrote ${opts.out}`);
|
|
2724
|
+
} else {
|
|
2725
|
+
process.stdout.write(payload.endsWith("\n") ? payload : `${payload}
|
|
2726
|
+
`);
|
|
2727
|
+
}
|
|
2728
|
+
} catch (err) {
|
|
2729
|
+
fail4(err);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
async function importAction(path, opts) {
|
|
2733
|
+
try {
|
|
2734
|
+
const store = resolveStore();
|
|
2735
|
+
const ctx = await buildNotesContext(store);
|
|
2736
|
+
const config = createConfig();
|
|
2737
|
+
const workspace = await resolveActiveWorkspace(ctx.authed, opts.workspace, config.get("workspace"));
|
|
2738
|
+
const files = collectMarkdownFiles(path, Boolean(opts.recursive));
|
|
2739
|
+
if (files.length === 0) {
|
|
2740
|
+
console.error("No .md files found.");
|
|
2741
|
+
process.exit(EXIT.error);
|
|
2742
|
+
}
|
|
2743
|
+
const folders = await listFolders(ctx.authed, workspace.id);
|
|
2744
|
+
let dest;
|
|
2745
|
+
if (opts.folder) {
|
|
2746
|
+
const f = resolveFolder(folders, opts.folder);
|
|
2747
|
+
dest = { id: f.id, name: f.name };
|
|
2748
|
+
} else {
|
|
2749
|
+
const target = resolveNewNoteFolderId(workspace.defaultFolderId ?? null, folders);
|
|
2750
|
+
if (!target) {
|
|
2751
|
+
throw new ApiError(
|
|
2752
|
+
"This workspace has no folders. Create one first (e.g. `notate folders create Notes`), or pass --folder.",
|
|
2753
|
+
2,
|
|
2754
|
+
"no_root"
|
|
2755
|
+
);
|
|
2756
|
+
}
|
|
2757
|
+
dest = { id: target, name: folders.find((x) => x.id === target)?.name ?? "" };
|
|
2758
|
+
}
|
|
2759
|
+
const existing = new Set(
|
|
2760
|
+
(await listNotes(ctx, { workspaceId: workspace.id, pageSize: EXPORT_BATCH_CAP })).notes.map(
|
|
2761
|
+
(n) => n.title
|
|
2762
|
+
)
|
|
2763
|
+
);
|
|
2764
|
+
const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()).filter(Boolean) : void 0;
|
|
2765
|
+
let imported = 0;
|
|
2766
|
+
let skipped = 0;
|
|
2767
|
+
for (const file of files) {
|
|
2768
|
+
const content = readFileSync4(file, "utf-8");
|
|
2769
|
+
const title = deriveTitle(content, file);
|
|
2770
|
+
if (existing.has(title)) {
|
|
2771
|
+
skipped++;
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2774
|
+
if (!opts.dryRun) {
|
|
2775
|
+
await createNote(ctx, {
|
|
2776
|
+
title,
|
|
2777
|
+
content,
|
|
2778
|
+
folderId: dest.id,
|
|
2779
|
+
folder: dest.name,
|
|
2780
|
+
tags,
|
|
2781
|
+
workspaceId: workspace.id
|
|
2782
|
+
});
|
|
2783
|
+
existing.add(title);
|
|
2784
|
+
}
|
|
2785
|
+
imported++;
|
|
2786
|
+
}
|
|
2787
|
+
const verb = opts.dryRun ? "Would import" : "Imported";
|
|
2788
|
+
const tail = skipped ? ` (${skipped} skipped \u2014 already exist)` : "";
|
|
2789
|
+
console.log(`${verb} ${imported} note(s)${tail}.`);
|
|
2790
|
+
} catch (err) {
|
|
2791
|
+
fail4(err);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
function registerImportExportCommands(notes) {
|
|
2795
|
+
notes.command("export [title]").description("Export a note (markdown/JSON), or a folder/all to a directory").option("-o, --out <path>", "output file (single) or directory (batch)").option("--format <fmt>", "md | json (single-note only, default md)").option("--folder <name>", "export all notes in a folder").option("--all", "export every note in the workspace").option("--id <id>", "address a single note by id").option("--workspace <name|id>", "workspace scope").option("--quiet", "suppress status output").action(exportAction);
|
|
2796
|
+
notes.command("import <path>").description("Import a markdown file or a directory of .md files").option("--folder <name>", "place imported notes in a folder").option("--tags <csv>", "add tags to imported notes").option("--recursive", "include subdirectories").option("--dry-run", "preview without creating notes").option("--workspace <name|id>", "workspace scope").action(importAction);
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
// src/commands/config.ts
|
|
2800
|
+
function setAction(key, value) {
|
|
2801
|
+
if (!isConfigKey(key)) {
|
|
2802
|
+
console.error(`Unknown config key "${key}". Valid keys: ${CONFIG_KEYS.join(", ")}.`);
|
|
2803
|
+
process.exit(EXIT.usage);
|
|
2804
|
+
}
|
|
2805
|
+
let coerced;
|
|
2806
|
+
try {
|
|
2807
|
+
coerced = coerceConfigValue(key, value);
|
|
2808
|
+
} catch (err) {
|
|
2809
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
2810
|
+
process.exit(EXIT.usage);
|
|
2811
|
+
}
|
|
2812
|
+
const config = createConfig();
|
|
2813
|
+
config.set(key, coerced);
|
|
2814
|
+
console.log(`${key} = ${coerced === null ? "(none)" : coerced}`);
|
|
2815
|
+
}
|
|
2816
|
+
function getAction2(key) {
|
|
2817
|
+
if (!isConfigKey(key)) {
|
|
2818
|
+
console.error(`Unknown config key "${key}". Valid keys: ${CONFIG_KEYS.join(", ")}.`);
|
|
2819
|
+
process.exit(EXIT.usage);
|
|
2820
|
+
}
|
|
2821
|
+
const value = createConfig().get(key);
|
|
2822
|
+
console.log(value === null || value === void 0 ? "(none)" : String(value));
|
|
2823
|
+
}
|
|
2824
|
+
function listAction5(opts) {
|
|
2825
|
+
const config = createConfig();
|
|
2826
|
+
const store = Object.fromEntries(
|
|
2827
|
+
CONFIG_KEYS.map((k) => [k, config.get(k)])
|
|
2828
|
+
);
|
|
2829
|
+
if (opts.json) {
|
|
2830
|
+
console.log(JSON.stringify(store, null, 2));
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
for (const k of CONFIG_KEYS) {
|
|
2834
|
+
const v = store[k];
|
|
2835
|
+
console.log(`${k} = ${v === null || v === void 0 ? "(none)" : String(v)}`);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
function resetAction() {
|
|
2839
|
+
createConfig().clear();
|
|
2840
|
+
console.log("Config reset to defaults.");
|
|
2841
|
+
}
|
|
2842
|
+
function registerConfigCommands(program2) {
|
|
2843
|
+
const config = program2.command("config").description("Manage CLI configuration");
|
|
2844
|
+
config.command("set <key> <value>").description(`Set a value (keys: ${CONFIG_KEYS.join(", ")})`).action(setAction);
|
|
2845
|
+
config.command("get <key>").description("Print a value").action(getAction2);
|
|
2846
|
+
config.command("list").description("Show all config").option("--json", "output JSON").action(listAction5);
|
|
2847
|
+
config.command("reset").description("Reset config to defaults").action(resetAction);
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// src/lib/completion.ts
|
|
2851
|
+
function extractCommandTree(program2) {
|
|
2852
|
+
const walk = (cmd) => ({
|
|
2853
|
+
name: cmd.name(),
|
|
2854
|
+
subcommands: cmd.commands.map(walk)
|
|
2855
|
+
});
|
|
2856
|
+
return walk(program2);
|
|
2857
|
+
}
|
|
2858
|
+
function summarize(tree) {
|
|
2859
|
+
const top = tree.subcommands.map((c) => c.name).filter((n) => n !== "help");
|
|
2860
|
+
const groups = tree.subcommands.filter((c) => c.subcommands.length > 0).map((c) => ({
|
|
2861
|
+
name: c.name,
|
|
2862
|
+
subs: c.subcommands.map((s) => s.name).filter((n) => n !== "help")
|
|
2863
|
+
}));
|
|
2864
|
+
return { top, groups };
|
|
2865
|
+
}
|
|
2866
|
+
function generateCompletion(shell, tree) {
|
|
2867
|
+
const { top, groups } = summarize(tree);
|
|
2868
|
+
switch (shell) {
|
|
2869
|
+
case "bash":
|
|
2870
|
+
return generateBash(top, groups);
|
|
2871
|
+
case "zsh":
|
|
2872
|
+
return generateZsh(top, groups);
|
|
2873
|
+
case "fish":
|
|
2874
|
+
return generateFish(top, groups);
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
function generateBash(top, groups) {
|
|
2878
|
+
const cases = groups.map((g) => ` ${g.name}) COMPREPLY=( $(compgen -W "${g.subs.join(" ")}" -- "$cur") );;`).join("\n");
|
|
2879
|
+
return `# notate bash completion
|
|
2880
|
+
_notate() {
|
|
2881
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
2882
|
+
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
2883
|
+
COMPREPLY=( $(compgen -W "${top.join(" ")}" -- "$cur") )
|
|
2884
|
+
return
|
|
2885
|
+
fi
|
|
2886
|
+
case "\${COMP_WORDS[1]}" in
|
|
2887
|
+
${cases}
|
|
2888
|
+
*) ;;
|
|
2889
|
+
esac
|
|
2890
|
+
}
|
|
2891
|
+
complete -F _notate notate
|
|
2892
|
+
`;
|
|
2893
|
+
}
|
|
2894
|
+
function generateZsh(top, groups) {
|
|
2895
|
+
const cases = groups.map((g) => ` ${g.name}) compadd ${g.subs.join(" ")} ;;`).join("\n");
|
|
2896
|
+
return `#compdef notate
|
|
2897
|
+
_notate() {
|
|
2898
|
+
if (( CURRENT == 2 )); then
|
|
2899
|
+
compadd ${top.join(" ")}
|
|
2900
|
+
return
|
|
2901
|
+
fi
|
|
2902
|
+
case "\${words[2]}" in
|
|
2903
|
+
${cases}
|
|
2904
|
+
esac
|
|
2905
|
+
}
|
|
2906
|
+
compdef _notate notate
|
|
2907
|
+
`;
|
|
2908
|
+
}
|
|
2909
|
+
function generateFish(top, groups) {
|
|
2910
|
+
const lines = [
|
|
2911
|
+
"# notate fish completion",
|
|
2912
|
+
"complete -c notate -f",
|
|
2913
|
+
`complete -c notate -n __fish_use_subcommand -a "${top.join(" ")}"`
|
|
2914
|
+
];
|
|
2915
|
+
for (const g of groups) {
|
|
2916
|
+
lines.push(
|
|
2917
|
+
`complete -c notate -n "__fish_seen_subcommand_from ${g.name}" -a "${g.subs.join(" ")}"`
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
return `${lines.join("\n")}
|
|
2921
|
+
`;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
// src/commands/completion.ts
|
|
2925
|
+
var SHELLS = ["bash", "zsh", "fish"];
|
|
2926
|
+
function registerCompletionCommand(program2) {
|
|
2927
|
+
program2.command("completion <shell>").description("Print a shell completion script (bash, zsh, or fish)").action((shell) => {
|
|
2928
|
+
if (!SHELLS.includes(shell)) {
|
|
2929
|
+
console.error(`Unsupported shell "${shell}". Use: ${SHELLS.join(", ")}.`);
|
|
2930
|
+
process.exit(EXIT.usage);
|
|
2931
|
+
}
|
|
2932
|
+
console.log(generateCompletion(shell, extractCommandTree(program2)));
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// src/cli.ts
|
|
2937
|
+
var require3 = createRequire2(import.meta.url);
|
|
2938
|
+
var pkg = require3("../package.json");
|
|
2939
|
+
var program = new Command();
|
|
2940
|
+
program.name("notate").description(
|
|
2941
|
+
pkg.description ?? "Notate.md CLI - Empower your markdown"
|
|
2942
|
+
).version(pkg.version, "-v, --version", "output the version number");
|
|
2943
|
+
registerAuthCommands(program);
|
|
2944
|
+
registerImportExportCommands(registerNotesCommands(program));
|
|
2945
|
+
registerFoldersCommands(program);
|
|
2946
|
+
registerTagsCommands(program);
|
|
2947
|
+
registerWorkspacesCommands(program);
|
|
2948
|
+
registerStatsCommand(program);
|
|
2949
|
+
registerAiCommands(program);
|
|
2950
|
+
registerTranscribeCommand(program);
|
|
2951
|
+
registerConfigCommands(program);
|
|
2952
|
+
registerCompletionCommand(program);
|
|
2953
|
+
program.parseAsync().catch((err) => {
|
|
2954
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
2955
|
+
process.exit(1);
|
|
2956
|
+
});
|