@letterapp/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,56 +1,540 @@
1
1
  #!/usr/bin/env node
2
- import { COMMANDS, DEFAULT_COMMAND, findCommand } from "./commands/registry.js";
3
- import { banner, color, error, log } from "./lib/ui.js";
4
- const VERSION = "0.1.0";
5
- function printHelp() {
6
- banner();
7
- log(`${color.bold("Usage")} letter <command> [options]`);
8
- log();
9
- log(color.bold("Commands"));
10
- for (const c of COMMANDS) {
11
- const names = [c.name, ...(c.aliases ?? [])].join(", ");
12
- log(` ${names.padEnd(18)} ${color.dim(c.summary)}`);
13
- }
14
- log(` ${"help".padEnd(18)} ${color.dim("Show this help")}`);
15
- log();
16
- log(color.bold("Login options"));
17
- log(` ${"--no-open".padEnd(18)} ${color.dim("Don't auto-open the browser; print the URL")}`);
18
- log(` ${"--yes, -y".padEnd(18)} ${color.dim("Non-interactive (for agents/CI prompts)")}`);
19
- log(` ${"--no-install".padEnd(18)} ${color.dim("Skip installing @letterapp/node")}`);
20
- log(` ${"--base-url <url>".padEnd(18)} ${color.dim("Target a self-hosted / local Letter")}`);
21
- log(` ${"--api-key <key>".padEnd(18)} ${color.dim("CI only: write a key without the device flow")}`);
22
- log();
23
- log(color.dim("Docs: https://letter.app/docs/agent-setup"));
24
- log();
25
- }
26
- async function main() {
27
- const argv = process.argv.slice(2);
28
- const first = argv[0];
29
- if (first === "--version" || first === "-v") {
30
- log(VERSION);
31
- return 0;
32
- }
33
- if (first === "help" || first === "--help" || first === "-h") {
34
- printHelp();
35
- return 0;
36
- }
37
- // No command, or a flag-first invocation (e.g. `letter --no-open`): run the
38
- // default command with all args.
39
- if (!first || first.startsWith("-")) {
40
- const cmd = findCommand(DEFAULT_COMMAND);
41
- return cmd.run(argv);
42
- }
43
- const cmd = findCommand(first);
44
- if (!cmd) {
45
- error(`Unknown command: ${first}`);
46
- log(`Run ${color.cyan("letter help")} to see available commands.`);
47
- return 1;
48
- }
49
- return cmd.run(argv.slice(1));
50
- }
51
- main()
52
- .then((code) => process.exit(code))
53
- .catch((err) => {
54
- error(err instanceof Error ? err.message : String(err));
55
- process.exit(1);
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/output.ts
7
+ import chalk from "chalk";
8
+ import ora from "ora";
9
+ import { createInterface } from "readline";
10
+ var jsonMode = false;
11
+ function setJsonMode(enabled) {
12
+ jsonMode = enabled;
13
+ }
14
+ function isJsonMode() {
15
+ return jsonMode;
16
+ }
17
+ function printJson(data) {
18
+ console.log(JSON.stringify(data, null, 2));
19
+ }
20
+ function log(msg = "") {
21
+ if (jsonMode) return;
22
+ console.log(msg);
23
+ }
24
+ function printSuccess(msg) {
25
+ if (jsonMode) return;
26
+ console.log(chalk.green("\u2713") + " " + msg);
27
+ }
28
+ function printInfo(msg) {
29
+ if (jsonMode) return;
30
+ console.log(chalk.cyan("\u203A") + " " + msg);
31
+ }
32
+ function printWarning(msg) {
33
+ if (jsonMode) return;
34
+ console.log(chalk.yellow("!") + " " + msg);
35
+ }
36
+ function printError(err) {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ if (jsonMode) {
39
+ printJson({ error: msg });
40
+ return;
41
+ }
42
+ console.error(chalk.red("\u2717") + " " + msg);
43
+ }
44
+ function banner() {
45
+ if (jsonMode) return;
46
+ console.log("");
47
+ console.log(" " + chalk.bold("Letter") + chalk.red(".") + " " + chalk.dim("CLI"));
48
+ console.log("");
49
+ }
50
+ function spinner(text) {
51
+ return ora({ text, color: "cyan", isEnabled: !jsonMode });
52
+ }
53
+ function prompt(question) {
54
+ if (!process.stdin.isTTY) return Promise.resolve("");
55
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
56
+ return new Promise((resolve) => {
57
+ rl.question(question, (answer) => {
58
+ rl.close();
59
+ resolve(answer.trim());
60
+ });
61
+ });
62
+ }
63
+ var c = chalk;
64
+
65
+ // src/config.ts
66
+ import Conf from "conf";
67
+ import { homedir } from "os";
68
+ import path from "path";
69
+ import { mkdir, readFile, writeFile, rm } from "fs/promises";
70
+ var DEFAULT_BASE_URL = "https://api.letter.app";
71
+ var config = new Conf({
72
+ projectName: "letterapp-cli",
73
+ schema: {
74
+ baseUrl: { type: "string", default: DEFAULT_BASE_URL }
75
+ }
76
+ });
77
+ function getBaseUrl(flag) {
78
+ const raw = flag || process.env.LETTER_BASE_URL || config.get("baseUrl") || DEFAULT_BASE_URL;
79
+ return raw.replace(/\/$/, "");
80
+ }
81
+ function setBaseUrl(url) {
82
+ config.set("baseUrl", url.replace(/\/$/, ""));
83
+ }
84
+ function getConfigPath() {
85
+ return config.path;
86
+ }
87
+ function resetConfig() {
88
+ config.clear();
89
+ }
90
+ function credentialsPath() {
91
+ return path.join(homedir(), ".letter", "credentials.json");
92
+ }
93
+ async function saveCredential(cred) {
94
+ const file = credentialsPath();
95
+ await mkdir(path.dirname(file), { recursive: true, mode: 448 });
96
+ await writeFile(file, `${JSON.stringify(cred, null, 2)}
97
+ `, { mode: 384 });
98
+ return file;
99
+ }
100
+ async function readCredential() {
101
+ try {
102
+ const raw = await readFile(credentialsPath(), "utf8");
103
+ return JSON.parse(raw);
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+ async function clearCredential() {
109
+ await rm(credentialsPath(), { force: true });
110
+ }
111
+
112
+ // src/client.ts
113
+ var USER_AGENT = "@letterapp/cli";
114
+ async function startDeviceAuth(base) {
115
+ const res = await fetch(`${base}/v1/cli/auth/start`, {
116
+ method: "POST",
117
+ headers: { "content-type": "application/json", "user-agent": USER_AGENT },
118
+ body: "{}"
119
+ });
120
+ if (!res.ok) {
121
+ throw new Error(
122
+ `Could not start login (HTTP ${res.status}). Is ${base} reachable?`
123
+ );
124
+ }
125
+ return await res.json();
126
+ }
127
+ async function pollDeviceAuth(base, deviceCode) {
128
+ const res = await fetch(`${base}/v1/cli/auth/poll`, {
129
+ method: "POST",
130
+ headers: { "content-type": "application/json", "user-agent": USER_AGENT },
131
+ body: JSON.stringify({ device_code: deviceCode })
132
+ });
133
+ if (res.status === 429) {
134
+ const retryAfter = Number(res.headers.get("retry-after") ?? "5");
135
+ return { status: "slow_down", retryAfter };
136
+ }
137
+ if (!res.ok) {
138
+ throw new Error(`Login poll failed (HTTP ${res.status}).`);
139
+ }
140
+ return await res.json();
141
+ }
142
+ var LetterClient = class {
143
+ maxRetries = 3;
144
+ async resolveAuth() {
145
+ const cred = await readCredential();
146
+ const token = process.env.LETTER_API_KEY || cred?.apiKey || "";
147
+ if (!token) {
148
+ throw new Error(
149
+ "Not connected. Run `letter login` (or set LETTER_API_KEY) first."
150
+ );
151
+ }
152
+ const base = getBaseUrl(process.env.LETTER_API_KEY ? void 0 : cred?.baseUrl);
153
+ return { base, token };
154
+ }
155
+ async request(method, path3, attempt = 1) {
156
+ const { base, token } = await this.resolveAuth();
157
+ const res = await fetch(`${base}${path3}`, {
158
+ method,
159
+ headers: {
160
+ authorization: `Bearer ${token}`,
161
+ accept: "application/json",
162
+ "user-agent": USER_AGENT
163
+ }
164
+ });
165
+ if (res.status === 429 && attempt <= this.maxRetries) {
166
+ const retryAfter = Number(res.headers.get("retry-after") || 2);
167
+ await new Promise((r) => setTimeout(r, retryAfter * 1e3 * attempt));
168
+ return this.request(method, path3, attempt + 1);
169
+ }
170
+ const data = res.status === 204 ? {} : await res.json();
171
+ if (!res.ok) {
172
+ const msg = data?.error?.message || `HTTP ${res.status}`;
173
+ const err = new Error(msg);
174
+ err.status = res.status;
175
+ throw err;
176
+ }
177
+ return { status: res.status, data };
178
+ }
179
+ get(path3) {
180
+ return this.request("GET", path3);
181
+ }
182
+ };
183
+ var client = new LetterClient();
184
+
185
+ // src/browser.ts
186
+ import { spawn } from "child_process";
187
+ function openUrl(url) {
188
+ const platform = process.platform;
189
+ let command;
190
+ let args;
191
+ if (platform === "darwin") {
192
+ command = "open";
193
+ args = [url];
194
+ } else if (platform === "win32") {
195
+ command = "cmd";
196
+ args = ["/c", "start", "", url];
197
+ } else {
198
+ command = "xdg-open";
199
+ args = [url];
200
+ }
201
+ try {
202
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
203
+ child.on("error", () => {
204
+ });
205
+ child.unref();
206
+ return true;
207
+ } catch {
208
+ return false;
209
+ }
210
+ }
211
+
212
+ // src/env-file.ts
213
+ import path2 from "path";
214
+ import { readFile as readFile2, writeFile as writeFile2, stat } from "fs/promises";
215
+ async function upsertEnv(cwd, file, entries) {
216
+ const filePath = path2.join(cwd, file);
217
+ let contents = "";
218
+ try {
219
+ contents = await readFile2(filePath, "utf8");
220
+ } catch {
221
+ contents = "";
222
+ }
223
+ let next = contents;
224
+ for (const [key, value] of Object.entries(entries)) {
225
+ const line = `${key}=${value}`;
226
+ const re = new RegExp(`^${escapeRegExp(key)}=.*$`, "m");
227
+ if (re.test(next)) {
228
+ next = next.replace(re, line);
229
+ } else {
230
+ if (next.length && !next.endsWith("\n")) next += "\n";
231
+ next += `${line}
232
+ `;
233
+ }
234
+ }
235
+ await writeFile2(filePath, next, "utf8");
236
+ return filePath;
237
+ }
238
+ async function fileExists(cwd, name) {
239
+ try {
240
+ await stat(path2.join(cwd, name));
241
+ return true;
242
+ } catch {
243
+ return false;
244
+ }
245
+ }
246
+ function escapeRegExp(s) {
247
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
248
+ }
249
+
250
+ // src/pm.ts
251
+ import { spawn as spawn2 } from "child_process";
252
+ import { readFile as readFile3 } from "fs/promises";
253
+ async function detectPackageManager(cwd) {
254
+ if (await fileExists(cwd, "pnpm-lock.yaml")) return "pnpm";
255
+ if (await fileExists(cwd, "yarn.lock")) return "yarn";
256
+ if (await fileExists(cwd, "bun.lockb")) return "bun";
257
+ if (await fileExists(cwd, "package-lock.json")) return "npm";
258
+ const ua = process.env.npm_config_user_agent ?? "";
259
+ if (ua.startsWith("pnpm")) return "pnpm";
260
+ if (ua.startsWith("yarn")) return "yarn";
261
+ if (ua.startsWith("bun")) return "bun";
262
+ return "npm";
263
+ }
264
+ async function detectFramework(cwd) {
265
+ try {
266
+ const pkg = JSON.parse(await readFile3(`${cwd}/package.json`, "utf8"));
267
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
268
+ if (deps.next) return "Next.js";
269
+ if (deps.nuxt) return "Nuxt";
270
+ if (deps["@remix-run/node"] || deps["@remix-run/react"]) return "Remix";
271
+ if (deps.express) return "Express";
272
+ if (deps.fastify) return "Fastify";
273
+ if (deps.hono) return "Hono";
274
+ if (deps["@sveltejs/kit"]) return "SvelteKit";
275
+ return null;
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+ function installCommand(pm, pkg) {
281
+ switch (pm) {
282
+ case "pnpm":
283
+ return `pnpm add ${pkg}`;
284
+ case "yarn":
285
+ return `yarn add ${pkg}`;
286
+ case "bun":
287
+ return `bun add ${pkg}`;
288
+ default:
289
+ return `npm install ${pkg}`;
290
+ }
291
+ }
292
+ function runInstall(pm, pkg, cwd) {
293
+ const args = pm === "npm" ? ["install", pkg] : ["add", pkg];
294
+ return new Promise((resolve) => {
295
+ const child = spawn2(pm, args, {
296
+ cwd,
297
+ stdio: "inherit",
298
+ shell: process.platform === "win32"
299
+ });
300
+ child.on("error", () => resolve(1));
301
+ child.on("close", (code) => resolve(code ?? 1));
302
+ });
303
+ }
304
+
305
+ // src/commands/login.ts
306
+ var SDK_PACKAGE = "@letterapp/node";
307
+ var ENV_FILE = ".env.local";
308
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
309
+ function registerLoginCommand(program2) {
310
+ program2.command("login", { isDefault: true }).alias("init").description("Connect this project to Letter (interactive device login)").option("--no-open", "Don't open the browser; print the URL to open manually").option("-y, --yes", "Non-interactive: don't wait for Enter (agents/CI)").option("--no-install", "Skip installing the SDK").option("--base-url <url>", "Target a self-hosted or local Letter instance").option(
311
+ "--api-key <key>",
312
+ "CI only: write a key without the device flow (never use in chat)"
313
+ ).action(async (opts) => {
314
+ const code = await runLogin(opts);
315
+ if (code !== 0) process.exitCode = code;
316
+ });
317
+ }
318
+ async function runLogin(opts) {
319
+ const base = getBaseUrl(opts.baseUrl);
320
+ const cwd = process.cwd();
321
+ const interactive = process.stdin.isTTY && !opts.yes;
322
+ banner();
323
+ if (opts.apiKey) {
324
+ const entries = { LETTER_API_KEY: opts.apiKey };
325
+ if (base !== DEFAULT_BASE_URL) entries.LETTER_BASE_URL = base;
326
+ const file = await upsertEnv(cwd, ENV_FILE, entries);
327
+ printSuccess(`Saved LETTER_API_KEY to ${rel(cwd, file)} (--api-key).`);
328
+ printWarning("--api-key is for CI. For interactive setup, run `letter login`.");
329
+ return 0;
330
+ }
331
+ let flow;
332
+ try {
333
+ flow = await startDeviceAuth(base);
334
+ } catch (err) {
335
+ printError(err);
336
+ return 1;
337
+ }
338
+ log(c.bold("Confirm this code in your browser:"));
339
+ log();
340
+ log(" " + c.cyan(c.bold(flow.user_code)));
341
+ log();
342
+ if (interactive && opts.open) {
343
+ await prompt(c.dim("Press Enter to open your browser\u2026 "));
344
+ }
345
+ if (opts.open) openUrl(flow.verification_uri_complete);
346
+ printInfo("If your browser didn't open, visit:");
347
+ log(" " + c.blue(flow.verification_uri_complete));
348
+ log();
349
+ printInfo("Waiting for you to approve\u2026 (Ctrl+C to cancel)");
350
+ const deadline = Date.now() + flow.expires_in * 1e3;
351
+ let intervalMs = Math.max(1, flow.interval) * 1e3;
352
+ while (Date.now() < deadline) {
353
+ await sleep(intervalMs);
354
+ let res;
355
+ try {
356
+ res = await pollDeviceAuth(base, flow.device_code);
357
+ } catch (err) {
358
+ printError(err);
359
+ return 1;
360
+ }
361
+ if (res.status === "authorization_pending") continue;
362
+ if (res.status === "slow_down") {
363
+ intervalMs += 1e3;
364
+ continue;
365
+ }
366
+ if (res.status === "access_denied") {
367
+ printError(new Error("Request denied in the browser. Nothing was changed."));
368
+ return 1;
369
+ }
370
+ if (res.status === "expired_token") {
371
+ printError(new Error("This login expired. Run the command again to retry."));
372
+ return 1;
373
+ }
374
+ return finish(res.api_key, res.base_url, res.project, cwd, opts.install);
375
+ }
376
+ printError(new Error("Timed out waiting for approval. Run the command again."));
377
+ return 1;
378
+ }
379
+ async function finish(apiKey, baseUrl, project, cwd, doInstall) {
380
+ log();
381
+ printSuccess(`Approved for project ${c.bold(project.name)}.`);
382
+ const entries = { LETTER_API_KEY: apiKey };
383
+ if (baseUrl && baseUrl !== DEFAULT_BASE_URL) entries.LETTER_BASE_URL = baseUrl;
384
+ const envFile = await upsertEnv(cwd, ENV_FILE, entries);
385
+ printSuccess(`Saved LETTER_API_KEY to ${rel(cwd, envFile)}.`);
386
+ try {
387
+ const credFile = await saveCredential({
388
+ apiKey,
389
+ baseUrl: baseUrl || DEFAULT_BASE_URL,
390
+ project,
391
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
392
+ });
393
+ printSuccess(`Stored credentials in ${tildify(credFile)} for tooling (MCP).`);
394
+ } catch {
395
+ printWarning("Could not write ~/.letter/credentials.json (continuing).");
396
+ }
397
+ const pm = await detectPackageManager(cwd);
398
+ if (doInstall) {
399
+ printInfo(`Installing ${SDK_PACKAGE} with ${pm}\u2026`);
400
+ const code = await runInstall(pm, SDK_PACKAGE, cwd);
401
+ if (code === 0) printSuccess(`Installed ${SDK_PACKAGE}.`);
402
+ else printWarning(`Install failed. Run: ${installCommand(pm, SDK_PACKAGE)}`);
403
+ } else {
404
+ printInfo(`Skipped install. Run: ${installCommand(pm, SDK_PACKAGE)}`);
405
+ }
406
+ printNextSteps(await detectFramework(cwd));
407
+ return 0;
408
+ }
409
+ function printNextSteps(framework) {
410
+ log();
411
+ log(c.bold("Next steps"));
412
+ if (framework) log(c.dim(`Detected ${framework}.`));
413
+ log(` 1. Create a server-side client that reads ${c.cyan("process.env.LETTER_API_KEY")}.`);
414
+ log(` 2. Call ${c.cyan("letter.identify(...)")} where users sign up or log in.`);
415
+ log(` 3. Call ${c.cyan("letter.track(...)")} on 2-3 key actions.`);
416
+ log();
417
+ log(c.dim("Check it landed: ") + c.bold("letter status"));
418
+ log(c.dim("Your API key is in .env.local - keep it out of source control."));
419
+ log();
420
+ }
421
+ function rel(cwd, file) {
422
+ return file.startsWith(cwd) ? file.slice(cwd.length + 1) || file : file;
423
+ }
424
+ function tildify(file) {
425
+ const home = process.env.HOME || process.env.USERPROFILE;
426
+ return home && file.startsWith(home) ? `~${file.slice(home.length)}` : file;
427
+ }
428
+
429
+ // src/commands/auth.ts
430
+ function mask(key) {
431
+ return key.length > 12 ? key.slice(0, 8) + "\u2026" + key.slice(-4) : "set";
432
+ }
433
+ function registerAuthCommands(program2) {
434
+ const auth = program2.command("auth").description("Manage the stored Letter connection");
435
+ auth.command("status").description("Show whether this machine is connected to Letter").action(async () => {
436
+ const cred = await readCredential();
437
+ const envKey = process.env.LETTER_API_KEY;
438
+ if (!cred && !envKey) {
439
+ if (isJsonMode()) printJson({ connected: false });
440
+ else printInfo("Not connected. Run " + c.bold("letter login") + " to set up.");
441
+ return;
442
+ }
443
+ if (isJsonMode()) {
444
+ printJson({
445
+ connected: true,
446
+ source: envKey ? "env" : "credentials",
447
+ project: cred?.project ?? null,
448
+ base_url: cred?.baseUrl ?? null,
449
+ key: envKey ? "env" : cred ? mask(cred.apiKey) : null
450
+ });
451
+ return;
452
+ }
453
+ printSuccess("Connected" + (cred ? ` to ${c.bold(cred.project.name)}` : ""));
454
+ if (cred) {
455
+ console.log(c.dim(" Project: ") + cred.project.slug);
456
+ console.log(c.dim(" Key: ") + mask(cred.apiKey));
457
+ console.log(c.dim(" API: ") + cred.baseUrl);
458
+ }
459
+ if (envKey) console.log(c.dim(" LETTER_API_KEY is set in the environment."));
460
+ });
461
+ auth.command("logout").description("Remove the stored credential (~/.letter/credentials.json)").action(async () => {
462
+ await clearCredential();
463
+ printSuccess("Removed stored credential.");
464
+ });
465
+ }
466
+
467
+ // src/commands/status.ts
468
+ function registerStatusCommand(program2) {
469
+ program2.command("status").description("Check whether your project has received any contacts or events").action(async () => {
470
+ const spin = spinner("Checking your Letter project\u2026").start();
471
+ try {
472
+ const { data } = await client.get("/v1/status");
473
+ spin.stop();
474
+ if (isJsonMode()) {
475
+ printJson(data);
476
+ return;
477
+ }
478
+ const cred = await readCredential();
479
+ const contacts = data.contacts ?? 0;
480
+ const events = data.events ?? 0;
481
+ if (cred) printInfo(`Project ${c.bold(cred.project.name)}`);
482
+ if (contacts > 0 || events > 0) {
483
+ printSuccess(`Connected. ${contacts} contact(s), ${events} event(s) received.`);
484
+ } else {
485
+ printInfo("Connected, but no data yet. Fire your first identify/track call.");
486
+ }
487
+ } catch (err) {
488
+ spin.stop();
489
+ printError(err);
490
+ process.exitCode = 1;
491
+ }
492
+ });
493
+ }
494
+
495
+ // src/commands/config.ts
496
+ function registerConfigCommands(program2) {
497
+ const cfg = program2.command("config").description("Manage CLI configuration");
498
+ cfg.command("set").description("Set a config value").argument("<key>", "Config key: base-url").argument("<value>", "Value to set").action((key, value) => {
499
+ switch (key) {
500
+ case "base-url":
501
+ setBaseUrl(value);
502
+ printSuccess(`Base URL set to ${getBaseUrl()}`);
503
+ break;
504
+ default:
505
+ printError(new Error(`Unknown config key: ${key}. Valid keys: base-url`));
506
+ process.exitCode = 1;
507
+ }
508
+ });
509
+ cfg.command("get").description("Show CLI configuration").argument("[key]", "Config key (omit to show all)").action((key) => {
510
+ const all = { base_url: getBaseUrl(), config_path: getConfigPath() };
511
+ if (key && key !== "base-url" && key !== "path") {
512
+ printError(new Error(`Unknown config key: ${key}`));
513
+ process.exitCode = 1;
514
+ return;
515
+ }
516
+ if (isJsonMode()) {
517
+ printJson(key === "base-url" ? { base_url: all.base_url } : key === "path" ? { config_path: all.config_path } : all);
518
+ return;
519
+ }
520
+ if (key === "base-url") console.log(all.base_url);
521
+ else if (key === "path") console.log(all.config_path);
522
+ else for (const [k, v] of Object.entries(all)) console.log(c.bold(k + ":") + " " + v);
523
+ });
524
+ cfg.command("reset").description("Reset CLI configuration to defaults").action(() => {
525
+ resetConfig();
526
+ printSuccess("Configuration reset.");
527
+ });
528
+ }
529
+
530
+ // src/index.ts
531
+ var program = new Command();
532
+ program.name("letter").description("Connect your app to Letter, then manage it from the command line").version("0.2.0").option("--json", "Output raw JSON (for scripting / agents)").hook("preAction", (thisCommand) => {
533
+ if (thisCommand.opts().json) setJsonMode(true);
56
534
  });
535
+ registerLoginCommand(program);
536
+ registerAuthCommands(program);
537
+ registerStatusCommand(program);
538
+ registerConfigCommands(program);
539
+ program.parseAsync();
540
+ //# sourceMappingURL=index.js.map