lattris 0.1.2 → 0.1.3
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/dist/build.json +1 -4
- package/dist/main.js +277 -75
- package/package.json +1 -1
package/dist/build.json
CHANGED
package/dist/main.js
CHANGED
|
@@ -1019,6 +1019,18 @@ export const validAccessToken = async (cfg) => {
|
|
|
1019
1019
|
throw new Error('not-logged-in');
|
|
1020
1020
|
}
|
|
1021
1021
|
};
|
|
1022
|
+
// An absent or unrefreshable session sends no Authorization header, since the
|
|
1023
|
+
// API refuses any present invalid bearer by its own rule.
|
|
1024
|
+
export const optionalAccessToken = async (cfg) => {
|
|
1025
|
+
try {
|
|
1026
|
+
return await validAccessToken(cfg);
|
|
1027
|
+
}
|
|
1028
|
+
catch (error) {
|
|
1029
|
+
if (error instanceof Error && error.message === 'not-logged-in')
|
|
1030
|
+
return undefined;
|
|
1031
|
+
throw error;
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1022
1034
|
// Render a door to STDERR (stdout stays clean for piped data):
|
|
1023
1035
|
// <message>
|
|
1024
1036
|
// Run: lattris <next.action>
|
|
@@ -1174,10 +1186,13 @@ export const edgeRefusal = (status, headers, bytes) => {
|
|
|
1174
1186
|
// ALREADY rendered the door and the caller just propagates the exit code. 204 (the
|
|
1175
1187
|
// delete route's success) has no body to parse.
|
|
1176
1188
|
const HTTP_NO_CONTENT = 204;
|
|
1177
|
-
export const apiFetch = async (accessToken, url, init = {}) => {
|
|
1189
|
+
export const apiFetch = async (accessToken, url, init = {}, addressKind = 'api') => {
|
|
1190
|
+
const headers = { ...init.headers };
|
|
1191
|
+
if (accessToken !== undefined)
|
|
1192
|
+
headers.Authorization = `Bearer ${accessToken}`;
|
|
1178
1193
|
const res = await fetch(url, {
|
|
1179
1194
|
...init,
|
|
1180
|
-
headers
|
|
1195
|
+
headers
|
|
1181
1196
|
});
|
|
1182
1197
|
if (!res.ok) {
|
|
1183
1198
|
// Bytes, not text: the edge excerpt is cut by byte before decoding, and
|
|
@@ -1203,11 +1218,8 @@ export const apiFetch = async (accessToken, url, init = {}) => {
|
|
|
1203
1218
|
data = JSON.parse(rawText);
|
|
1204
1219
|
}
|
|
1205
1220
|
catch {
|
|
1206
|
-
// A
|
|
1207
|
-
|
|
1208
|
-
// wrong; without this branch it surfaces as "Unexpected token '<'"
|
|
1209
|
-
// AFTER a successful login.
|
|
1210
|
-
process.stderr.write('The server answered with a web page, not the API — check the environment\u2019s `api` URL.\n');
|
|
1221
|
+
// A gateway fallthrough can be 200 HTML; it must not pass as a JSON read.
|
|
1222
|
+
process.stderr.write(`The server answered with a web page, not the API — check the environment\u2019s \`${addressKind}\` URL.\n`);
|
|
1211
1223
|
return { ok: false, body: {}, exit: EXIT.GENERIC };
|
|
1212
1224
|
}
|
|
1213
1225
|
return { ok: true, data, rawText };
|
|
@@ -1756,13 +1768,13 @@ export const buildCreateNoteBody = (content, flags, allowBannedWords = false) =>
|
|
|
1756
1768
|
// receipt. Not cosmetic: capturedAt is uneditable (PATCH carries only title/content),
|
|
1757
1769
|
// so this is the person's ONE immediate chance to catch a wrong-as-recorded
|
|
1758
1770
|
// timestamp — the only remedy past this point is delete + recapture.
|
|
1759
|
-
export const formatSaved = (b) => {
|
|
1771
|
+
export const formatSaved = (b, address) => {
|
|
1760
1772
|
const lines = [`Saved memory ${b.id} (type: ${b.type})`];
|
|
1761
1773
|
if (b.intent)
|
|
1762
1774
|
lines.push(` intent ${b.intent}`);
|
|
1763
1775
|
// title is echoed back from the caller's own --title — sanitized like every
|
|
1764
|
-
// other rendered title
|
|
1765
|
-
//
|
|
1776
|
+
// other rendered title. The machine receipt is JSON; this formatter is only
|
|
1777
|
+
// the terminal channel.
|
|
1766
1778
|
if (b.title)
|
|
1767
1779
|
lines.push(` title ${sanitizeForTerminal(b.title)}`);
|
|
1768
1780
|
if (b.aiSummary)
|
|
@@ -1772,6 +1784,8 @@ export const formatSaved = (b) => {
|
|
|
1772
1784
|
lines.push(` fingerprint ${b.fingerprint}`);
|
|
1773
1785
|
if (b.revisionNo)
|
|
1774
1786
|
lines.push(` revisionNo ${b.revisionNo}`);
|
|
1787
|
+
if (address)
|
|
1788
|
+
lines.push(` address ${address}`);
|
|
1775
1789
|
return lines.join('\n');
|
|
1776
1790
|
};
|
|
1777
1791
|
// Read all of stdin. Strips a SINGLE trailing newline — the ubiquitous echo/heredoc
|
|
@@ -2317,14 +2331,22 @@ const captureMemory = async (cfg, args) => {
|
|
|
2317
2331
|
process.exitCode = result.exit;
|
|
2318
2332
|
return;
|
|
2319
2333
|
}
|
|
2320
|
-
|
|
2334
|
+
const record = result.data;
|
|
2335
|
+
const address = await composeAddressFor(cfg, accessToken, record);
|
|
2336
|
+
const { href, ...fields } = record;
|
|
2337
|
+
const receipt = {
|
|
2338
|
+
...fields,
|
|
2339
|
+
...(href === undefined ? {} : { href }),
|
|
2340
|
+
...(address === undefined ? {} : { address })
|
|
2341
|
+
};
|
|
2342
|
+
emit(receipt, JSON.stringify(receipt), () => formatSaved(record, address));
|
|
2321
2343
|
// Capture-and-link: a CLI-side composition of the two doors the wire
|
|
2322
2344
|
// actually has (capture, then edge) — never a pretended atomicity.
|
|
2323
2345
|
if (flags.linkedFrom !== undefined || flags.linksTo !== undefined) {
|
|
2324
2346
|
// --linked-from: the existing node points AT the new one; --links-to:
|
|
2325
2347
|
// the new node points outward.
|
|
2326
|
-
const fromRef = flags.linkedFrom ??
|
|
2327
|
-
const toId = flags.linkedFrom !== undefined ?
|
|
2348
|
+
const fromRef = flags.linkedFrom ?? record.id;
|
|
2349
|
+
const toId = flags.linkedFrom !== undefined ? record.id : flags.linksTo;
|
|
2328
2350
|
const edge = await apiFetch(accessToken, `${cfg.apiUrl}/memories/${encodeURIComponent(fromRef)}/edges`, {
|
|
2329
2351
|
method: 'POST',
|
|
2330
2352
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -2380,10 +2402,30 @@ export const composeAddress = (webUrl, personHandle, block) => {
|
|
|
2380
2402
|
const ref = slug ? `${slug}-${block.handle}` : block.handle;
|
|
2381
2403
|
return `${webUrl}/by/${personHandle}/memories/${ref}`;
|
|
2382
2404
|
};
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
const
|
|
2405
|
+
export const parseWebsiteMemoryAddress = (webUrl, value) => {
|
|
2406
|
+
try {
|
|
2407
|
+
const base = new URL(webUrl);
|
|
2408
|
+
const address = new URL(value);
|
|
2409
|
+
if (address.origin !== base.origin || address.username || address.password)
|
|
2410
|
+
return undefined;
|
|
2411
|
+
const basePath = stripTrailingSlash(base.pathname);
|
|
2412
|
+
const route = address.pathname.slice(basePath.length);
|
|
2413
|
+
if (!address.pathname.startsWith(`${basePath}/by/`))
|
|
2414
|
+
return undefined;
|
|
2415
|
+
const parts = route.split('/');
|
|
2416
|
+
if (parts.length !== 5 || parts[1] !== 'by' || parts[3] !== 'memories')
|
|
2417
|
+
return undefined;
|
|
2418
|
+
const ownerHandle = decodeURIComponent(parts[2]);
|
|
2419
|
+
const ref = decodeURIComponent(parts[4]);
|
|
2420
|
+
if (!ownerHandle || !ref || ownerHandle.includes('/') || ref.includes('/'))
|
|
2421
|
+
return undefined;
|
|
2422
|
+
return { ownerHandle, ref };
|
|
2423
|
+
}
|
|
2424
|
+
catch {
|
|
2425
|
+
return undefined;
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
const ownHandleFor = async (cfg, accessToken) => {
|
|
2387
2429
|
try {
|
|
2388
2430
|
const res = await fetch(`${cfg.apiUrl}/person/me`, {
|
|
2389
2431
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
@@ -2391,12 +2433,17 @@ const composeAddressFor = async (cfg, accessToken, block) => {
|
|
|
2391
2433
|
if (!res.ok)
|
|
2392
2434
|
return undefined;
|
|
2393
2435
|
const { handle } = (await res.json());
|
|
2394
|
-
return handle
|
|
2436
|
+
return typeof handle === 'string' && handle.length > 0 ? handle : undefined;
|
|
2395
2437
|
}
|
|
2396
2438
|
catch {
|
|
2397
2439
|
return undefined;
|
|
2398
2440
|
}
|
|
2399
2441
|
};
|
|
2442
|
+
// A missing convenience address must not fail the record already in hand.
|
|
2443
|
+
const composeAddressFor = async (cfg, accessToken, block) => {
|
|
2444
|
+
const handle = await ownHandleFor(cfg, accessToken);
|
|
2445
|
+
return handle ? composeAddress(cfg.webUrl, handle, block) : undefined;
|
|
2446
|
+
};
|
|
2400
2447
|
// title/content are the caller's own free text — sanitized before a TTY ever
|
|
2401
2448
|
// renders them (piped JSON stays wire-verbatim; that channel is machine data by
|
|
2402
2449
|
// contract, sanitization is a rendering concern, not a storage one). id/type/
|
|
@@ -2524,8 +2571,8 @@ export const formatNodeWalk = (w, address) => {
|
|
|
2524
2571
|
// no other lever restores the bare pre-walk read, byte-identical on the
|
|
2525
2572
|
// wire. Lever values ride through verbatim — the API owns validation (depth
|
|
2526
2573
|
// range, depth-requires-links); the client makes no decisions.
|
|
2527
|
-
export const buildGetMemoryUrl = (
|
|
2528
|
-
const url = new URL(`${
|
|
2574
|
+
export const buildGetMemoryUrl = (webUrl, ownerHandle, ref, flags) => {
|
|
2575
|
+
const url = new URL(`${webUrl}/by/${encodeURIComponent(ownerHandle)}/memories/${encodeURIComponent(ref)}`);
|
|
2529
2576
|
const links = flags.links ?? '1';
|
|
2530
2577
|
const bare = links === '0' && flags.depth === undefined && flags.content === undefined;
|
|
2531
2578
|
if (!bare) {
|
|
@@ -2537,51 +2584,63 @@ export const buildGetMemoryUrl = (apiUrl, idOrPrefix, flags) => {
|
|
|
2537
2584
|
}
|
|
2538
2585
|
return url;
|
|
2539
2586
|
};
|
|
2540
|
-
const getMemory = async (cfg,
|
|
2587
|
+
const getMemory = async (cfg, idOrAddress, flags, by) => {
|
|
2588
|
+
const isAddress = /^https?:\/\//i.test(idOrAddress);
|
|
2589
|
+
const address = isAddress ? parseWebsiteMemoryAddress(cfg.webUrl, idOrAddress) : undefined;
|
|
2590
|
+
if (isAddress && !address) {
|
|
2591
|
+
process.stderr.write(`The memory address must be on ${cfg.webUrl}/by/<owner>/memories/<ref>.\n`);
|
|
2592
|
+
process.exitCode = EXIT.USAGE;
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
if (address && by && address.ownerHandle !== by) {
|
|
2596
|
+
process.stderr.write('--by must name the same owner as the memory address.\n');
|
|
2597
|
+
process.exitCode = EXIT.USAGE;
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
const storedSession = loadSession();
|
|
2601
|
+
let ownerHandle = address?.ownerHandle ?? by ?? storedSession?.handle;
|
|
2541
2602
|
let accessToken;
|
|
2542
|
-
|
|
2543
|
-
accessToken = await
|
|
2603
|
+
if (!ownerHandle && storedSession) {
|
|
2604
|
+
accessToken = await optionalAccessToken(cfg);
|
|
2605
|
+
if (accessToken) {
|
|
2606
|
+
ownerHandle = await ownHandleFor(cfg, accessToken);
|
|
2607
|
+
if (ownerHandle) {
|
|
2608
|
+
const currentSession = loadSession();
|
|
2609
|
+
if (currentSession && currentSession.handle !== ownerHandle) {
|
|
2610
|
+
saveSession({ ...currentSession, handle: ownerHandle });
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2544
2614
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
process.exitCode = EXIT.
|
|
2615
|
+
if (!ownerHandle) {
|
|
2616
|
+
process.stderr.write('A memory id needs its wall: use get memory <website-address> or get memory <id> --by <owner-handle>.\n');
|
|
2617
|
+
process.exitCode = EXIT.USAGE;
|
|
2548
2618
|
return;
|
|
2549
2619
|
}
|
|
2550
|
-
|
|
2620
|
+
accessToken ??= await optionalAccessToken(cfg);
|
|
2621
|
+
const url = buildGetMemoryUrl(cfg.webUrl, ownerHandle, address?.ref ?? idOrAddress, flags);
|
|
2551
2622
|
const enveloped = url.search !== '';
|
|
2552
|
-
const result = await apiFetch(accessToken, url
|
|
2623
|
+
const result = await apiFetch(accessToken, url, {
|
|
2624
|
+
headers: { Accept: 'application/json' }
|
|
2625
|
+
}, 'web');
|
|
2553
2626
|
if (!result.ok) {
|
|
2554
2627
|
renderAmbiguousIfPresent(result.body);
|
|
2628
|
+
if (result.body.code === 'memory:not-found' && !address && !by) {
|
|
2629
|
+
process.stderr.write('For another wall, use get memory <website-address> or get memory <id> --by <owner-handle>.\n');
|
|
2630
|
+
}
|
|
2555
2631
|
process.exitCode = result.exit;
|
|
2556
2632
|
return;
|
|
2557
2633
|
}
|
|
2558
|
-
// The address needs the caller's OWN handle, which this response does not
|
|
2559
|
-
// carry — so it costs a second request. Paid ONLY at a TTY: piped output is
|
|
2560
|
-
// the wire verbatim and gains nothing from an address, so a script never
|
|
2561
|
-
// pays for a line it will not receive. Fetched rather than cached: caching
|
|
2562
|
-
// would be safe (a handle is assigned once and never reassigned, so it
|
|
2563
|
-
// cannot go stale), but it would mean writing state from a read path and
|
|
2564
|
-
// carrying a miss-path for sessions that predate it — for one extra request
|
|
2565
|
-
// on an interactive command that is already making one. Cache later if it
|
|
2566
|
-
// ever matters; the immutability that makes it safe is not going anywhere.
|
|
2567
|
-
//
|
|
2568
|
-
// Deliberately NOT through apiFetch: that renders a door on failure and hands
|
|
2569
|
-
// back an exit code, which is right for a call the caller asked for and wrong
|
|
2570
|
-
// for this one. The record is already in hand; a convenience that cannot be
|
|
2571
|
-
// built should vanish quietly, not print "Session invalid or expired" over a
|
|
2572
|
-
// memory that printed perfectly well.
|
|
2573
|
-
// Bare or enveloped, the block whose address we'd compose is the same
|
|
2574
|
-
// record — the envelope's anchor carries handle+title either way.
|
|
2575
2634
|
const record = enveloped ? result.data.node : result.data;
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
}
|
|
2635
|
+
const displayAddress = process.stdout.isTTY
|
|
2636
|
+
? composeAddress(cfg.webUrl, ownerHandle, record)
|
|
2637
|
+
: undefined;
|
|
2580
2638
|
emit(result.data, result.rawText, () => enveloped
|
|
2581
|
-
? formatNodeWalk(result.data,
|
|
2582
|
-
: formatMemoryDetail(result.data,
|
|
2639
|
+
? formatNodeWalk(result.data, displayAddress)
|
|
2640
|
+
: formatMemoryDetail(result.data, displayAddress));
|
|
2583
2641
|
};
|
|
2584
2642
|
const GET_FLAG_NAMES = {
|
|
2643
|
+
'--by': 'by',
|
|
2585
2644
|
'--revision': 'revision',
|
|
2586
2645
|
'--type': 'type',
|
|
2587
2646
|
'--intent': 'intent',
|
|
@@ -2661,23 +2720,51 @@ const getMemories = async (cfg, flags) => {
|
|
|
2661
2720
|
process.stderr.write(`More: ${renderRunHint(argv)}\n`);
|
|
2662
2721
|
}
|
|
2663
2722
|
};
|
|
2723
|
+
const getWallMemories = async (cfg, ownerHandle, flags) => {
|
|
2724
|
+
const accessToken = await optionalAccessToken(cfg);
|
|
2725
|
+
const url = new URL(`${cfg.webUrl}/by/${encodeURIComponent(ownerHandle)}/memories`);
|
|
2726
|
+
if (flags.limit !== undefined)
|
|
2727
|
+
url.searchParams.set('limit', flags.limit);
|
|
2728
|
+
if (flags.cursor !== undefined)
|
|
2729
|
+
url.searchParams.set('cursor', flags.cursor);
|
|
2730
|
+
if (flags.content !== undefined)
|
|
2731
|
+
url.searchParams.set('content', flags.content);
|
|
2732
|
+
const result = await apiFetch(accessToken, url, {
|
|
2733
|
+
headers: { Accept: 'application/json' }
|
|
2734
|
+
}, 'web');
|
|
2735
|
+
if (!result.ok) {
|
|
2736
|
+
process.exitCode = result.exit;
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
emit(result.data, result.rawText, formatMemoryPage);
|
|
2740
|
+
if (result.data.nextCursor) {
|
|
2741
|
+
const argv = ['get', 'memories', '--by', ownerHandle];
|
|
2742
|
+
if (flags.limit !== undefined)
|
|
2743
|
+
argv.push('--limit', flags.limit);
|
|
2744
|
+
if (flags.content !== undefined)
|
|
2745
|
+
argv.push('--content', flags.content);
|
|
2746
|
+
argv.push('--cursor', result.data.nextCursor);
|
|
2747
|
+
process.stderr.write(`More: ${renderRunHint(argv)}\n`);
|
|
2748
|
+
}
|
|
2749
|
+
};
|
|
2664
2750
|
// Printed by bare `lattris get` and `lattris get --help` (spec: everything the
|
|
2665
2751
|
// tool accepts is discoverable from the tool). Same discovery-by-depth pattern
|
|
2666
2752
|
// capture uses, one level: get has no separate "form" to go deeper into, so the
|
|
2667
2753
|
// nouns, the positional, and the flags all live at this one level.
|
|
2668
2754
|
export const GET_HELP = [
|
|
2669
|
-
'get — read your memories
|
|
2755
|
+
'get — read your memories or a public wall.',
|
|
2670
2756
|
'',
|
|
2671
2757
|
'Nouns:',
|
|
2672
|
-
' memories list,
|
|
2673
|
-
' memory one, by
|
|
2758
|
+
' memories your list, or one wall with --by <owner-handle>',
|
|
2759
|
+
' memory one, by website address or id with --by <owner-handle> \u2014 its edges ride along',
|
|
2674
2760
|
' history one note\u2019s history: every change, newest first, and the head fingerprint',
|
|
2675
2761
|
' deleted your deleted items, with the fingerprint restore and erase need',
|
|
2676
2762
|
' questions the questions left on your tree, open first',
|
|
2677
2763
|
'',
|
|
2678
2764
|
'Usage:',
|
|
2679
2765
|
' lattris get memories [--shared-with-me] [--q <words>] [--type <name>] [--intent <name>] [--with <audience>] [--limit <n>] [--cursor <token>] [--content 0|1]',
|
|
2680
|
-
' lattris get
|
|
2766
|
+
' lattris get memories --by <owner-handle> [--limit <n>] [--cursor <token>] [--content 0|1]',
|
|
2767
|
+
' lattris get memory <website-address|id> [--by <owner-handle>] [--links 0|1] [--depth <n>] [--content 0|1]',
|
|
2681
2768
|
' lattris get memory <id-or-prefix> --revision <n> one revision, as history kept it',
|
|
2682
2769
|
' lattris get history <id-or-prefix> [--limit <n>] [--before <operation-id>]',
|
|
2683
2770
|
' lattris get deleted [--limit <n>] [--cursor <token>]',
|
|
@@ -2694,6 +2781,8 @@ export const GET_HELP = [
|
|
|
2694
2781
|
' (private — no grant at all)',
|
|
2695
2782
|
' --limit <n> page size, 1-100 (default 50)',
|
|
2696
2783
|
' --cursor <token> the nextCursor from a previous page',
|
|
2784
|
+
' --by <handle> one wall on the website address; an id without a',
|
|
2785
|
+
' session needs it, while a full address carries it',
|
|
2697
2786
|
' --shared-with-me list memories other people granted to you; each row',
|
|
2698
2787
|
' names its owner and whether it is on your graph',
|
|
2699
2788
|
'',
|
|
@@ -2716,11 +2805,11 @@ export const GET_HELP = [
|
|
|
2716
2805
|
'',
|
|
2717
2806
|
'Every read of a note prints its fingerprint (the head of its history) and',
|
|
2718
2807
|
'revisionNo; every read prints structureHash, a hash of the block\u2019s links and',
|
|
2719
|
-
'grants as
|
|
2808
|
+
'grants as this reader sees them. An edit sends the fingerprint back as --base-fingerprint.',
|
|
2720
2809
|
'',
|
|
2721
|
-
"A memory's web address is <site>/by/<
|
|
2722
|
-
'
|
|
2723
|
-
'
|
|
2810
|
+
"A memory's web address is <site>/by/<owner-handle>/memories/<slug>-<memory-handle>.",
|
|
2811
|
+
'A /by read asks for JSON with Accept: application/json; it sends a bearer',
|
|
2812
|
+
'only when the session has a live one. `get memory` prints the address at a TTY.',
|
|
2724
2813
|
'',
|
|
2725
2814
|
'Aliases: read, show, view, fetch → get'
|
|
2726
2815
|
].join('\n');
|
|
@@ -2747,21 +2836,113 @@ const GET_NOUNS = new Set([
|
|
|
2747
2836
|
// when the build wrote one, and is ABSENT rather than guessed when it did not —
|
|
2748
2837
|
// `tsc` alone does not write that stamp, the install path does, so a package
|
|
2749
2838
|
// built by CI carries it only if CI writes it.
|
|
2839
|
+
const runtimeJson = (path) => {
|
|
2840
|
+
try {
|
|
2841
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
2842
|
+
}
|
|
2843
|
+
catch {
|
|
2844
|
+
return undefined;
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
const currentPackageVersion = () => {
|
|
2848
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
2849
|
+
const pkg = runtimeJson(join(here, '..', 'package.json'));
|
|
2850
|
+
return typeof pkg?.version === 'string' ? pkg.version : 'unknown';
|
|
2851
|
+
};
|
|
2750
2852
|
const printVersion = () => {
|
|
2751
2853
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
2752
|
-
const
|
|
2854
|
+
const build = runtimeJson(join(here, 'build.json'));
|
|
2855
|
+
const version = currentPackageVersion();
|
|
2856
|
+
const commit = typeof build?.commit === 'string' ? build.commit.slice(0, 12) : undefined;
|
|
2857
|
+
process.stdout.write(commit ? `lattris ${version} (${commit})\n` : `lattris ${version}\n`);
|
|
2858
|
+
};
|
|
2859
|
+
const VERSION_CHECK_URL = 'https://registry.npmjs.org/lattris/latest';
|
|
2860
|
+
const VERSION_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
2861
|
+
const VERSION_CHECK_TIMEOUT_MS = 1500;
|
|
2862
|
+
const parseVersion = (version) => {
|
|
2863
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
|
|
2864
|
+
if (!match)
|
|
2865
|
+
return undefined;
|
|
2866
|
+
return {
|
|
2867
|
+
core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])],
|
|
2868
|
+
prerelease: match[4] === undefined ? null : match[4].split('.')
|
|
2869
|
+
};
|
|
2870
|
+
};
|
|
2871
|
+
// SemVer precedence without adding a runtime dependency to the zero-dependency
|
|
2872
|
+
// CLI. Returns negative when left is older, positive when it is newer.
|
|
2873
|
+
const compareVersions = (left, right) => {
|
|
2874
|
+
const a = parseVersion(left);
|
|
2875
|
+
const b = parseVersion(right);
|
|
2876
|
+
if (!a || !b)
|
|
2877
|
+
return undefined;
|
|
2878
|
+
for (let i = 0; i < a.core.length; i++) {
|
|
2879
|
+
if (a.core[i] < b.core[i])
|
|
2880
|
+
return -1;
|
|
2881
|
+
if (a.core[i] > b.core[i])
|
|
2882
|
+
return 1;
|
|
2883
|
+
}
|
|
2884
|
+
if (a.prerelease === null && b.prerelease === null)
|
|
2885
|
+
return 0;
|
|
2886
|
+
if (a.prerelease === null)
|
|
2887
|
+
return 1;
|
|
2888
|
+
if (b.prerelease === null)
|
|
2889
|
+
return -1;
|
|
2890
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
2891
|
+
for (let i = 0; i < length; i++) {
|
|
2892
|
+
const av = a.prerelease[i];
|
|
2893
|
+
const bv = b.prerelease[i];
|
|
2894
|
+
if (av === undefined)
|
|
2895
|
+
return -1;
|
|
2896
|
+
if (bv === undefined)
|
|
2897
|
+
return 1;
|
|
2898
|
+
if (av === bv)
|
|
2899
|
+
continue;
|
|
2900
|
+
const aNumber = /^\d+$/.test(av);
|
|
2901
|
+
const bNumber = /^\d+$/.test(bv);
|
|
2902
|
+
if (aNumber && bNumber)
|
|
2903
|
+
return BigInt(av) < BigInt(bv) ? -1 : 1;
|
|
2904
|
+
if (aNumber !== bNumber)
|
|
2905
|
+
return aNumber ? -1 : 1;
|
|
2906
|
+
return av < bv ? -1 : 1;
|
|
2907
|
+
}
|
|
2908
|
+
return 0;
|
|
2909
|
+
};
|
|
2910
|
+
// A courtesy, never a gate: every read, request, parse and write failure is
|
|
2911
|
+
// swallowed, and this function never changes the command's exit code. Startup
|
|
2912
|
+
// launches it without awaiting it, so the command begins while the registry
|
|
2913
|
+
// request is in flight; the bounded request cannot hold a quick command open
|
|
2914
|
+
// indefinitely.
|
|
2915
|
+
export const checkForNewVersion = async ({ currentVersion, stateDir, now = Date.now(), fetchImpl = fetch }) => {
|
|
2916
|
+
try {
|
|
2917
|
+
const cachePath = join(stateDir, 'version-check.json');
|
|
2918
|
+
const cache = runtimeJson(cachePath);
|
|
2919
|
+
if (typeof cache?.checkedAt === 'number' && now - cache.checkedAt <= VERSION_CHECK_TTL_MS) {
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2922
|
+
const response = await fetchImpl(VERSION_CHECK_URL, {
|
|
2923
|
+
headers: { Accept: 'application/json' },
|
|
2924
|
+
signal: AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS)
|
|
2925
|
+
});
|
|
2926
|
+
if (!response.ok)
|
|
2927
|
+
return;
|
|
2928
|
+
const body = (await response.json());
|
|
2929
|
+
if (typeof body.version !== 'string' || parseVersion(body.version) === undefined)
|
|
2930
|
+
return;
|
|
2753
2931
|
try {
|
|
2754
|
-
|
|
2932
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
2933
|
+
writeFileSync(cachePath, `${JSON.stringify({ checkedAt: now, latestVersion: body.version })}\n`, { mode: 0o600 });
|
|
2755
2934
|
}
|
|
2756
2935
|
catch {
|
|
2757
|
-
|
|
2936
|
+
// An unwritable cache means a later startup may retry; it never makes
|
|
2937
|
+
// this command fail or hides an update the registry already answered.
|
|
2758
2938
|
}
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2939
|
+
if (compareVersions(currentVersion, body.version) === -1) {
|
|
2940
|
+
process.stderr.write(`lattris ${currentVersion} → ${body.version} available: npm update -g lattris\n`);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
catch {
|
|
2944
|
+
// Offline, timed out, malformed response, unreadable cache: all silent.
|
|
2945
|
+
}
|
|
2765
2946
|
};
|
|
2766
2947
|
const HELP_TOKENS = new Set(['--help', '-h', 'help']);
|
|
2767
2948
|
export const LOGIN_HELP = [
|
|
@@ -2933,6 +3114,11 @@ const get = async (cfg, noun, rest) => {
|
|
|
2933
3114
|
return;
|
|
2934
3115
|
}
|
|
2935
3116
|
if (flags.revision !== undefined) {
|
|
3117
|
+
if (flags.by !== undefined || /^https?:\/\//i.test(id)) {
|
|
3118
|
+
process.stderr.write('A revision read uses your own id or prefix; --by and website addresses do not apply.\n');
|
|
3119
|
+
process.exitCode = EXIT.USAGE;
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
2936
3122
|
const walkFlag = ['links', 'depth', 'content'].find((k) => flags[k] !== undefined);
|
|
2937
3123
|
if (walkFlag !== undefined) {
|
|
2938
3124
|
process.stderr.write(`--${walkFlag} does not apply to a revision read.\n`);
|
|
@@ -2942,7 +3128,7 @@ const get = async (cfg, noun, rest) => {
|
|
|
2942
3128
|
await getRevision(cfg, id, flags.revision);
|
|
2943
3129
|
return;
|
|
2944
3130
|
}
|
|
2945
|
-
await getMemory(cfg, id, { links: flags.links, depth: flags.depth, content: flags.content });
|
|
3131
|
+
await getMemory(cfg, id, { links: flags.links, depth: flags.depth, content: flags.content }, flags.by);
|
|
2946
3132
|
}
|
|
2947
3133
|
else if (noun === 'memories') {
|
|
2948
3134
|
// The symmetric refusal: a walk lever on a list would be parsed clean and
|
|
@@ -2953,10 +3139,22 @@ const get = async (cfg, noun, rest) => {
|
|
|
2953
3139
|
process.exitCode = EXIT.USAGE;
|
|
2954
3140
|
return;
|
|
2955
3141
|
}
|
|
2956
|
-
|
|
3142
|
+
if (flags.by !== undefined) {
|
|
3143
|
+
const ownerOnly = ['type', 'intent', 'q', 'with', 'sharedWithMe'].find((key) => flags[key] !== undefined);
|
|
3144
|
+
if (ownerOnly !== undefined) {
|
|
3145
|
+
const flagName = ownerOnly === 'sharedWithMe' ? 'shared-with-me' : ownerOnly;
|
|
3146
|
+
process.stderr.write(`--${flagName} does not apply to a wall listing.\n`);
|
|
3147
|
+
process.exitCode = EXIT.USAGE;
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
await getWallMemories(cfg, flags.by, flags);
|
|
3151
|
+
}
|
|
3152
|
+
else {
|
|
3153
|
+
await getMemories(cfg, flags);
|
|
3154
|
+
}
|
|
2957
3155
|
}
|
|
2958
3156
|
else {
|
|
2959
|
-
process.stderr.write('
|
|
3157
|
+
process.stderr.write('A website address or id is required for a single memory. To list your own: lattris get memories\n');
|
|
2960
3158
|
process.exitCode = EXIT.USAGE;
|
|
2961
3159
|
}
|
|
2962
3160
|
};
|
|
@@ -6606,10 +6804,14 @@ export const main = async () => {
|
|
|
6606
6804
|
? VERB_ALIASES[rawCommand]
|
|
6607
6805
|
: rawCommand
|
|
6608
6806
|
: rawCommand;
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6807
|
+
void checkForNewVersion({
|
|
6808
|
+
currentVersion: currentPackageVersion(),
|
|
6809
|
+
stateDir: join(stateHome(), 'lattris')
|
|
6810
|
+
});
|
|
6811
|
+
// HELP BEFORE COMMAND WORK: a help token in the subcommand's args means
|
|
6812
|
+
// help — never a session or API call, exit 0. The best-effort registry
|
|
6813
|
+
// courtesy above is startup work rather than command work. Commands whose
|
|
6814
|
+
// own handler already treats help first return null and fall through unchanged.
|
|
6613
6815
|
const subArgs = argv.slice(3);
|
|
6614
6816
|
if (command !== undefined && subArgs.some((t) => HELP_TOKENS.has(t))) {
|
|
6615
6817
|
const help = commandHelpText(command, subArgs);
|