inbox-to-md 0.1.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/dist/mime.js ADDED
@@ -0,0 +1,105 @@
1
+ // MIME body-structure helpers: pick which part of an email to download and
2
+ // decode it. The sync engine fetches BODYSTRUCTURE first and then downloads
3
+ // only the text part it needs, so attachment bytes (base64-encoded and often
4
+ // megabytes) never cross the wire; attachments are described from the
5
+ // structure metadata alone.
6
+ function leafNodes(root) {
7
+ if (root.childNodes && root.childNodes.length > 0) {
8
+ return root.childNodes.flatMap((child) => leafNodes(child));
9
+ }
10
+ return [root];
11
+ }
12
+ // A non-multipart message has a single unnumbered node; IMAP addresses its
13
+ // body as part "1".
14
+ export function findTextPart(root) {
15
+ let plain = null;
16
+ let html = null;
17
+ for (const node of leafNodes(root)) {
18
+ const type = (node.type ?? '').toLowerCase();
19
+ if ((node.disposition ?? '').toLowerCase() === 'attachment')
20
+ continue;
21
+ const ref = {
22
+ part: node.part ?? '1',
23
+ encoding: (node.encoding ?? '').toLowerCase(),
24
+ charset: node.parameters?.charset ?? 'utf-8',
25
+ isHtml: type === 'text/html',
26
+ };
27
+ if (type === 'text/plain' && plain === null)
28
+ plain = ref;
29
+ else if (type === 'text/html' && html === null)
30
+ html = ref;
31
+ }
32
+ return plain ?? html;
33
+ }
34
+ // Attachments are described from structure metadata only — their bytes are
35
+ // never downloaded. Anything explicitly marked as an attachment counts, as
36
+ // does any named non-text part (inline images and the like).
37
+ export function collectAttachments(root) {
38
+ const attachments = [];
39
+ for (const node of leafNodes(root)) {
40
+ const type = (node.type ?? '').toLowerCase();
41
+ const name = node.dispositionParameters?.filename ?? node.parameters?.name ?? null;
42
+ const isAttachment = (node.disposition ?? '').toLowerCase() === 'attachment' ||
43
+ (name !== null && !type.startsWith('text/'));
44
+ if (!isAttachment)
45
+ continue;
46
+ attachments.push({
47
+ name: name ?? '(unnamed)',
48
+ size: node.size ?? 0,
49
+ contentType: type || 'application/octet-stream',
50
+ });
51
+ }
52
+ return attachments;
53
+ }
54
+ // =XX hex escapes, soft line breaks (=\r\n or =\n) removed; a stray '=' that
55
+ // fits neither form is kept literally, as decoders conventionally do.
56
+ function decodeQuotedPrintable(raw) {
57
+ const s = raw.toString('latin1');
58
+ const out = [];
59
+ for (let i = 0; i < s.length; i++) {
60
+ if (s[i] !== '=') {
61
+ out.push(s.charCodeAt(i));
62
+ continue;
63
+ }
64
+ if (s[i + 1] === '\r' && s[i + 2] === '\n') {
65
+ i += 2;
66
+ continue;
67
+ }
68
+ if (s[i + 1] === '\n') {
69
+ i += 1;
70
+ continue;
71
+ }
72
+ const hex = s.slice(i + 1, i + 3);
73
+ if (/^[0-9A-Fa-f]{2}$/.test(hex)) {
74
+ out.push(parseInt(hex, 16));
75
+ i += 2;
76
+ continue;
77
+ }
78
+ out.push(0x3d); // '='
79
+ }
80
+ return Buffer.from(out);
81
+ }
82
+ // Decodes a raw body part (as stored on the server) to a string: undo the
83
+ // content-transfer-encoding, then the charset. Unknown charsets fall back to
84
+ // UTF-8 rather than failing the whole email — mojibake in one message is
85
+ // recoverable, a sync error is noise on every run.
86
+ export function decodeTextPart(raw, encoding, charset) {
87
+ let bytes;
88
+ switch (encoding) {
89
+ case 'base64':
90
+ bytes = Buffer.from(raw.toString('latin1'), 'base64'); // Buffer.from skips whitespace
91
+ break;
92
+ case 'quoted-printable':
93
+ bytes = decodeQuotedPrintable(raw);
94
+ break;
95
+ default: // 7bit / 8bit / binary
96
+ bytes = raw;
97
+ }
98
+ try {
99
+ return new TextDecoder(charset).decode(bytes);
100
+ }
101
+ catch {
102
+ return new TextDecoder('utf-8').decode(bytes);
103
+ }
104
+ }
105
+ //# sourceMappingURL=mime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mime.js","sourceRoot":"","sources":["../src/mime.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare const COMPACT_SYSTEM_PROMPT: string;
@@ -0,0 +1,11 @@
1
+ // All model prompts, kept in one place so they can be reviewed and tuned
2
+ // without digging through the call sites.
3
+ // System prompt for `npm run compact` window compaction (compact.ts).
4
+ export const COMPACT_SYSTEM_PROMPT = 'You compact email archives. Rewrite the given emails as a dense markdown digest: ' +
5
+ 'keep every sender, date, decision, commitment, number, and open question; drop ' +
6
+ 'signatures, quoted reply chains, and boilerplate. Each email is preceded by a ' +
7
+ '<!-- source: ... --> comment naming its source file; ALWAYS cite that source next ' +
8
+ 'to every fact as [<source>], e.g. [2026-05-20-re]. When the input is already a ' +
9
+ 'digest, preserve its existing [...] citations on every fact you keep. ' +
10
+ 'Respond with the digest only.';
11
+ //# sourceMappingURL=prompts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,0CAA0C;AAE1C,sEAAsE;AACtE,MAAM,CAAC,MAAM,qBAAqB,GAChC,mFAAmF;IACnF,iFAAiF;IACjF,gFAAgF;IAChF,oFAAoF;IACpF,iFAAiF;IACjF,wEAAwE;IACxE,+BAA+B,CAAC"}
@@ -0,0 +1,6 @@
1
+ export interface SyncedEmail {
2
+ path: string;
3
+ citationKey: string;
4
+ }
5
+ export declare function listSyncedEmails(syncPath: string): SyncedEmail[];
6
+ export declare function readSyncedEmail(email: SyncedEmail): string;
@@ -0,0 +1,16 @@
1
+ // Read-only access to the synced email markdown files of one sync directory.
2
+ // Filenames start with a YYYY-MM-DD stamp (see markdown.ts buildFilename), so
3
+ // a lexicographic sort on the basename is chronological.
4
+ import { readFileSync, readdirSync } from 'fs';
5
+ import { basename, join } from 'path';
6
+ // Emails in syncPath, oldest first.
7
+ export function listSyncedEmails(syncPath) {
8
+ return readdirSync(syncPath)
9
+ .filter((name) => name.endsWith('.md'))
10
+ .sort((a, b) => a.localeCompare(b))
11
+ .map((name) => ({ path: join(syncPath, name), citationKey: basename(name, '.md') }));
12
+ }
13
+ export function readSyncedEmail(email) {
14
+ return readFileSync(email.path, 'utf8');
15
+ }
16
+ //# sourceMappingURL=ref-emails.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ref-emails.js","sourceRoot":"","sources":["../src/ref-emails.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,8EAA8E;AAC9E,yDAAyD;AAEzD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,IAAI,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAStC,oCAAoC;AACpC,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,OAAO,WAAW,CAAC,QAAQ,CAAC;SACzB,MAAM,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SAC9C,IAAI,CAAC,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;SAClD,GAAG,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;AACjG,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAkB;IAChD,OAAO,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC1C,CAAC"}
package/dist/sync.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { Config } from './config.js';
2
+ interface ExistingFile {
3
+ name: string;
4
+ date: Date | null;
5
+ mailbox: string | null;
6
+ }
7
+ export declare function computeSinceDate(now?: Date): Date;
8
+ export declare function collectExistingFiles(dir: string): Map<string, ExistingFile>;
9
+ export declare function runSync(config: Config, since?: Date, forceRewrite?: boolean): Promise<boolean>;
10
+ export {};
package/dist/sync.js ADDED
@@ -0,0 +1,296 @@
1
+ // `npm start` — sync engine. For each configured account, fetches mail from
2
+ // INBOX and the Sent mailbox (received this month or last month by default)
3
+ // and writes one markdown file per email into the account's sync path.
4
+ // Idempotent: emails already on disk (matched by message-id in the files'
5
+ // frontmatter) are skipped without re-downloading their bodies — unless
6
+ // --force-rewrite, which re-downloads them and overwrites the files in place
7
+ // (e.g. to pick up frontmatter added by a newer version of renderEmail). Mirrors
8
+ // deletions too: a file whose email has vanished from the server (and whose
9
+ // date is safely inside the sync window) is deleted after a clean sync.
10
+ import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync } from 'fs';
11
+ import { join } from 'path';
12
+ import { bold, dim, green, red } from './ansi.js';
13
+ import { closeImapClient, createImapClient, describeImapError } from './imap.js';
14
+ import { buildFilename, extractFrontmatterValue, extractMessageId, fallbackHash, readFrontmatterHead, renderEmail } from './markdown.js';
15
+ import { collectAttachments, decodeTextPart, findTextPart } from './mime.js';
16
+ // SEARCH SINCE works on the server's internal date at day granularity in the
17
+ // server's timezone, while our frontmatter stores the email's header date —
18
+ // the two can disagree around the boundary. Only prune files comfortably
19
+ // inside the window so a boundary mismatch can't delete a still-live email.
20
+ const PRUNE_BOUNDARY_MARGIN_MS = 24 * 60 * 60 * 1000;
21
+ // In-place progress line, TTY only: piped output stays clean (matching the
22
+ // ansi.ts helpers, which also degrade to plain text off-TTY). Overwrites
23
+ // itself with \r and is cleared before any real line is printed.
24
+ class ProgressLine {
25
+ active = false;
26
+ update(text) {
27
+ if (!process.stdout.isTTY)
28
+ return;
29
+ process.stdout.write(`\r\x1b[2K ${dim(text)}`);
30
+ this.active = true;
31
+ }
32
+ clear() {
33
+ if (!this.active)
34
+ return;
35
+ process.stdout.write('\r\x1b[2K');
36
+ this.active = false;
37
+ }
38
+ }
39
+ export function computeSinceDate(now = new Date()) {
40
+ // Month -1 in January normalizes to December of the prior year.
41
+ return new Date(now.getFullYear(), now.getMonth() - 1, 1);
42
+ }
43
+ // The files on disk are the only dedupe state — no sidecar index, which would
44
+ // drift the moment the user deletes or moves a file and then silently skip
45
+ // re-syncing it.
46
+ export function collectExistingFiles(dir) {
47
+ const existing = new Map();
48
+ for (const name of readdirSync(dir)) {
49
+ if (!name.endsWith('.md'))
50
+ continue;
51
+ const head = readFrontmatterHead(join(dir, name));
52
+ if (head === null)
53
+ continue; // vanished between readdir and open; nothing to dedupe against
54
+ const id = extractMessageId(head);
55
+ if (!id)
56
+ continue;
57
+ const dateRaw = extractFrontmatterValue(head, 'date');
58
+ const date = dateRaw === null ? null : new Date(dateRaw);
59
+ existing.set(id, {
60
+ name,
61
+ date: date !== null && !Number.isNaN(date.getTime()) ? date : null,
62
+ mailbox: extractFrontmatterValue(head, 'mailbox'),
63
+ });
64
+ }
65
+ return existing;
66
+ }
67
+ // Deletes files whose email is no longer on the server. Only files we can
68
+ // positively rule dead are removed: the email's date must sit safely inside
69
+ // the sync window (older mail was never searched, so its absence from
70
+ // serverIds means nothing) and its mailbox must be one we actually synced
71
+ // this run. Everything else — including files with unparseable frontmatter —
72
+ // is left alone.
73
+ function pruneDeletedEmails(dir, existing, serverIds, syncedMailboxes, since) {
74
+ const cutoff = since.getTime() + PRUNE_BOUNDARY_MARGIN_MS;
75
+ let deleted = 0;
76
+ for (const [id, file] of existing) {
77
+ if (serverIds.has(id))
78
+ continue;
79
+ if (!file.date || file.date.getTime() < cutoff)
80
+ continue;
81
+ if (!file.mailbox || !syncedMailboxes.includes(file.mailbox))
82
+ continue;
83
+ rmSync(join(dir, file.name), { force: true });
84
+ deleted++;
85
+ }
86
+ return deleted;
87
+ }
88
+ // A truncated file whose frontmatter already contains the message-id would
89
+ // make every future run skip a half-written email — so write to a tmp name
90
+ // (not *.md, invisible to the scanner) and rename into place.
91
+ function writeFileAtomic(path, content) {
92
+ const tmp = path + '.tmp';
93
+ rmSync(tmp, { force: true });
94
+ writeFileSync(tmp, content);
95
+ renameSync(tmp, path);
96
+ }
97
+ function chooseTargetPath(dir, email, fetchDate, existing, forceRewrite) {
98
+ const date = email.date ?? fetchDate;
99
+ const subject = email.subject;
100
+ const messageId = email.messageId;
101
+ if (messageId === '') {
102
+ // No message-id to dedupe on, so the filename itself is the identity:
103
+ // deterministic hash of from/date/subject means existsSync === synced.
104
+ const name = buildFilename(date, subject, fallbackHash(email.from, date.toISOString(), subject));
105
+ const path = join(dir, name);
106
+ return { path, alreadySynced: !forceRewrite && existsSync(path) };
107
+ }
108
+ // Force-rewrite overwrites the file this email already lives in, keeping
109
+ // its name stable rather than minting a duplicate under a fresh suffix.
110
+ if (forceRewrite) {
111
+ const prior = existing.get(messageId);
112
+ if (prior)
113
+ return { path: join(dir, prior.name), alreadySynced: false };
114
+ }
115
+ // Message-id dedupe already ran (phase A), so an existing file here is a
116
+ // different email that happens to share date + subject — pick a free name.
117
+ // The counter goes in the suffix slot, after the slug: appending it to the
118
+ // subject doesn't work because slugify truncates long subjects to 80 chars,
119
+ // slicing the counter off and looping forever on the same name.
120
+ let path = join(dir, buildFilename(date, subject));
121
+ for (let n = 2; existsSync(path); n++) {
122
+ path = join(dir, buildFilename(date, subject, String(n)));
123
+ }
124
+ return { path, alreadySynced: false };
125
+ }
126
+ function formatAddressList(list) {
127
+ return (list ?? [])
128
+ .map((a) => (a.name ? `${a.name} <${a.address ?? ''}>` : (a.address ?? '')))
129
+ .join(', ');
130
+ }
131
+ // Downloads and assembles one email using body structure instead of full
132
+ // source: the envelope covers the headers, the structure describes the
133
+ // attachments, and only the single text part is actually fetched — so
134
+ // attachment bytes never leave the server.
135
+ async function downloadEmail(client, uid) {
136
+ const msg = await client.fetchOne(uid, { envelope: true, bodyStructure: true }, { uid: true });
137
+ if (!msg || !msg.envelope || !msg.bodyStructure)
138
+ throw new Error('server returned no envelope/body structure');
139
+ let text = '';
140
+ let html = '';
141
+ const textRef = findTextPart(msg.bodyStructure);
142
+ if (textRef) {
143
+ const partMsg = await client.fetchOne(uid, { bodyParts: [textRef.part] }, { uid: true });
144
+ const raw = partMsg ? partMsg.bodyParts?.get(textRef.part) : undefined;
145
+ if (!raw)
146
+ throw new Error(`server returned no body part ${textRef.part}`);
147
+ const decoded = decodeTextPart(raw, textRef.encoding, textRef.charset);
148
+ if (textRef.isHtml)
149
+ html = decoded;
150
+ else
151
+ text = decoded;
152
+ }
153
+ return {
154
+ from: formatAddressList(msg.envelope.from),
155
+ to: formatAddressList(msg.envelope.to),
156
+ subject: msg.envelope.subject ?? '',
157
+ date: msg.envelope.date ?? null,
158
+ messageId: (msg.envelope.messageId ?? '').trim(),
159
+ text,
160
+ html,
161
+ attachments: collectAttachments(msg.bodyStructure),
162
+ };
163
+ }
164
+ // Locates the account's Sent mailbox: the RFC 6154 special-use flag when the
165
+ // server provides it, else the usual names (Sent, Sent Mail, Sent Items, …).
166
+ // Returns null when nothing matches — some servers simply have no Sent box.
167
+ async function findSentMailbox(client) {
168
+ const boxes = await client.list();
169
+ const bySpecialUse = boxes.find((b) => b.specialUse === '\\Sent');
170
+ if (bySpecialUse)
171
+ return bySpecialUse.path;
172
+ const byName = boxes.find((b) => /^sent( messages| items| mail)?$/i.test(b.name));
173
+ return byName ? byName.path : null;
174
+ }
175
+ async function syncMailbox(client, mailbox, since, dir, seen, serverIds, existing, forceRewrite, counts) {
176
+ const lock = await client.getMailboxLock(mailbox);
177
+ const progress = new ProgressLine();
178
+ try {
179
+ const uids = await client.search({ since }, { uid: true });
180
+ if (!uids || uids.length === 0)
181
+ return;
182
+ // Phase A: envelopes only. Filter out already-synced message-ids before
183
+ // downloading anything heavy — on re-runs (the common case) this means
184
+ // an unchanged mailbox costs envelopes, never full bodies.
185
+ const newUids = [];
186
+ let checked = 0;
187
+ for await (const msg of client.fetch(uids, { envelope: true }, { uid: true })) {
188
+ progress.update(`${mailbox}: checking ${++checked}/${uids.length}`);
189
+ const messageId = (msg.envelope?.messageId ?? '').trim();
190
+ if (messageId !== '')
191
+ serverIds.add(messageId); // still alive on the server — protect from pruning
192
+ if (messageId !== '' && seen.has(messageId)) {
193
+ counts.skipped++;
194
+ continue;
195
+ }
196
+ if (messageId !== '')
197
+ seen.add(messageId); // dedupes duplicates within this run too
198
+ newUids.push(msg.uid);
199
+ }
200
+ if (newUids.length === 0)
201
+ return; // imapflow rejects an empty fetch range
202
+ // Phase B: body text for new mail only (attachments stay on the server —
203
+ // see downloadEmail). One message at a time rather than a single bulk
204
+ // FETCH: the progress line can update before each download, and one bad
205
+ // message can't stall the entire pipeline.
206
+ for (const [i, uid] of newUids.entries()) {
207
+ progress.update(`${mailbox}: downloading ${i + 1}/${newUids.length}`);
208
+ try {
209
+ const email = await downloadEmail(client, uid);
210
+ const { path, alreadySynced } = chooseTargetPath(dir, email, new Date(), existing, forceRewrite);
211
+ if (alreadySynced) {
212
+ counts.skipped++;
213
+ continue;
214
+ }
215
+ writeFileAtomic(path, renderEmail(email, new Date(), mailbox));
216
+ counts.written++;
217
+ }
218
+ catch (err) {
219
+ counts.errors++;
220
+ const detail = err instanceof Error ? err.message : String(err);
221
+ progress.clear(); // don't let the error line splice into the progress line
222
+ console.error(red(` error on ${mailbox} uid ${uid}: ${detail}`));
223
+ }
224
+ }
225
+ }
226
+ finally {
227
+ progress.clear();
228
+ lock.release();
229
+ }
230
+ }
231
+ async function syncAccount(account, since, forceRewrite) {
232
+ const counts = { written: 0, skipped: 0, deleted: 0, errors: 0 };
233
+ const dir = account.syncPath;
234
+ mkdirSync(dir, { recursive: true });
235
+ const existing = collectExistingFiles(dir);
236
+ // Shared across mailboxes, so a message that lives in both INBOX and Sent
237
+ // (e.g. mail to yourself) is written once. INBOX is synced first, so the
238
+ // received copy wins. Force-rewrite starts empty so files on disk don't
239
+ // suppress re-downloading, but still dedupes across mailboxes within the run.
240
+ const seen = forceRewrite ? new Set() : new Set(existing.keys());
241
+ // Every message-id observed on the server this run, whether or not it was
242
+ // already synced — the ground truth pruning compares against.
243
+ const serverIds = new Set();
244
+ const client = createImapClient(account);
245
+ await client.connect();
246
+ try {
247
+ const mailboxes = ['INBOX'];
248
+ const sent = await findSentMailbox(client);
249
+ if (sent)
250
+ mailboxes.push(sent);
251
+ else
252
+ console.error(`${account.label}: no Sent mailbox found — syncing INBOX only`);
253
+ for (const mailbox of mailboxes) {
254
+ await syncMailbox(client, mailbox, since, dir, seen, serverIds, existing, forceRewrite, counts);
255
+ }
256
+ // Runs only after every mailbox listed its messages without throwing —
257
+ // an aborted sync leaves serverIds incomplete and must not delete anything
258
+ // (a mid-run failure propagates out of the loop above and skips this).
259
+ counts.deleted = pruneDeletedEmails(dir, existing, serverIds, mailboxes, since);
260
+ return counts;
261
+ }
262
+ finally {
263
+ await closeImapClient(client);
264
+ }
265
+ }
266
+ // Syncs every account, isolating failures: one broken account logs loudly and
267
+ // the rest still run. Returns true only when everything was fully clean.
268
+ export async function runSync(config, since = computeSinceDate(), forceRewrite = false) {
269
+ if (config.accounts.length === 0) {
270
+ console.error('No accounts configured. Run `npm run auth` to add one.');
271
+ return false;
272
+ }
273
+ console.log(dim(`Syncing INBOX + Sent mail since ${since.toDateString()}${forceRewrite ? ' (force-rewrite: overwriting already-synced files)' : ''}`));
274
+ let allOk = true;
275
+ const totals = { written: 0, skipped: 0, deleted: 0, errors: 0 };
276
+ for (const account of config.accounts) {
277
+ try {
278
+ const counts = await syncAccount(account, since, forceRewrite);
279
+ totals.written += counts.written;
280
+ totals.skipped += counts.skipped;
281
+ totals.deleted += counts.deleted;
282
+ totals.errors += counts.errors;
283
+ const errorPart = counts.errors > 0 ? red(`${counts.errors} errors`) : green('0 errors');
284
+ console.log(`${bold(account.label)}: ${counts.written} new · ${counts.skipped} skipped · ${counts.deleted} deleted · ${errorPart} → ${account.syncPath}`);
285
+ if (counts.errors > 0)
286
+ allOk = false;
287
+ }
288
+ catch (err) {
289
+ allOk = false;
290
+ console.error(`${bold(account.label)}: ${red(`FAILED — ${describeImapError(err, account.host, account.port)}`)}`);
291
+ }
292
+ }
293
+ console.log(dim(`Total: ${totals.written} new · ${totals.skipped} skipped · ${totals.deleted} deleted · ${totals.errors} errors`));
294
+ return allOk;
295
+ }
296
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"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"}
package/dist/tui.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ export type Key = {
2
+ name: 'up' | 'down' | 'left' | 'right' | 'enter' | 'backspace' | 'ctrl-c' | 'ctrl-x' | 'escape';
3
+ } | {
4
+ name: 'char';
5
+ ch: string;
6
+ };
7
+ export declare function readKey(): Promise<Key>;
8
+ export declare function enterTui(): void;
9
+ export declare function exitTui(): void;
10
+ export interface MenuOption<V extends string = string> {
11
+ label: string;
12
+ value: V;
13
+ }
14
+ export type MenuResult<V extends string> = {
15
+ kind: 'pick';
16
+ value: V;
17
+ } | {
18
+ kind: 'ctrl-x';
19
+ value: V;
20
+ } | {
21
+ kind: 'escape';
22
+ };
23
+ export declare function pickFromMenu<V extends string>(title: string, options: MenuOption<V>[], footer?: string | null, opts?: {
24
+ enableCtrlX?: boolean;
25
+ }): Promise<MenuResult<V>>;
26
+ export type FieldKind = 'text' | 'secret' | 'number' | 'select';
27
+ export interface FormField {
28
+ key: string;
29
+ label: string;
30
+ kind: FieldKind;
31
+ value: string;
32
+ options?: string[];
33
+ hint?: string;
34
+ }
35
+ export interface FormSpec {
36
+ title: string;
37
+ hint?: string;
38
+ fields: FormField[];
39
+ submitLabel?: string;
40
+ verify?: (values: Record<string, string>) => Promise<string | null>;
41
+ }
42
+ export declare function runForm(spec: FormSpec): Promise<Record<string, string> | 'back'>;