lattris 0.1.0 → 0.1.2
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 +21 -12
- package/dist/build.json +4 -0
- package/dist/main.js +249 -85
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -57,22 +57,32 @@ Run `lattris help <command>` for any of them, or `lattris --help` for the full l
|
|
|
57
57
|
|
|
58
58
|
## Where it keeps things
|
|
59
59
|
|
|
60
|
-
Credentials live
|
|
61
|
-
logout` removes them. Nothing is written to the directory you run
|
|
60
|
+
Credentials live at `~/lattris-machines/<account>/<env>/session.json` and are readable
|
|
61
|
+
only by you. `lattris logout` removes them. Nothing is written to the directory you run
|
|
62
|
+
it from.
|
|
62
63
|
|
|
63
|
-
##
|
|
64
|
+
## Release
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
the registry records an attestation linking the tarball to the commit on `main` it was
|
|
67
|
-
built from and the workflow run that built it. The first published version was published
|
|
68
|
-
by hand to claim the name and carries no attestation, so do not assume one — check:
|
|
66
|
+
Releasing has two steps. First, open a normal version-bump PR from a branch:
|
|
69
67
|
|
|
70
68
|
```bash
|
|
71
|
-
|
|
69
|
+
cd apps/lattris/cli
|
|
70
|
+
npm version patch --no-git-tag-version
|
|
71
|
+
git add package.json
|
|
72
|
+
git commit -m "chore(lattris-cli): bump package version"
|
|
72
73
|
```
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
After that PR merges, whoever holds npm publish rights updates a clean local `main` and runs:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cd apps/lattris/cli
|
|
79
|
+
pnpm release
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The release script fetches `origin/main` and refuses unless the current branch is `main`,
|
|
83
|
+
`HEAD` equals `origin/main`, and the whole working tree is clean. It builds the CLI, writes
|
|
84
|
+
the release commit to `dist/build.json`, publishes to npm, and pushes the annotated
|
|
85
|
+
`lattris-v<version>` tag. `lattris --version` then names that release commit.
|
|
76
86
|
|
|
77
87
|
## Source, terms and reporting
|
|
78
88
|
|
|
@@ -82,5 +92,4 @@ Nothing here grants you rights beyond installing and running the tool, and no
|
|
|
82
92
|
permission should be inferred from the absence of a licence file.
|
|
83
93
|
|
|
84
94
|
There is no public issue tracker, so this package names none — a link you cannot
|
|
85
|
-
open is worse than no link.
|
|
86
|
-
series@lattris.com.
|
|
95
|
+
open is worse than no link.
|
package/dist/build.json
ADDED
package/dist/main.js
CHANGED
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
// deputy / scoped-grant machinery here. It reuses the exact SPA-minted-Hydra-JWT
|
|
15
15
|
// path the API already validates; zero server-side auth changes.
|
|
16
16
|
//
|
|
17
|
-
// Zero runtime dependencies — Node ≥
|
|
18
|
-
// node:crypto).
|
|
19
|
-
//
|
|
20
|
-
// exported for unit tests; `main()` runs only
|
|
17
|
+
// Zero runtime dependencies — the compiled bin runs on Node ≥ 22 built-ins only
|
|
18
|
+
// (global fetch, node:http, node:crypto). Running the TypeScript source directly
|
|
19
|
+
// still uses the repo's Node 24 toolchain via `node --experimental-strip-types
|
|
20
|
+
// src/main.ts <cmd>`. Functions are exported for unit tests; `main()` runs only
|
|
21
|
+
// when this file is the entry point.
|
|
21
22
|
// Strip-only mode erases TYPES, never TS-only syntax: no constructor parameter
|
|
22
23
|
// properties, enums, or namespaces anywhere in src/ — a parameter property
|
|
23
24
|
// crashes the no-build run outright (ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX).
|
|
@@ -30,11 +31,11 @@ import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
|
30
31
|
import { chmodSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
31
32
|
import { createServer } from 'node:http';
|
|
32
33
|
import { homedir } from 'node:os';
|
|
33
|
-
import { dirname, join, resolve, sep } from 'node:path';
|
|
34
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
34
35
|
import { createInterface } from 'node:readline';
|
|
35
36
|
import { fileURLToPath } from 'node:url';
|
|
36
|
-
import { extractChain } from
|
|
37
|
-
import { ChainSaveError, driveChain, loadReceipts, saveReceipts } from
|
|
37
|
+
import { extractChain } from "./extract-claude-jsonl.js";
|
|
38
|
+
import { ChainSaveError, driveChain, loadReceipts, saveReceipts } from "./save-session-chain.js";
|
|
38
39
|
// ─── exit codes ────────────────────────────────────────────────────────────────
|
|
39
40
|
// Stable, meaningful exit codes so a script can tell failure kinds apart without
|
|
40
41
|
// parsing prose (CLI client-contract spec §3). Code 4 is the machine twin of a
|
|
@@ -48,6 +49,18 @@ export const EXIT = {
|
|
|
48
49
|
DOOR: 4 // a door must be walked first (e.g. person:uninitiated)
|
|
49
50
|
};
|
|
50
51
|
const stripTrailingSlash = (s) => s.replace(/\/+$/, '');
|
|
52
|
+
// A published package contains only the runtime files allow-listed in
|
|
53
|
+
// package.json. A worktree build also contains installed-check.js, whose whole
|
|
54
|
+
// purpose is to judge the repo-backed symlink install. That absent/present seam
|
|
55
|
+
// lets a clean npm install go to the product by default while source and worktree
|
|
56
|
+
// development keep their existing local default. Explicit --env, .lattris and
|
|
57
|
+
// LATTRIS_ENV always win over this fallback.
|
|
58
|
+
export const defaultEnvironmentForEntry = (entryUrl = import.meta.url) => {
|
|
59
|
+
const entryPath = fileURLToPath(entryUrl);
|
|
60
|
+
const entryDir = dirname(entryPath);
|
|
61
|
+
const isBuiltMain = basename(entryPath) === 'main.js' && basename(entryDir) === 'dist';
|
|
62
|
+
return isBuiltMain && !existsSync(join(entryDir, 'installed-check.js')) ? 'prod' : 'local';
|
|
63
|
+
};
|
|
51
64
|
// The environments registry — a JSON file at a well-known location. Missing
|
|
52
65
|
// file or missing key → the built-in localdev defaults below, so the CLI
|
|
53
66
|
// works out of the box with no registry (the existing behavior).
|
|
@@ -646,6 +659,7 @@ export const CLI_COMMAND_WORDS = new Set([
|
|
|
646
659
|
'invite',
|
|
647
660
|
'share',
|
|
648
661
|
'unshare',
|
|
662
|
+
'consent',
|
|
649
663
|
'restore',
|
|
650
664
|
'erase',
|
|
651
665
|
'resend',
|
|
@@ -1201,11 +1215,10 @@ export const apiFetch = async (accessToken, url, init = {}) => {
|
|
|
1201
1215
|
// ─── the client-makes-no-decisions output rule ──────────────────────────────────
|
|
1202
1216
|
// A person reading a terminal and a program reading a pipe want different things
|
|
1203
1217
|
// from the SAME response: a person wants the labelled/columnar form, a program
|
|
1204
|
-
// wants
|
|
1205
|
-
//
|
|
1206
|
-
//
|
|
1207
|
-
//
|
|
1208
|
-
// structured output can never drift from the API (deliberate).
|
|
1218
|
+
// wants the API's wire body without re-stringification. Piped output preserves
|
|
1219
|
+
// the response bytes (key order, whitespace and number formatting included) and
|
|
1220
|
+
// adds only a missing final newline, so successive shell output cannot run into
|
|
1221
|
+
// the JSON. A response that already ends in a newline is unchanged.
|
|
1209
1222
|
// renderHuman picks what a person needs to see; it never changes what a machine
|
|
1210
1223
|
// receives. An empty human rendering (e.g. no memories) writes nothing rather
|
|
1211
1224
|
// than a stray blank line.
|
|
@@ -1216,7 +1229,7 @@ export const emit = (data, rawText, renderHuman) => {
|
|
|
1216
1229
|
process.stdout.write(`${human}\n`);
|
|
1217
1230
|
}
|
|
1218
1231
|
else {
|
|
1219
|
-
process.stdout.write(rawText);
|
|
1232
|
+
process.stdout.write(rawText.endsWith('\n') ? rawText : `${rawText}\n`);
|
|
1220
1233
|
}
|
|
1221
1234
|
};
|
|
1222
1235
|
// The product word is "lattris number", never
|
|
@@ -2343,7 +2356,9 @@ export const formatAudiences = (audiences) => audiences.length === 0 ? '(private
|
|
|
2343
2356
|
// title is the caller's own free text — sanitized before a TTY renders it (the
|
|
2344
2357
|
// piped/JSON path never calls this formatter). id/capturedAt/intent are all
|
|
2345
2358
|
// API-constrained shapes, never free text.
|
|
2346
|
-
export const formatBlockLine = (b) =>
|
|
2359
|
+
export const formatBlockLine = (b) => b.ownerHandle === undefined
|
|
2360
|
+
? `${b.id} ${b.handle} ${b.capturedAt} ${b.intent ?? '-'} ${b.visibility.public ? 'public' : '-'} ${sanitizeForTerminal(b.title ?? '(untitled)')}`
|
|
2361
|
+
: `${b.id} ${b.handle} ${b.capturedAt} by ${sanitizeForTerminal(b.ownerDisplayName ?? b.ownerHandle)} (@${b.ownerHandle}) ${b.consented ? 'on graph' : 'off graph'} ${sanitizeForTerminal(b.title ?? '(untitled)')}`;
|
|
2347
2362
|
// ─── composing a memory's web address ──────────────────────────────────────────
|
|
2348
2363
|
// The client COMPOSES an address; only the server RESOLVES one, and it resolves
|
|
2349
2364
|
// on the block handle alone. That split is why carrying these rules here is
|
|
@@ -2580,7 +2595,7 @@ const GET_FLAG_NAMES = {
|
|
|
2580
2595
|
};
|
|
2581
2596
|
// Which side of get each flag belongs to — the applicability refusals in the
|
|
2582
2597
|
// dispatcher read these, so a new flag must pick a side to compile at all.
|
|
2583
|
-
const LIST_ONLY_FLAGS = ['type', 'intent', 'limit', 'cursor', 'q', 'with'];
|
|
2598
|
+
const LIST_ONLY_FLAGS = ['type', 'intent', 'limit', 'cursor', 'q', 'with', 'sharedWithMe'];
|
|
2584
2599
|
// --content is the shape lever on BOTH reads: on the one memory it drops the
|
|
2585
2600
|
// words, on the list it drops the excerpts. The rest of these are the walk's.
|
|
2586
2601
|
const WALK_ONLY_FLAGS = ['links', 'depth', 'revision'];
|
|
@@ -2614,16 +2629,15 @@ const getMemories = async (cfg, flags) => {
|
|
|
2614
2629
|
url.searchParams.set('audience', flags.with);
|
|
2615
2630
|
if (flags.content !== undefined)
|
|
2616
2631
|
url.searchParams.set('content', flags.content);
|
|
2632
|
+
if (flags.sharedWithMe)
|
|
2633
|
+
url.searchParams.set('sharedWithMe', '1');
|
|
2617
2634
|
const result = await apiFetch(accessToken, url);
|
|
2618
2635
|
if (!result.ok) {
|
|
2619
2636
|
process.exitCode = result.exit;
|
|
2620
2637
|
return;
|
|
2621
2638
|
}
|
|
2622
2639
|
emit(result.data, result.rawText, formatMemoryPage);
|
|
2623
|
-
if (result.data.
|
|
2624
|
-
process.stderr.write('No memories yet.\n');
|
|
2625
|
-
}
|
|
2626
|
-
else if (result.data.nextCursor) {
|
|
2640
|
+
if (result.data.nextCursor) {
|
|
2627
2641
|
// Reconstruct the FULL continuation — active filters + the new cursor — so
|
|
2628
2642
|
// copy-pasting the hint never silently drops back to an unfiltered page.
|
|
2629
2643
|
// ARGV elements through the one renderer, never a preformed string: a
|
|
@@ -2641,6 +2655,8 @@ const getMemories = async (cfg, flags) => {
|
|
|
2641
2655
|
argv.push('--limit', flags.limit);
|
|
2642
2656
|
if (flags.content !== undefined)
|
|
2643
2657
|
argv.push('--content', flags.content);
|
|
2658
|
+
if (flags.sharedWithMe)
|
|
2659
|
+
argv.push('--shared-with-me');
|
|
2644
2660
|
argv.push('--cursor', result.data.nextCursor);
|
|
2645
2661
|
process.stderr.write(`More: ${renderRunHint(argv)}\n`);
|
|
2646
2662
|
}
|
|
@@ -2660,7 +2676,7 @@ export const GET_HELP = [
|
|
|
2660
2676
|
' questions the questions left on your tree, open first',
|
|
2661
2677
|
'',
|
|
2662
2678
|
'Usage:',
|
|
2663
|
-
' lattris get memories [--q <words>] [--type <name>] [--intent <name>] [--with <audience>] [--limit <n>] [--cursor <token>] [--content 0|1]',
|
|
2679
|
+
' lattris get memories [--shared-with-me] [--q <words>] [--type <name>] [--intent <name>] [--with <audience>] [--limit <n>] [--cursor <token>] [--content 0|1]',
|
|
2664
2680
|
' lattris get memory <id-or-prefix> [--links 0|1] [--depth <n>] [--content 0|1]',
|
|
2665
2681
|
' lattris get memory <id-or-prefix> --revision <n> one revision, as history kept it',
|
|
2666
2682
|
' lattris get history <id-or-prefix> [--limit <n>] [--before <operation-id>]',
|
|
@@ -2678,6 +2694,8 @@ export const GET_HELP = [
|
|
|
2678
2694
|
' (private — no grant at all)',
|
|
2679
2695
|
' --limit <n> page size, 1-100 (default 50)',
|
|
2680
2696
|
' --cursor <token> the nextCursor from a previous page',
|
|
2697
|
+
' --shared-with-me list memories other people granted to you; each row',
|
|
2698
|
+
' names its owner and whether it is on your graph',
|
|
2681
2699
|
'',
|
|
2682
2700
|
' --links 0|1 edges on the one memory (default 1): direction, verb,',
|
|
2683
2701
|
' reason, and each neighbour\u2019s finding aid. --links 0',
|
|
@@ -2792,6 +2810,8 @@ export const commandHelpText = (command, args) => {
|
|
|
2792
2810
|
return ERASE_HELP;
|
|
2793
2811
|
case 'contact':
|
|
2794
2812
|
return CONTACT_HELP;
|
|
2813
|
+
case 'consent':
|
|
2814
|
+
return CONSENT_HELP;
|
|
2795
2815
|
case 'capture': {
|
|
2796
2816
|
// The form's own help when a form was named — mirroring the
|
|
2797
2817
|
// sub-dispatcher's routing (positional 'session' OR --type session);
|
|
@@ -2871,7 +2891,15 @@ const get = async (cfg, noun, rest) => {
|
|
|
2871
2891
|
await getDeleted(cfg, rest);
|
|
2872
2892
|
return;
|
|
2873
2893
|
}
|
|
2874
|
-
const
|
|
2894
|
+
const sharedWithMeCount = rest.filter((arg) => arg === '--shared-with-me').length;
|
|
2895
|
+
if (sharedWithMeCount > 1) {
|
|
2896
|
+
process.stderr.write('--shared-with-me was given more than once. Send it once.\n');
|
|
2897
|
+
process.exitCode = EXIT.USAGE;
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
const { flags, positionals, unknown } = parseArgs(GET_FLAG_NAMES, rest.filter((arg) => arg !== '--shared-with-me'));
|
|
2901
|
+
if (sharedWithMeCount === 1)
|
|
2902
|
+
flags.sharedWithMe = '1';
|
|
2875
2903
|
if (unknown) {
|
|
2876
2904
|
process.stderr.write(`Unknown flag: ${unknown}\n`);
|
|
2877
2905
|
process.exitCode = EXIT.USAGE;
|
|
@@ -2936,6 +2964,10 @@ const get = async (cfg, noun, rest) => {
|
|
|
2936
2964
|
// like a note's content. Everything else is API-constrained shape.
|
|
2937
2965
|
export const formatQuestion = (q) => {
|
|
2938
2966
|
const lines = [`id ${q.id}`, `createdAt ${q.createdAt}`];
|
|
2967
|
+
if (q.kind === 'share') {
|
|
2968
|
+
lines.push(`shared by @${sanitizeForTerminal(q.sharerHandle ?? '(unknown)')}`, `memory ${q.aboutBlockId ?? '(missing)'}`, `seen ${q.seenAt ?? '(unseen)'}`);
|
|
2969
|
+
return lines.join('\n');
|
|
2970
|
+
}
|
|
2939
2971
|
if (q.aboutBlockId)
|
|
2940
2972
|
lines.push(`about ${q.aboutBlockId}`);
|
|
2941
2973
|
lines.push(q.closedAs === null
|
|
@@ -2945,7 +2977,7 @@ export const formatQuestion = (q) => {
|
|
|
2945
2977
|
// here, but the same sanitize-before-TTY law applies to any rendered field.
|
|
2946
2978
|
if (q.closeNote)
|
|
2947
2979
|
lines.push(`note ${sanitizeForTerminal(q.closeNote, Infinity)}`);
|
|
2948
|
-
lines.push('', sanitizeForTerminal(q.body, Infinity));
|
|
2980
|
+
lines.push('', sanitizeForTerminal(q.body ?? '', Infinity));
|
|
2949
2981
|
return lines.join('\n');
|
|
2950
2982
|
};
|
|
2951
2983
|
export const formatQuestionList = (qs) => qs.map((q) => formatQuestion(q)).join('\n\n');
|
|
@@ -2965,9 +2997,6 @@ const getQuestions = async (cfg, mine) => {
|
|
|
2965
2997
|
return;
|
|
2966
2998
|
}
|
|
2967
2999
|
emit(result.data, result.rawText, formatQuestionList);
|
|
2968
|
-
if (result.data.length === 0) {
|
|
2969
|
-
process.stderr.write(mine ? 'No questions asked yet.\n' : 'No questions.\n');
|
|
2970
|
-
}
|
|
2971
3000
|
};
|
|
2972
3001
|
// ask — POST /by/:handle/questions. THE BODY IS THE GRANT (v1 doctrine): the
|
|
2973
3002
|
// prose from stdin is exactly what crosses to the tree's owner — no
|
|
@@ -4170,9 +4199,9 @@ const update = async (cfg, noun, rest) => {
|
|
|
4170
4199
|
// touches it. So a link is not admission, and the CLI says so rather than
|
|
4171
4200
|
// letting a person discover it at the door.
|
|
4172
4201
|
//
|
|
4173
|
-
// Minting is
|
|
4174
|
-
//
|
|
4175
|
-
//
|
|
4202
|
+
// Minting is open only on accounts with the server-side entitlement. The
|
|
4203
|
+
// server's refusal message rides the shared API error path unchanged. There is
|
|
4204
|
+
// no quota once open, and revoking one link does not close the door.
|
|
4176
4205
|
export const INVITE_HELP = [
|
|
4177
4206
|
'invite — bring one person in.',
|
|
4178
4207
|
'',
|
|
@@ -4185,13 +4214,14 @@ export const INVITE_HELP = [
|
|
|
4185
4214
|
'when you send it.',
|
|
4186
4215
|
'',
|
|
4187
4216
|
'Usage:',
|
|
4188
|
-
' lattris invite
|
|
4189
|
-
' lattris invite --for
|
|
4217
|
+
' lattris invite <contact-name-or-id> [--yes]',
|
|
4218
|
+
' lattris invite --for <name-or-id> [--yes] compatibility spelling',
|
|
4219
|
+
'',
|
|
4220
|
+
'The contact must be one of yours. A name is matched exactly, ignoring case.',
|
|
4221
|
+
'If that name does not exist, a person contact is created before minting.',
|
|
4222
|
+
'Duplicate names are never guessed: use the contact id shown by',
|
|
4223
|
+
'`lattris contact list`.',
|
|
4190
4224
|
'',
|
|
4191
|
-
' --for optional; YOUR label for who this is for, so you can tell your',
|
|
4192
|
-
' outstanding links apart. It is shown on the invitation page and',
|
|
4193
|
-
' to you. It is not their name and not contact information — their',
|
|
4194
|
-
' real name comes from them, at signup.',
|
|
4195
4225
|
' --yes required when not at a terminal (there is no prompt to answer',
|
|
4196
4226
|
' there); at a terminal you are always asked.',
|
|
4197
4227
|
'',
|
|
@@ -4218,6 +4248,22 @@ const formatInvite = (cfg, i) => {
|
|
|
4218
4248
|
return `revoked${label}\n minted ${i.createdAt}`;
|
|
4219
4249
|
return `live${label}\n ${inviteLink(cfg, i.token)}\n minted ${i.createdAt}`;
|
|
4220
4250
|
};
|
|
4251
|
+
// Names are labels, so exact duplicate labels must never be guessed. UUIDs are
|
|
4252
|
+
// ids, never names: an unknown UUID refuses instead of creating a strangely
|
|
4253
|
+
// named contact that only looks like an existing id.
|
|
4254
|
+
export const resolveInviteContact = (contacts, selector) => {
|
|
4255
|
+
if (isUuidKey(selector)) {
|
|
4256
|
+
const contact = contacts.find((c) => c.id.toLowerCase() === selector.toLowerCase());
|
|
4257
|
+
return contact ? { kind: 'found', contact } : { kind: 'missing-id' };
|
|
4258
|
+
}
|
|
4259
|
+
const folded = selector.toLowerCase();
|
|
4260
|
+
const matches = contacts.filter((c) => c.displayName.toLowerCase() === folded);
|
|
4261
|
+
if (matches.length === 1)
|
|
4262
|
+
return { kind: 'found', contact: matches[0] };
|
|
4263
|
+
if (matches.length > 1)
|
|
4264
|
+
return { kind: 'ambiguous', matches };
|
|
4265
|
+
return { kind: 'missing-name', displayName: selector };
|
|
4266
|
+
};
|
|
4221
4267
|
const invite = async (cfg, rest) => {
|
|
4222
4268
|
if (rest.some((t) => HELP_TOKENS.has(t))) {
|
|
4223
4269
|
process.stdout.write(`${INVITE_HELP}\n`);
|
|
@@ -4230,25 +4276,25 @@ const invite = async (cfg, rest) => {
|
|
|
4230
4276
|
process.exitCode = EXIT.USAGE;
|
|
4231
4277
|
return;
|
|
4232
4278
|
}
|
|
4233
|
-
if (positionals.length >
|
|
4234
|
-
process.stderr.write(
|
|
4279
|
+
if (positionals.length > 1 || (positionals.length === 1 && flags.for !== undefined)) {
|
|
4280
|
+
process.stderr.write(`${INVITE_HELP}\n`);
|
|
4281
|
+
process.exitCode = EXIT.USAGE;
|
|
4282
|
+
return;
|
|
4283
|
+
}
|
|
4284
|
+
const selector = positionals[0] ?? flags.for;
|
|
4285
|
+
if (selector === undefined || selector.trim().length === 0) {
|
|
4286
|
+
process.stderr.write('A contact name or id is required.\n');
|
|
4235
4287
|
process.exitCode = EXIT.USAGE;
|
|
4236
4288
|
return;
|
|
4237
4289
|
}
|
|
4238
4290
|
// Same law as delete and born-public capture: an AI proposes, it cannot
|
|
4239
|
-
// commit. A
|
|
4240
|
-
// destructive, but it spends something real — a slot a person will hand to
|
|
4241
|
-
// someone — and it should never happen because a model thought it helpful.
|
|
4291
|
+
// commit. A non-TTY caller must make the person's approval explicit.
|
|
4242
4292
|
if (!process.stdin.isTTY && !yes) {
|
|
4243
4293
|
process.stderr.write('Refusing to mint without confirmation. Pass --yes once the person has ' +
|
|
4244
4294
|
'asked you to invite someone.\n');
|
|
4245
4295
|
process.exitCode = EXIT.USAGE;
|
|
4246
4296
|
return;
|
|
4247
4297
|
}
|
|
4248
|
-
if (process.stdin.isTTY && !(await confirm('Mint an invitation to hand to one person?'))) {
|
|
4249
|
-
process.stdout.write('Not minted.\n');
|
|
4250
|
-
return;
|
|
4251
|
-
}
|
|
4252
4298
|
let accessToken;
|
|
4253
4299
|
try {
|
|
4254
4300
|
accessToken = await validAccessToken(cfg);
|
|
@@ -4258,16 +4304,64 @@ const invite = async (cfg, rest) => {
|
|
|
4258
4304
|
process.exitCode = EXIT.NOAUTH;
|
|
4259
4305
|
return;
|
|
4260
4306
|
}
|
|
4307
|
+
const contactsResult = await apiFetch(accessToken, `${cfg.apiUrl}/contacts`);
|
|
4308
|
+
if (!contactsResult.ok) {
|
|
4309
|
+
process.exitCode = contactsResult.exit;
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
const resolution = resolveInviteContact(contactsResult.data, selector.trim());
|
|
4313
|
+
if (resolution.kind === 'missing-id') {
|
|
4314
|
+
process.stderr.write(`No contact ${sanitizeForTerminal(selector)} among yours. ${renderRunHint(['contact', 'list'])} shows them.\n`);
|
|
4315
|
+
process.exitCode = EXIT.GENERIC;
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
if (resolution.kind === 'ambiguous') {
|
|
4319
|
+
process.stderr.write(`${sanitizeForTerminal(selector)} matches more than one of your contacts:\n${resolution.matches.map((c) => ` ${formatContact(c)}`).join('\n')}\nUse a contact id instead.\n`);
|
|
4320
|
+
process.exitCode = EXIT.USAGE;
|
|
4321
|
+
return;
|
|
4322
|
+
}
|
|
4323
|
+
const displayName = resolution.kind === 'found' ? resolution.contact.displayName : resolution.displayName;
|
|
4324
|
+
if (resolution.kind === 'found' && resolution.contact.type !== 'person') {
|
|
4325
|
+
process.stderr.write(`${sanitizeForTerminal(displayName)} is an AI contact. An invitation must go to a person contact.\n`);
|
|
4326
|
+
process.exitCode = EXIT.USAGE;
|
|
4327
|
+
return;
|
|
4328
|
+
}
|
|
4329
|
+
const action = resolution.kind === 'missing-name'
|
|
4330
|
+
? `Create person contact "${sanitizeForTerminal(displayName)}" and mint their invitation?`
|
|
4331
|
+
: `Mint an invitation for ${sanitizeForTerminal(displayName)}?`;
|
|
4332
|
+
if (process.stdin.isTTY && !(await confirm(action))) {
|
|
4333
|
+
process.stdout.write('Not minted.\n');
|
|
4334
|
+
return;
|
|
4335
|
+
}
|
|
4336
|
+
let contact;
|
|
4337
|
+
let createdContact = false;
|
|
4338
|
+
if (resolution.kind === 'missing-name') {
|
|
4339
|
+
const created = await apiFetch(accessToken, `${cfg.apiUrl}/contacts`, {
|
|
4340
|
+
method: 'POST',
|
|
4341
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4342
|
+
body: JSON.stringify({ displayName, type: 'person' })
|
|
4343
|
+
});
|
|
4344
|
+
if (!created.ok) {
|
|
4345
|
+
process.exitCode = created.exit;
|
|
4346
|
+
return;
|
|
4347
|
+
}
|
|
4348
|
+
contact = created.data;
|
|
4349
|
+
createdContact = true;
|
|
4350
|
+
}
|
|
4351
|
+
else {
|
|
4352
|
+
contact = resolution.contact;
|
|
4353
|
+
}
|
|
4261
4354
|
const result = await apiFetch(accessToken, `${cfg.apiUrl}/invites`, {
|
|
4262
4355
|
method: 'POST',
|
|
4263
4356
|
headers: { 'Content-Type': 'application/json' },
|
|
4264
|
-
body: JSON.stringify(
|
|
4357
|
+
body: JSON.stringify({ contactId: contact.id })
|
|
4265
4358
|
});
|
|
4266
4359
|
if (!result.ok) {
|
|
4267
4360
|
process.exitCode = result.exit;
|
|
4268
4361
|
return;
|
|
4269
4362
|
}
|
|
4270
|
-
emit(result.data, result.rawText, (i) =>
|
|
4363
|
+
emit(result.data, result.rawText, (i) => `${createdContact ? `Recorded person contact ${formatContact(contact)}.\n\n` : ''}` +
|
|
4364
|
+
`Invitation minted${i.targetName ? ` for ${i.targetName}` : ''}.\n\n` +
|
|
4271
4365
|
` ${inviteLink(cfg, i.token)}\n\n` +
|
|
4272
4366
|
`Hand it to ONE person — it is spent when someone signs up with it.\n` +
|
|
4273
4367
|
`It is not permission to join; whether they may register is decided separately.\n` +
|
|
@@ -4289,7 +4383,7 @@ const getInvites = async (cfg) => {
|
|
|
4289
4383
|
return;
|
|
4290
4384
|
}
|
|
4291
4385
|
emit(result.data, result.rawText, (list) => list.length === 0
|
|
4292
|
-
? 'No invitations yet. `lattris invite
|
|
4386
|
+
? 'No invitations yet. `lattris invite <contact>` mints one.'
|
|
4293
4387
|
: list.map((i) => formatInvite(cfg, i)).join('\n\n'));
|
|
4294
4388
|
};
|
|
4295
4389
|
// ─── share (memory <id> --with <audience>) ─────────────────────────────────────
|
|
@@ -4305,17 +4399,36 @@ const describeAudience = (audience) => audience === 'public' ? 'public — anyon
|
|
|
4305
4399
|
const SHARE_FLAG_NAMES = {
|
|
4306
4400
|
'--with': 'with'
|
|
4307
4401
|
};
|
|
4402
|
+
const resolveExistingPersonContact = async (cfg, accessToken, selector) => {
|
|
4403
|
+
const result = await apiFetch(accessToken, `${cfg.apiUrl}/contacts`);
|
|
4404
|
+
if (!result.ok) {
|
|
4405
|
+
process.exitCode = result.exit;
|
|
4406
|
+
return undefined;
|
|
4407
|
+
}
|
|
4408
|
+
const resolution = resolveInviteContact(result.data, selector.trim());
|
|
4409
|
+
if (resolution.kind === 'ambiguous') {
|
|
4410
|
+
process.stderr.write(`${sanitizeForTerminal(selector)} matches more than one of your contacts:\n${resolution.matches.map((c) => ` ${formatContact(c)}`).join('\n')}\nUse a contact id instead.\n`);
|
|
4411
|
+
process.exitCode = EXIT.USAGE;
|
|
4412
|
+
return undefined;
|
|
4413
|
+
}
|
|
4414
|
+
if (resolution.kind !== 'found') {
|
|
4415
|
+
process.stderr.write(`No contact ${sanitizeForTerminal(selector)} among yours. ${renderRunHint(['contact', 'list'])} shows them.\n`);
|
|
4416
|
+
process.exitCode = EXIT.GENERIC;
|
|
4417
|
+
return undefined;
|
|
4418
|
+
}
|
|
4419
|
+
if (resolution.contact.type !== 'person' || resolution.contact.claimedPersonId == null) {
|
|
4420
|
+
process.stderr.write('This person has no account yet — invite them first.\n');
|
|
4421
|
+
process.exitCode = EXIT.USAGE;
|
|
4422
|
+
return undefined;
|
|
4423
|
+
}
|
|
4424
|
+
return { ...resolution.contact, claimedPersonId: resolution.contact.claimedPersonId };
|
|
4425
|
+
};
|
|
4308
4426
|
const shareMemory = async (cfg, idOrPrefix, audience, yes) => {
|
|
4309
4427
|
// ALL local preconditions settle before any network/token touch — same law
|
|
4310
4428
|
// as delete: a question that was always going to be refused must never
|
|
4311
4429
|
// spend a token refresh first.
|
|
4312
4430
|
if (audience === undefined) {
|
|
4313
|
-
process.stderr.write(
|
|
4314
|
-
process.exitCode = EXIT.USAGE;
|
|
4315
|
-
return;
|
|
4316
|
-
}
|
|
4317
|
-
if (!SHARE_AUDIENCES.includes(audience)) {
|
|
4318
|
-
process.stderr.write(`Unknown audience: ${audience}. Accepted: ${SHARE_AUDIENCES.join(', ')}\n`);
|
|
4431
|
+
process.stderr.write('--with is required. Use public or one of your contact names or ids.\n');
|
|
4319
4432
|
process.exitCode = EXIT.USAGE;
|
|
4320
4433
|
return;
|
|
4321
4434
|
}
|
|
@@ -4337,6 +4450,11 @@ const shareMemory = async (cfg, idOrPrefix, audience, yes) => {
|
|
|
4337
4450
|
// the confirm fetch and share by the FULL id from then on — not the raw
|
|
4338
4451
|
// (possibly a prefix) argument again.
|
|
4339
4452
|
let shareId = idOrPrefix;
|
|
4453
|
+
const contact = audience === 'public'
|
|
4454
|
+
? undefined
|
|
4455
|
+
: await resolveExistingPersonContact(cfg, accessToken, audience);
|
|
4456
|
+
if (audience !== 'public' && !contact)
|
|
4457
|
+
return;
|
|
4340
4458
|
if (process.stdin.isTTY) {
|
|
4341
4459
|
const fetched = await apiFetch(accessToken, `${cfg.apiUrl}/memories/${encodeURIComponent(idOrPrefix)}`);
|
|
4342
4460
|
if (!fetched.ok) {
|
|
@@ -4345,7 +4463,7 @@ const shareMemory = async (cfg, idOrPrefix, audience, yes) => {
|
|
|
4345
4463
|
return;
|
|
4346
4464
|
}
|
|
4347
4465
|
const title = sanitizeForTerminal(fetched.data.title ?? '(untitled)');
|
|
4348
|
-
const confirmed = await confirmAction(`Share "${title}" (${fetched.data.id}) with ${describeAudience(audience)}? [y/N] `);
|
|
4466
|
+
const confirmed = await confirmAction(`Share "${title}" (${fetched.data.id}) with ${describeAudience(contact?.displayName ?? audience)}? [y/N] `);
|
|
4349
4467
|
if (confirmed === 'interrupted') {
|
|
4350
4468
|
process.stderr.write('Cancelled.\n');
|
|
4351
4469
|
process.exitCode = EXIT.GENERIC;
|
|
@@ -4360,7 +4478,7 @@ const shareMemory = async (cfg, idOrPrefix, audience, yes) => {
|
|
|
4360
4478
|
const result = await apiFetch(accessToken, `${cfg.apiUrl}/memories/${encodeURIComponent(shareId)}/grants`, {
|
|
4361
4479
|
method: 'POST',
|
|
4362
4480
|
headers: { 'Content-Type': 'application/json' },
|
|
4363
|
-
body: JSON.stringify({ audience })
|
|
4481
|
+
body: JSON.stringify(contact ? { kind: 'person', contactId: contact.id } : { audience: 'public' })
|
|
4364
4482
|
});
|
|
4365
4483
|
if (!result.ok) {
|
|
4366
4484
|
renderAmbiguousIfPresent(result.body);
|
|
@@ -4373,15 +4491,15 @@ const shareMemory = async (cfg, idOrPrefix, audience, yes) => {
|
|
|
4373
4491
|
// wire, byte-verbatim (emit's non-TTY branch); a TTY gets the human line
|
|
4374
4492
|
// instead. shareId (not the response body, which never echoes an id) is
|
|
4375
4493
|
// what makes the human line locatable — closed over, not a second field.
|
|
4376
|
-
emit(result.data, result.rawText, (grant) => `Shared memory ${shareId} with ${grant.kind}`);
|
|
4494
|
+
emit(result.data, result.rawText, (grant) => `Shared memory ${shareId} with ${contact?.displayName ?? grant.kind}`);
|
|
4377
4495
|
};
|
|
4378
4496
|
export const SHARE_HELP = [
|
|
4379
|
-
'share — grant
|
|
4497
|
+
'share — grant public or a person contact access to a memory.',
|
|
4380
4498
|
'',
|
|
4381
4499
|
'Usage:',
|
|
4382
|
-
' lattris share memory <id-or-prefix> --with <
|
|
4500
|
+
' lattris share memory <id-or-prefix> --with <public-or-contact> [--yes]',
|
|
4383
4501
|
'',
|
|
4384
|
-
' --with
|
|
4502
|
+
' --with public, or an exact contact name or id from `lattris contact list`.',
|
|
4385
4503
|
' --yes required when not at a terminal; at a terminal you',
|
|
4386
4504
|
' are always asked, --yes or not.',
|
|
4387
4505
|
'',
|
|
@@ -4415,29 +4533,10 @@ const share = async (cfg, noun, rest) => {
|
|
|
4415
4533
|
}
|
|
4416
4534
|
await shareMemory(cfg, positionals[0], flags.with, yes);
|
|
4417
4535
|
};
|
|
4418
|
-
// ─── unshare (memory <id> --with <audience>) ───────────────────────────────────
|
|
4419
|
-
// Withdraws an audience — v1 accepts only "public". No confirmation: spec
|
|
4420
|
-
// rules withdrawal the safe direction (recoverable by re-sharing). The
|
|
4421
|
-
// record is ALWAYS resolved first — needed either way: to withdraw by the
|
|
4422
|
-
// FULL id (never re-resolve a prefix, same law as share/delete), or to echo
|
|
4423
|
-
// its current audiences + title in the bare-unshare refusal (owner-only
|
|
4424
|
-
// operation, so the echo leaks nothing this caller couldn't already see via
|
|
4425
|
-
// a plain get).
|
|
4426
|
-
export const UNSHARE_AUDIENCES = ['public'];
|
|
4427
4536
|
const UNSHARE_FLAG_NAMES = {
|
|
4428
4537
|
'--with': 'with'
|
|
4429
4538
|
};
|
|
4430
4539
|
const unshareMemory = async (cfg, idOrPrefix, audience) => {
|
|
4431
|
-
// A PRESENT but unknown audience is a pure value check — it needs nothing
|
|
4432
|
-
// fetched, so it settles before any network/token touch like every other
|
|
4433
|
-
// local precondition in this file. A MISSING audience is different: its
|
|
4434
|
-
// refusal echoes the record's current state, which only the fetch below
|
|
4435
|
-
// can supply, so that one stays gated behind it.
|
|
4436
|
-
if (audience !== undefined && !UNSHARE_AUDIENCES.includes(audience)) {
|
|
4437
|
-
process.stderr.write(`Unknown audience: ${audience}. Accepted: ${UNSHARE_AUDIENCES.join(', ')}\n`);
|
|
4438
|
-
process.exitCode = EXIT.USAGE;
|
|
4439
|
-
return;
|
|
4440
|
-
}
|
|
4441
4540
|
let accessToken;
|
|
4442
4541
|
try {
|
|
4443
4542
|
accessToken = await validAccessToken(cfg);
|
|
@@ -4447,6 +4546,11 @@ const unshareMemory = async (cfg, idOrPrefix, audience) => {
|
|
|
4447
4546
|
process.exitCode = EXIT.NOAUTH;
|
|
4448
4547
|
return;
|
|
4449
4548
|
}
|
|
4549
|
+
const contact = audience === undefined || audience === 'public'
|
|
4550
|
+
? undefined
|
|
4551
|
+
: await resolveExistingPersonContact(cfg, accessToken, audience);
|
|
4552
|
+
if (audience !== undefined && audience !== 'public' && !contact)
|
|
4553
|
+
return;
|
|
4450
4554
|
const fetched = await apiFetch(accessToken, `${cfg.apiUrl}/memories/${encodeURIComponent(idOrPrefix)}`);
|
|
4451
4555
|
if (!fetched.ok) {
|
|
4452
4556
|
renderAmbiguousIfPresent(fetched.body);
|
|
@@ -4455,26 +4559,28 @@ const unshareMemory = async (cfg, idOrPrefix, audience) => {
|
|
|
4455
4559
|
}
|
|
4456
4560
|
if (audience === undefined) {
|
|
4457
4561
|
const title = sanitizeForTerminal(fetched.data.title ?? '(untitled)');
|
|
4458
|
-
process.stderr.write(
|
|
4562
|
+
process.stderr.write('--with is required. Use public or one of your contact names or ids.\n');
|
|
4459
4563
|
process.stderr.write(`"${title}" (${fetched.data.id}) is currently shared with: ${formatAudiences(fetched.data.audiences)}\n`);
|
|
4460
4564
|
process.exitCode = EXIT.USAGE;
|
|
4461
4565
|
return;
|
|
4462
4566
|
}
|
|
4463
|
-
const result = await apiFetch(accessToken,
|
|
4567
|
+
const result = await apiFetch(accessToken, contact
|
|
4568
|
+
? `${cfg.apiUrl}/memories/${encodeURIComponent(fetched.data.id)}/grants/person/${contact.claimedPersonId}`
|
|
4569
|
+
: `${cfg.apiUrl}/memories/${encodeURIComponent(fetched.data.id)}/grants/public`, { method: 'DELETE' });
|
|
4464
4570
|
if (!result.ok) {
|
|
4465
4571
|
process.exitCode = result.exit;
|
|
4466
4572
|
return;
|
|
4467
4573
|
}
|
|
4468
|
-
process.stderr.write(`Unshared memory ${fetched.data.id} from ${
|
|
4574
|
+
process.stderr.write(`Unshared memory ${fetched.data.id} from ${contact?.displayName ?? 'public'}\n`);
|
|
4469
4575
|
};
|
|
4470
4576
|
export const UNSHARE_HELP = [
|
|
4471
4577
|
'unshare — withdraw an audience from a memory. No confirmation — withdrawal',
|
|
4472
4578
|
'is the safe direction.',
|
|
4473
4579
|
'',
|
|
4474
4580
|
'Usage:',
|
|
4475
|
-
' lattris unshare memory <id-or-prefix> --with <
|
|
4581
|
+
' lattris unshare memory <id-or-prefix> --with <public-or-contact>',
|
|
4476
4582
|
'',
|
|
4477
|
-
' --with
|
|
4583
|
+
' --with public, or an exact contact name or id from `lattris contact list`.',
|
|
4478
4584
|
'',
|
|
4479
4585
|
'Bare (no --with): refuses and shows what the memory is currently shared',
|
|
4480
4586
|
'with, so you can see the state and the fix in one line.',
|
|
@@ -4505,6 +4611,58 @@ const unshare = async (cfg, noun, rest) => {
|
|
|
4505
4611
|
}
|
|
4506
4612
|
await unshareMemory(cfg, positionals[0], flags.with);
|
|
4507
4613
|
};
|
|
4614
|
+
// ─── consent (<shared-memory-id> [--withdraw]) ─────────────────────────────
|
|
4615
|
+
export const CONSENT_HELP = [
|
|
4616
|
+
'consent — choose whether a memory shared with you appears on your graph.',
|
|
4617
|
+
'',
|
|
4618
|
+
'Usage:',
|
|
4619
|
+
' lattris consent <id-or-prefix>',
|
|
4620
|
+
' lattris consent <id-or-prefix> --withdraw',
|
|
4621
|
+
'',
|
|
4622
|
+
'The choice is yours alone. Withdrawing a share hides the block regardless;',
|
|
4623
|
+
'if its owner shares it again later, your previous choice still stands.'
|
|
4624
|
+
].join('\n');
|
|
4625
|
+
const consent = async (cfg, rest) => {
|
|
4626
|
+
if (rest.some((arg) => HELP_TOKENS.has(arg))) {
|
|
4627
|
+
process.stdout.write(`${CONSENT_HELP}\n`);
|
|
4628
|
+
return;
|
|
4629
|
+
}
|
|
4630
|
+
const withdrawCount = rest.filter((arg) => arg === '--withdraw').length;
|
|
4631
|
+
const args = rest.filter((arg) => arg !== '--withdraw');
|
|
4632
|
+
const unknown = args.find((arg) => arg.startsWith('--'));
|
|
4633
|
+
if (unknown !== undefined) {
|
|
4634
|
+
process.stderr.write(`Unknown flag: ${unknown}\n`);
|
|
4635
|
+
process.exitCode = EXIT.USAGE;
|
|
4636
|
+
return;
|
|
4637
|
+
}
|
|
4638
|
+
if (withdrawCount > 1 || args.length !== 1) {
|
|
4639
|
+
process.stderr.write(`Usage: lattris consent <id-or-prefix> [--withdraw]\n`);
|
|
4640
|
+
process.exitCode = EXIT.USAGE;
|
|
4641
|
+
return;
|
|
4642
|
+
}
|
|
4643
|
+
let accessToken;
|
|
4644
|
+
try {
|
|
4645
|
+
accessToken = await validAccessToken(cfg);
|
|
4646
|
+
}
|
|
4647
|
+
catch {
|
|
4648
|
+
renderNotLoggedIn();
|
|
4649
|
+
process.exitCode = EXIT.NOAUTH;
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4652
|
+
const result = await apiFetch(accessToken, `${cfg.apiUrl}/memories/${encodeURIComponent(args[0])}/consent`, {
|
|
4653
|
+
method: 'PUT',
|
|
4654
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4655
|
+
body: JSON.stringify({ consented: withdrawCount === 0 })
|
|
4656
|
+
});
|
|
4657
|
+
if (!result.ok) {
|
|
4658
|
+
renderAmbiguousIfPresent(result.body);
|
|
4659
|
+
process.exitCode = result.exit;
|
|
4660
|
+
return;
|
|
4661
|
+
}
|
|
4662
|
+
emit(result.data, result.rawText, (state) => state.consented
|
|
4663
|
+
? `Memory ${args[0]} may appear on your graph.`
|
|
4664
|
+
: `Memory ${args[0]} will stay off your graph.`);
|
|
4665
|
+
};
|
|
4508
4666
|
// ─── capture recording (train 8) ────────────────────────────────────────────
|
|
4509
4667
|
// The API meets ONE recording shape; vendor transcript formats translate HERE,
|
|
4510
4668
|
// CLI-side, so the door never grows a per-vendor arm. A recording is one door,
|
|
@@ -5889,7 +6047,7 @@ const usage = () => {
|
|
|
5889
6047
|
' lattris capture session <transcript> Save an AI conversation — the whole chain from its file',
|
|
5890
6048
|
' lattris get memories List your memories, newest first, with filters',
|
|
5891
6049
|
' lattris contact create "<name>" Record a person or an AI you work with (--ai); list, get, update',
|
|
5892
|
-
' lattris invite
|
|
6050
|
+
' lattris invite <contact> Mint an invitation for one of your person contacts',
|
|
5893
6051
|
' lattris get invites List the invitations you have minted',
|
|
5894
6052
|
' lattris get memory <id> Read one memory back \u2014 words and edges (--links/--depth/--content)',
|
|
5895
6053
|
' lattris ask <handle> Leave a question on someone\u2019s tree \u2014 `echo "\u2026" | lattris ask <handle>`',
|
|
@@ -5897,6 +6055,7 @@ const usage = () => {
|
|
|
5897
6055
|
' lattris close question <id> Close a question (--close-as updated|declined|already-answered)',
|
|
5898
6056
|
' lattris share memory <id> Grant an audience access (--with public)',
|
|
5899
6057
|
' lattris unshare memory <id> Withdraw an audience (--with public)',
|
|
6058
|
+
' lattris consent <id> Put a memory shared with you on your graph (--withdraw reverses)',
|
|
5900
6059
|
' lattris link <from> <to> State that one memory leads to another (--verb, --reason)',
|
|
5901
6060
|
' lattris unlink <from> <to> Remove that claim (--verb; the blocks stay)',
|
|
5902
6061
|
' lattris update memory <id> Correct a memory — `echo "…" | lattris update memory <id> --base-fingerprint <fp>`',
|
|
@@ -6420,11 +6579,13 @@ export const main = async () => {
|
|
|
6420
6579
|
process.env.LATTRIS_HOME = asResolvedDir;
|
|
6421
6580
|
}
|
|
6422
6581
|
}
|
|
6423
|
-
// Resolve the environment name: --env > .lattris env= > LATTRIS_ENV >
|
|
6582
|
+
// Resolve the environment name: --env > .lattris env= > LATTRIS_ENV > the
|
|
6583
|
+
// install-aware fallback (published package = prod; worktree/source = local).
|
|
6424
6584
|
// Must run AFTER --as (which sets LATTRIS_HOME, affecting resolveSessionHome
|
|
6425
6585
|
// which findBinding reads for the env= field).
|
|
6426
6586
|
const home = resolveSessionHome();
|
|
6427
|
-
resolvedEnvName =
|
|
6587
|
+
resolvedEnvName =
|
|
6588
|
+
envName ?? home.bindingEnv ?? process.env.LATTRIS_ENV ?? defaultEnvironmentForEntry();
|
|
6428
6589
|
envWasExplicit =
|
|
6429
6590
|
envName !== undefined || home.bindingEnv !== undefined || process.env.LATTRIS_ENV !== undefined;
|
|
6430
6591
|
const forceFlag = argv.includes('--force');
|
|
@@ -6525,6 +6686,9 @@ export const main = async () => {
|
|
|
6525
6686
|
case 'unshare':
|
|
6526
6687
|
await unshare(cfg, argv[3], argv.slice(4));
|
|
6527
6688
|
break;
|
|
6689
|
+
case 'consent':
|
|
6690
|
+
await consent(cfg, argv.slice(3));
|
|
6691
|
+
break;
|
|
6528
6692
|
case 'contact':
|
|
6529
6693
|
await contact(cfg, argv[3], argv.slice(4));
|
|
6530
6694
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lattris",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "First-party CLI for Lattris — sign in, capture notes and sessions, and read your own tree from the terminal.",
|
|
6
6
|
"keywords": [
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
],
|
|
12
12
|
"license": "UNLICENSED",
|
|
13
13
|
"homepage": "https://lattris.com",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node ../../../node_modules/vitest/vitest.mjs run --config vite.config.ts",
|
|
16
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && node ../../../node_modules/typescript/bin/tsc -p tsconfig.json",
|
|
17
|
+
"release": "node scripts/release.mjs"
|
|
18
|
+
},
|
|
14
19
|
"bin": {
|
|
15
20
|
"lattris": "./dist/main.js"
|
|
16
21
|
},
|