@letterapp/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -17
- package/dist/index.js +1020 -25
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -60,6 +60,30 @@ function prompt(question) {
|
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
|
+
function emit(data) {
|
|
64
|
+
if (jsonMode) {
|
|
65
|
+
printJson(data);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (data && typeof data === "object" && Array.isArray(data.data)) {
|
|
69
|
+
const env = data;
|
|
70
|
+
if (env.data.length === 0) {
|
|
71
|
+
console.log(chalk.dim("(none)"));
|
|
72
|
+
} else {
|
|
73
|
+
for (const row of env.data) {
|
|
74
|
+
const id = String(row.id ?? row.slug ?? row.external_id ?? "");
|
|
75
|
+
const label = row.name ?? row.email ?? row.domain ?? row.subject ?? "";
|
|
76
|
+
console.log(`${chalk.bold(id)}${label ? " " + label : ""}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (env.next_cursor) {
|
|
80
|
+
console.log(chalk.dim(`
|
|
81
|
+
\u203A more: --cursor ${env.next_cursor}`));
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
printJson(data);
|
|
86
|
+
}
|
|
63
87
|
var c = chalk;
|
|
64
88
|
|
|
65
89
|
// src/config.ts
|
|
@@ -87,6 +111,15 @@ function getConfigPath() {
|
|
|
87
111
|
function resetConfig() {
|
|
88
112
|
config.clear();
|
|
89
113
|
}
|
|
114
|
+
async function resolveProjectSlug(flag) {
|
|
115
|
+
if (flag) return flag;
|
|
116
|
+
if (process.env.LETTER_PROJECT) return process.env.LETTER_PROJECT;
|
|
117
|
+
const cred = await readCredential();
|
|
118
|
+
if (cred?.project?.slug) return cred.project.slug;
|
|
119
|
+
throw new Error(
|
|
120
|
+
"No project. Pass --project <slug>, set LETTER_PROJECT, or run `letter login`."
|
|
121
|
+
);
|
|
122
|
+
}
|
|
90
123
|
function credentialsPath() {
|
|
91
124
|
return path.join(homedir(), ".letter", "credentials.json");
|
|
92
125
|
}
|
|
@@ -181,6 +214,78 @@ var LetterClient = class {
|
|
|
181
214
|
}
|
|
182
215
|
};
|
|
183
216
|
var client = new LetterClient();
|
|
217
|
+
var ManagementClient = class {
|
|
218
|
+
maxRetries = 3;
|
|
219
|
+
async resolveAuth() {
|
|
220
|
+
const cred = await readCredential();
|
|
221
|
+
const token = process.env.LETTER_PAT || cred?.pat || "";
|
|
222
|
+
if (!token) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
"Not connected for management. Run `letter login` (or set LETTER_PAT) first."
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const base = getBaseUrl(process.env.LETTER_PAT ? void 0 : cred?.baseUrl);
|
|
228
|
+
return { base, token };
|
|
229
|
+
}
|
|
230
|
+
async request(method, path3, init = {}, attempt = 1) {
|
|
231
|
+
const { base, token } = await this.resolveAuth();
|
|
232
|
+
let url = `${base}${path3}`;
|
|
233
|
+
if (init.query) {
|
|
234
|
+
const qs = new URLSearchParams();
|
|
235
|
+
for (const [k, v] of Object.entries(init.query)) {
|
|
236
|
+
if (v !== void 0 && v !== "") qs.set(k, String(v));
|
|
237
|
+
}
|
|
238
|
+
const s = qs.toString();
|
|
239
|
+
if (s) url += `?${s}`;
|
|
240
|
+
}
|
|
241
|
+
const headers = {
|
|
242
|
+
authorization: `Bearer ${token}`,
|
|
243
|
+
accept: "application/json",
|
|
244
|
+
"user-agent": USER_AGENT
|
|
245
|
+
};
|
|
246
|
+
let body;
|
|
247
|
+
if (init.form) {
|
|
248
|
+
body = init.form;
|
|
249
|
+
} else if (init.body !== void 0) {
|
|
250
|
+
headers["content-type"] = "application/json";
|
|
251
|
+
body = JSON.stringify(init.body);
|
|
252
|
+
}
|
|
253
|
+
const res = await fetch(url, { method, headers, body });
|
|
254
|
+
if (res.status === 429 && attempt <= this.maxRetries) {
|
|
255
|
+
const retryAfter = Number(res.headers.get("retry-after") || 2);
|
|
256
|
+
await new Promise((r) => setTimeout(r, retryAfter * 1e3 * attempt));
|
|
257
|
+
return this.request(method, path3, init, attempt + 1);
|
|
258
|
+
}
|
|
259
|
+
const text = await res.text();
|
|
260
|
+
const data = text ? JSON.parse(text) : {};
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
const msg = data?.error?.message || `HTTP ${res.status}`;
|
|
263
|
+
const err = new Error(msg);
|
|
264
|
+
err.status = res.status;
|
|
265
|
+
throw err;
|
|
266
|
+
}
|
|
267
|
+
return { status: res.status, data };
|
|
268
|
+
}
|
|
269
|
+
get(path3, query) {
|
|
270
|
+
return this.request("GET", path3, { query });
|
|
271
|
+
}
|
|
272
|
+
post(path3, body) {
|
|
273
|
+
return this.request("POST", path3, { body });
|
|
274
|
+
}
|
|
275
|
+
postForm(path3, form) {
|
|
276
|
+
return this.request("POST", path3, { form });
|
|
277
|
+
}
|
|
278
|
+
patch(path3, body) {
|
|
279
|
+
return this.request("PATCH", path3, { body });
|
|
280
|
+
}
|
|
281
|
+
put(path3, body) {
|
|
282
|
+
return this.request("PUT", path3, { body });
|
|
283
|
+
}
|
|
284
|
+
delete(path3) {
|
|
285
|
+
return this.request("DELETE", path3);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
var mgmt = new ManagementClient();
|
|
184
289
|
|
|
185
290
|
// src/browser.ts
|
|
186
291
|
import { spawn } from "child_process";
|
|
@@ -249,7 +354,7 @@ function escapeRegExp(s) {
|
|
|
249
354
|
|
|
250
355
|
// src/pm.ts
|
|
251
356
|
import { spawn as spawn2 } from "child_process";
|
|
252
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
357
|
+
import { readFile as readFile3, readdir } from "fs/promises";
|
|
253
358
|
async function detectPackageManager(cwd) {
|
|
254
359
|
if (await fileExists(cwd, "pnpm-lock.yaml")) return "pnpm";
|
|
255
360
|
if (await fileExists(cwd, "yarn.lock")) return "yarn";
|
|
@@ -277,6 +382,121 @@ async function detectFramework(cwd) {
|
|
|
277
382
|
return null;
|
|
278
383
|
}
|
|
279
384
|
}
|
|
385
|
+
async function detectStack(cwd) {
|
|
386
|
+
if (await fileExists(cwd, "package.json")) {
|
|
387
|
+
return { runtime: "node", framework: await detectFramework(cwd) };
|
|
388
|
+
}
|
|
389
|
+
if (await fileExists(cwd, "pyproject.toml") || await fileExists(cwd, "requirements.txt") || await fileExists(cwd, "Pipfile") || await fileExists(cwd, "setup.py")) {
|
|
390
|
+
return { runtime: "python", framework: await detectPythonFramework(cwd) };
|
|
391
|
+
}
|
|
392
|
+
if (await fileExists(cwd, "Gemfile") || await hasGemspec(cwd)) {
|
|
393
|
+
return { runtime: "ruby", framework: await detectRubyFramework(cwd) };
|
|
394
|
+
}
|
|
395
|
+
if (await fileExists(cwd, "go.mod")) return { runtime: "go", framework: null };
|
|
396
|
+
if (await fileExists(cwd, "composer.json")) {
|
|
397
|
+
return { runtime: "php", framework: await detectPhpFramework(cwd) };
|
|
398
|
+
}
|
|
399
|
+
return { runtime: "other", framework: null };
|
|
400
|
+
}
|
|
401
|
+
function stackLabel(stack) {
|
|
402
|
+
if (stack.framework) return stack.framework;
|
|
403
|
+
switch (stack.runtime) {
|
|
404
|
+
case "node":
|
|
405
|
+
return "a Node.js project";
|
|
406
|
+
case "python":
|
|
407
|
+
return "a Python project";
|
|
408
|
+
case "ruby":
|
|
409
|
+
return "a Ruby project";
|
|
410
|
+
case "go":
|
|
411
|
+
return "a Go project";
|
|
412
|
+
case "php":
|
|
413
|
+
return "a PHP project";
|
|
414
|
+
default:
|
|
415
|
+
return "your project";
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function languageName(stack) {
|
|
419
|
+
switch (stack.runtime) {
|
|
420
|
+
case "node":
|
|
421
|
+
return "TypeScript";
|
|
422
|
+
case "python":
|
|
423
|
+
return "Python";
|
|
424
|
+
case "ruby":
|
|
425
|
+
return "Ruby";
|
|
426
|
+
case "go":
|
|
427
|
+
return "Go";
|
|
428
|
+
case "php":
|
|
429
|
+
return "PHP";
|
|
430
|
+
default:
|
|
431
|
+
return "your language";
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function readIf(cwd, name) {
|
|
435
|
+
try {
|
|
436
|
+
return await readFile3(`${cwd}/${name}`, "utf8");
|
|
437
|
+
} catch {
|
|
438
|
+
return "";
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async function hasGemspec(cwd) {
|
|
442
|
+
try {
|
|
443
|
+
return (await readdir(cwd)).some((f) => f.endsWith(".gemspec"));
|
|
444
|
+
} catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function detectPythonFramework(cwd) {
|
|
449
|
+
const txt = (await readIf(cwd, "requirements.txt") + await readIf(cwd, "pyproject.toml") + await readIf(cwd, "Pipfile")).toLowerCase();
|
|
450
|
+
if (txt.includes("django")) return "Django";
|
|
451
|
+
if (txt.includes("fastapi")) return "FastAPI";
|
|
452
|
+
if (txt.includes("flask")) return "Flask";
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
async function detectRubyFramework(cwd) {
|
|
456
|
+
const txt = (await readIf(cwd, "Gemfile")).toLowerCase();
|
|
457
|
+
if (txt.includes("rails")) return "Rails";
|
|
458
|
+
if (txt.includes("sinatra")) return "Sinatra";
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
async function detectPhpFramework(cwd) {
|
|
462
|
+
const txt = (await readIf(cwd, "composer.json")).toLowerCase();
|
|
463
|
+
if (txt.includes("laravel")) return "Laravel";
|
|
464
|
+
if (txt.includes("symfony")) return "Symfony";
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
var SDK_PYPI = "letterapp";
|
|
468
|
+
var SDK_GEM = "letterapp";
|
|
469
|
+
function sdkInstall(cmd, args) {
|
|
470
|
+
return { cmd, args, display: `${cmd} ${args.join(" ")}` };
|
|
471
|
+
}
|
|
472
|
+
async function pythonSdkInstall(cwd) {
|
|
473
|
+
if (await fileExists(cwd, "uv.lock")) return sdkInstall("uv", ["add", SDK_PYPI]);
|
|
474
|
+
const pyproject = (await readIf(cwd, "pyproject.toml")).toLowerCase();
|
|
475
|
+
if (await fileExists(cwd, "poetry.lock") || pyproject.includes("[tool.poetry]")) {
|
|
476
|
+
return sdkInstall("poetry", ["add", SDK_PYPI]);
|
|
477
|
+
}
|
|
478
|
+
if (await fileExists(cwd, "Pipfile")) {
|
|
479
|
+
return sdkInstall("pipenv", ["install", SDK_PYPI]);
|
|
480
|
+
}
|
|
481
|
+
return sdkInstall("pip", ["install", SDK_PYPI]);
|
|
482
|
+
}
|
|
483
|
+
async function rubySdkInstall(cwd) {
|
|
484
|
+
if (await fileExists(cwd, "Gemfile")) {
|
|
485
|
+
return sdkInstall("bundle", ["add", SDK_GEM]);
|
|
486
|
+
}
|
|
487
|
+
return sdkInstall("gem", ["install", SDK_GEM]);
|
|
488
|
+
}
|
|
489
|
+
function runSdkInstall(install, cwd) {
|
|
490
|
+
return new Promise((resolve) => {
|
|
491
|
+
const child = spawn2(install.cmd, install.args, {
|
|
492
|
+
cwd,
|
|
493
|
+
stdio: "inherit",
|
|
494
|
+
shell: process.platform === "win32"
|
|
495
|
+
});
|
|
496
|
+
child.on("error", () => resolve(1));
|
|
497
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
498
|
+
});
|
|
499
|
+
}
|
|
280
500
|
function installCommand(pm, pkg) {
|
|
281
501
|
switch (pm) {
|
|
282
502
|
case "pnpm":
|
|
@@ -304,8 +524,10 @@ function runInstall(pm, pkg, cwd) {
|
|
|
304
524
|
|
|
305
525
|
// src/commands/login.ts
|
|
306
526
|
var SDK_PACKAGE = "@letterapp/node";
|
|
307
|
-
var ENV_FILE = ".env.local";
|
|
308
527
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
528
|
+
function envFileFor(stack) {
|
|
529
|
+
return stack.runtime === "node" ? ".env.local" : ".env";
|
|
530
|
+
}
|
|
309
531
|
function registerLoginCommand(program2) {
|
|
310
532
|
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
533
|
"--api-key <key>",
|
|
@@ -321,9 +543,10 @@ async function runLogin(opts) {
|
|
|
321
543
|
const interactive = process.stdin.isTTY && !opts.yes;
|
|
322
544
|
banner();
|
|
323
545
|
if (opts.apiKey) {
|
|
546
|
+
const envFile = envFileFor(await detectStack(cwd));
|
|
324
547
|
const entries = { LETTER_API_KEY: opts.apiKey };
|
|
325
548
|
if (base !== DEFAULT_BASE_URL) entries.LETTER_BASE_URL = base;
|
|
326
|
-
const file = await upsertEnv(cwd,
|
|
549
|
+
const file = await upsertEnv(cwd, envFile, entries);
|
|
327
550
|
printSuccess(`Saved LETTER_API_KEY to ${rel(cwd, file)} (--api-key).`);
|
|
328
551
|
printWarning("--api-key is for CI. For interactive setup, run `letter login`.");
|
|
329
552
|
return 0;
|
|
@@ -371,51 +594,242 @@ async function runLogin(opts) {
|
|
|
371
594
|
printError(new Error("This login expired. Run the command again to retry."));
|
|
372
595
|
return 1;
|
|
373
596
|
}
|
|
374
|
-
return finish(res
|
|
597
|
+
return finish(res, cwd, opts.install);
|
|
375
598
|
}
|
|
376
599
|
printError(new Error("Timed out waiting for approval. Run the command again."));
|
|
377
600
|
return 1;
|
|
378
601
|
}
|
|
379
|
-
async function finish(
|
|
602
|
+
async function finish(approved, cwd, doInstall) {
|
|
603
|
+
const { api_key: apiKey, pat, base_url: baseUrl, project, workspace } = approved;
|
|
380
604
|
log();
|
|
381
605
|
printSuccess(`Approved for project ${c.bold(project.name)}.`);
|
|
606
|
+
const stack = await detectStack(cwd);
|
|
607
|
+
const isNode = stack.runtime === "node";
|
|
608
|
+
const envFile = envFileFor(stack);
|
|
382
609
|
const entries = { LETTER_API_KEY: apiKey };
|
|
383
610
|
if (baseUrl && baseUrl !== DEFAULT_BASE_URL) entries.LETTER_BASE_URL = baseUrl;
|
|
384
|
-
const
|
|
385
|
-
printSuccess(`Saved LETTER_API_KEY to ${rel(cwd,
|
|
611
|
+
const written = await upsertEnv(cwd, envFile, entries);
|
|
612
|
+
printSuccess(`Saved LETTER_API_KEY to ${rel(cwd, written)}.`);
|
|
386
613
|
try {
|
|
387
614
|
const credFile = await saveCredential({
|
|
388
615
|
apiKey,
|
|
616
|
+
pat,
|
|
389
617
|
baseUrl: baseUrl || DEFAULT_BASE_URL,
|
|
390
618
|
project,
|
|
619
|
+
workspace,
|
|
391
620
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
392
621
|
});
|
|
393
|
-
printSuccess(`Stored credentials in ${tildify(credFile)} for tooling (MCP).`);
|
|
622
|
+
printSuccess(`Stored credentials in ${tildify(credFile)} for tooling (MCP + CLI).`);
|
|
394
623
|
} catch {
|
|
395
624
|
printWarning("Could not write ~/.letter/credentials.json (continuing).");
|
|
396
625
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
626
|
+
if (isNode) {
|
|
627
|
+
const pm = await detectPackageManager(cwd);
|
|
628
|
+
if (doInstall) {
|
|
629
|
+
printInfo(`Installing ${SDK_PACKAGE} with ${pm}\u2026`);
|
|
630
|
+
const code = await runInstall(pm, SDK_PACKAGE, cwd);
|
|
631
|
+
if (code === 0) printSuccess(`Installed ${SDK_PACKAGE}.`);
|
|
632
|
+
else printWarning(`Install failed. Run: ${installCommand(pm, SDK_PACKAGE)}`);
|
|
633
|
+
} else {
|
|
634
|
+
printInfo(`Skipped install. Run: ${installCommand(pm, SDK_PACKAGE)}`);
|
|
635
|
+
}
|
|
636
|
+
printNodeInstructions(stack.framework, envFile);
|
|
637
|
+
} else if (stack.runtime === "python" || stack.runtime === "ruby") {
|
|
638
|
+
const install = stack.runtime === "python" ? await pythonSdkInstall(cwd) : await rubySdkInstall(cwd);
|
|
639
|
+
await installSdk(install, doInstall);
|
|
640
|
+
if (stack.runtime === "python") {
|
|
641
|
+
printPythonInstructions(stack.framework, envFile);
|
|
642
|
+
} else {
|
|
643
|
+
printRubyInstructions(stack.framework, envFile);
|
|
644
|
+
}
|
|
403
645
|
} else {
|
|
404
|
-
printInfo(
|
|
646
|
+
printInfo(
|
|
647
|
+
`Detected ${stackLabel(stack)}. No ${languageName(stack)} SDK yet, so use the HTTP API - no package installed.`
|
|
648
|
+
);
|
|
649
|
+
printHttpInstructions(stack, envFile, baseUrl || DEFAULT_BASE_URL);
|
|
405
650
|
}
|
|
406
|
-
printNextSteps(await detectFramework(cwd));
|
|
407
651
|
return 0;
|
|
408
652
|
}
|
|
409
|
-
function
|
|
653
|
+
async function installSdk(install, doInstall) {
|
|
654
|
+
if (doInstall) {
|
|
655
|
+
printInfo(`Installing letterapp (${install.display})\u2026`);
|
|
656
|
+
const code = await runSdkInstall(install, process.cwd());
|
|
657
|
+
if (code === 0) printSuccess("Installed letterapp.");
|
|
658
|
+
else printWarning(`Install failed. Run: ${install.display}`);
|
|
659
|
+
} else {
|
|
660
|
+
printInfo(`Skipped install. Run: ${install.display}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function printNodeInstructions(framework, envFile) {
|
|
664
|
+
log();
|
|
665
|
+
log(c.bold("Next: hand these steps to your coding agent"));
|
|
666
|
+
log(
|
|
667
|
+
c.dim(
|
|
668
|
+
framework ? `Detected ${framework}. The SDK is installed and LETTER_API_KEY is in ${envFile}.` : `The SDK is installed and LETTER_API_KEY is in ${envFile}.`
|
|
669
|
+
)
|
|
670
|
+
);
|
|
671
|
+
log();
|
|
672
|
+
log(
|
|
673
|
+
c.bold(" 1. ") + `Create a server-side client (e.g. ${c.cyan("lib/letter.ts")}):`
|
|
674
|
+
);
|
|
675
|
+
log(c.dim(' import { Letter } from "@letterapp/node";'));
|
|
676
|
+
log(c.dim(" export const letter = new Letter({"));
|
|
677
|
+
log(c.dim(" apiKey: process.env.LETTER_API_KEY!,"));
|
|
678
|
+
log(
|
|
679
|
+
c.dim(
|
|
680
|
+
" ...(process.env.LETTER_BASE_URL ? { baseUrl: process.env.LETTER_BASE_URL } : {}),"
|
|
681
|
+
)
|
|
682
|
+
);
|
|
683
|
+
log(c.dim(" });"));
|
|
684
|
+
log();
|
|
685
|
+
log(
|
|
686
|
+
c.bold(" 2. ") + `Call ${c.cyan("identify()")} where users sign up or log in:`
|
|
687
|
+
);
|
|
688
|
+
log(
|
|
689
|
+
c.dim(
|
|
690
|
+
" await letter.identify({ userId: user.id, email: user.email });"
|
|
691
|
+
)
|
|
692
|
+
);
|
|
693
|
+
log();
|
|
694
|
+
log(
|
|
695
|
+
c.bold(" 3. ") + `Call ${c.cyan("track()")} on your 2-3 most important actions:`
|
|
696
|
+
);
|
|
697
|
+
log(
|
|
698
|
+
c.dim(' await letter.track({ userId: user.id, event: "Signed Up" });')
|
|
699
|
+
);
|
|
700
|
+
log();
|
|
701
|
+
log(
|
|
702
|
+
c.dim(
|
|
703
|
+
" In serverless handlers or scripts, await letter.flush() before returning."
|
|
704
|
+
)
|
|
705
|
+
);
|
|
706
|
+
log();
|
|
707
|
+
log(c.dim("Verify it landed: ") + c.bold("letter status"));
|
|
708
|
+
log(
|
|
709
|
+
c.dim(
|
|
710
|
+
`Your API key is in ${envFile} - keep it out of source control, and don't echo its value.`
|
|
711
|
+
)
|
|
712
|
+
);
|
|
713
|
+
log();
|
|
714
|
+
}
|
|
715
|
+
function printPythonInstructions(framework, envFile) {
|
|
716
|
+
log();
|
|
717
|
+
log(c.bold("Next: hand these steps to your coding agent"));
|
|
718
|
+
log(
|
|
719
|
+
c.dim(
|
|
720
|
+
framework ? `Detected ${framework}. The SDK is installed and LETTER_API_KEY is in ${envFile}.` : `The SDK is installed and LETTER_API_KEY is in ${envFile}.`
|
|
721
|
+
)
|
|
722
|
+
);
|
|
723
|
+
log();
|
|
724
|
+
log(
|
|
725
|
+
c.bold(" 1. ") + `Create a shared client (e.g. ${c.cyan("letter.py")}):`
|
|
726
|
+
);
|
|
727
|
+
log(c.dim(" import os"));
|
|
728
|
+
log(c.dim(" from letterapp import Letter"));
|
|
729
|
+
log(c.dim(' letter = Letter(api_key=os.environ["LETTER_API_KEY"])'));
|
|
730
|
+
log(c.dim(' # Self-hosted/local: pass base_url=os.environ["LETTER_BASE_URL"].'));
|
|
731
|
+
log();
|
|
732
|
+
log(
|
|
733
|
+
c.bold(" 2. ") + `Call ${c.cyan("identify()")} where users sign up or log in:`
|
|
734
|
+
);
|
|
735
|
+
log(c.dim(" letter.identify(user_id=user.id, email=user.email)"));
|
|
736
|
+
log();
|
|
737
|
+
log(
|
|
738
|
+
c.bold(" 3. ") + `Call ${c.cyan("track()")} on your 2-3 most important actions:`
|
|
739
|
+
);
|
|
740
|
+
log(c.dim(' letter.track(user_id=user.id, event="Signed Up")'));
|
|
741
|
+
log();
|
|
742
|
+
log(
|
|
743
|
+
c.dim(
|
|
744
|
+
" In serverless handlers or scripts, call letter.flush() (or letter.close()) before returning."
|
|
745
|
+
)
|
|
746
|
+
);
|
|
747
|
+
log();
|
|
748
|
+
log(c.dim("Verify it landed: ") + c.bold("letter status"));
|
|
749
|
+
log(
|
|
750
|
+
c.dim(
|
|
751
|
+
`Your API key is in ${envFile} - keep it out of source control, and don't echo its value.`
|
|
752
|
+
)
|
|
753
|
+
);
|
|
754
|
+
log();
|
|
755
|
+
}
|
|
756
|
+
function printRubyInstructions(framework, envFile) {
|
|
757
|
+
log();
|
|
758
|
+
log(c.bold("Next: hand these steps to your coding agent"));
|
|
759
|
+
log(
|
|
760
|
+
c.dim(
|
|
761
|
+
framework ? `Detected ${framework}. The SDK is installed and LETTER_API_KEY is in ${envFile}.` : `The SDK is installed and LETTER_API_KEY is in ${envFile}.`
|
|
762
|
+
)
|
|
763
|
+
);
|
|
764
|
+
log();
|
|
765
|
+
log(
|
|
766
|
+
c.bold(" 1. ") + `Create a shared client (in Rails, ${c.cyan("config/initializers/letter.rb")}):`
|
|
767
|
+
);
|
|
768
|
+
log(c.dim(' require "letterapp"'));
|
|
769
|
+
log(c.dim(' LETTER = Letterapp::Client.new(api_key: ENV["LETTER_API_KEY"])'));
|
|
770
|
+
log(c.dim(' # Self-hosted/local: pass base_url: ENV["LETTER_BASE_URL"].'));
|
|
771
|
+
log();
|
|
772
|
+
log(
|
|
773
|
+
c.bold(" 2. ") + `Call ${c.cyan("identify")} where users sign up or log in:`
|
|
774
|
+
);
|
|
775
|
+
log(c.dim(" LETTER.identify(user_id: user.id, email: user.email)"));
|
|
410
776
|
log();
|
|
411
|
-
log(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
log(
|
|
415
|
-
log(` 3. Call ${c.cyan("letter.track(...)")} on 2-3 key actions.`);
|
|
777
|
+
log(
|
|
778
|
+
c.bold(" 3. ") + `Call ${c.cyan("track")} on your 2-3 most important actions:`
|
|
779
|
+
);
|
|
780
|
+
log(c.dim(' LETTER.track(user_id: user.id, event: "Signed Up")'));
|
|
416
781
|
log();
|
|
417
|
-
log(
|
|
418
|
-
|
|
782
|
+
log(
|
|
783
|
+
c.dim(
|
|
784
|
+
" In serverless handlers or scripts, call LETTER.flush before returning."
|
|
785
|
+
)
|
|
786
|
+
);
|
|
787
|
+
log();
|
|
788
|
+
log(c.dim("Verify it landed: ") + c.bold("letter status"));
|
|
789
|
+
log(
|
|
790
|
+
c.dim(
|
|
791
|
+
`Your API key is in ${envFile} - keep it out of source control, and don't echo its value.`
|
|
792
|
+
)
|
|
793
|
+
);
|
|
794
|
+
log();
|
|
795
|
+
}
|
|
796
|
+
function printHttpInstructions(stack, envFile, base) {
|
|
797
|
+
log();
|
|
798
|
+
log(c.bold("Next: hand these steps to your coding agent"));
|
|
799
|
+
log(
|
|
800
|
+
c.dim(
|
|
801
|
+
`Detected ${stackLabel(stack)}. There's no ${languageName(stack)} SDK yet, so call Letter's HTTP API directly in ${languageName(stack)} with your usual HTTP client.`
|
|
802
|
+
)
|
|
803
|
+
);
|
|
804
|
+
log();
|
|
805
|
+
log(
|
|
806
|
+
c.dim(
|
|
807
|
+
`LETTER_API_KEY is in ${envFile}. Read it from the environment; never inline it.`
|
|
808
|
+
)
|
|
809
|
+
);
|
|
810
|
+
log();
|
|
811
|
+
log(c.bold(" 1. ") + "identify users where they sign up or log in:");
|
|
812
|
+
log(c.dim(` POST ${base}/v1/identify`));
|
|
813
|
+
log(c.dim(" Authorization: Bearer $LETTER_API_KEY"));
|
|
814
|
+
log(c.dim(' { "userId": "<your user id>", "email": "<email>" }'));
|
|
815
|
+
log();
|
|
816
|
+
log(c.bold(" 2. ") + "track your 2-3 most important actions:");
|
|
817
|
+
log(c.dim(` POST ${base}/v1/track`));
|
|
818
|
+
log(c.dim(" Authorization: Bearer $LETTER_API_KEY"));
|
|
819
|
+
log(c.dim(' { "userId": "<your user id>", "event": "Signed Up" }'));
|
|
820
|
+
log();
|
|
821
|
+
log(
|
|
822
|
+
c.dim(
|
|
823
|
+
" Send Content-Type: application/json. The same auth/shape covers group and batch."
|
|
824
|
+
)
|
|
825
|
+
);
|
|
826
|
+
log();
|
|
827
|
+
log(c.dim("Verify it landed: ") + c.bold("letter status"));
|
|
828
|
+
log(
|
|
829
|
+
c.dim(
|
|
830
|
+
`Your API key is in ${envFile} - keep it out of source control, and don't echo its value.`
|
|
831
|
+
)
|
|
832
|
+
);
|
|
419
833
|
log();
|
|
420
834
|
}
|
|
421
835
|
function rel(cwd, file) {
|
|
@@ -527,14 +941,595 @@ function registerConfigCommands(program2) {
|
|
|
527
941
|
});
|
|
528
942
|
}
|
|
529
943
|
|
|
944
|
+
// src/commands/resources.ts
|
|
945
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
946
|
+
async function readValue(raw) {
|
|
947
|
+
if (raw.startsWith("@")) return (await readFile4(raw.slice(1), "utf8")).trim();
|
|
948
|
+
return raw;
|
|
949
|
+
}
|
|
950
|
+
async function buildBody(fields, opts) {
|
|
951
|
+
const body = {};
|
|
952
|
+
for (const f of fields) {
|
|
953
|
+
const v = opts[camel(f.key)];
|
|
954
|
+
if (v === void 0) continue;
|
|
955
|
+
if (f.json) {
|
|
956
|
+
body[f.key] = JSON.parse(await readValue(String(v)));
|
|
957
|
+
} else if (f.number) {
|
|
958
|
+
body[f.key] = Number(v);
|
|
959
|
+
} else if (f.boolean) {
|
|
960
|
+
body[f.key] = v === true || v === "true";
|
|
961
|
+
} else {
|
|
962
|
+
body[f.key] = await readValue(String(v));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return body;
|
|
966
|
+
}
|
|
967
|
+
async function buildForm(fields, opts) {
|
|
968
|
+
const form = new FormData();
|
|
969
|
+
for (const f of fields) {
|
|
970
|
+
const v = opts[camel(f.key)];
|
|
971
|
+
if (v === void 0) continue;
|
|
972
|
+
if (f.file) {
|
|
973
|
+
const path3 = String(v);
|
|
974
|
+
const bytes = await readFile4(path3);
|
|
975
|
+
const name = path3.split("/").pop() || f.key;
|
|
976
|
+
form.append(f.key, new Blob([new Uint8Array(bytes)]), name);
|
|
977
|
+
} else if (f.json) {
|
|
978
|
+
form.append(f.key, await readValue(String(v)));
|
|
979
|
+
} else {
|
|
980
|
+
form.append(f.key, String(v));
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return form;
|
|
984
|
+
}
|
|
985
|
+
function buildQuery(fields, opts) {
|
|
986
|
+
const q = {};
|
|
987
|
+
for (const f of fields) {
|
|
988
|
+
const v = opts[camel(f.key)];
|
|
989
|
+
if (v === void 0) continue;
|
|
990
|
+
q[f.key] = f.number ? Number(v) : String(v);
|
|
991
|
+
}
|
|
992
|
+
return q;
|
|
993
|
+
}
|
|
994
|
+
function camel(key) {
|
|
995
|
+
return key.replace(/[-_]([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
996
|
+
}
|
|
997
|
+
function applyFields(cmd, fields) {
|
|
998
|
+
for (const f of fields) {
|
|
999
|
+
cmd.option(f.flag, f.description ?? f.key);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
async function basePath(spec, project) {
|
|
1003
|
+
const seg = spec.segment ?? spec.name;
|
|
1004
|
+
if (!spec.scoped) return `/v1/${seg}`;
|
|
1005
|
+
const slug = await resolveProjectSlug(project);
|
|
1006
|
+
return `/v1/projects/${slug}/${seg}`;
|
|
1007
|
+
}
|
|
1008
|
+
function withProject(cmd, scoped) {
|
|
1009
|
+
if (scoped) {
|
|
1010
|
+
cmd.option("-p, --project <slug>", "Project slug (default: connected project)");
|
|
1011
|
+
}
|
|
1012
|
+
return cmd;
|
|
1013
|
+
}
|
|
1014
|
+
function fail(err) {
|
|
1015
|
+
printError(err);
|
|
1016
|
+
process.exitCode = 1;
|
|
1017
|
+
}
|
|
1018
|
+
function register(program2, spec) {
|
|
1019
|
+
const group = program2.command(spec.name).description(spec.describe);
|
|
1020
|
+
if (spec.alias) group.alias(spec.alias);
|
|
1021
|
+
const idName = spec.idName ?? "id";
|
|
1022
|
+
if (spec.list) {
|
|
1023
|
+
const cmd = withProject(
|
|
1024
|
+
group.command("list").description(`List ${spec.name}`),
|
|
1025
|
+
spec.scoped
|
|
1026
|
+
);
|
|
1027
|
+
if (spec.list.paginated) {
|
|
1028
|
+
cmd.option("--limit <n>", "Max items (1-100)");
|
|
1029
|
+
cmd.option("--cursor <cursor>", "Pagination cursor");
|
|
1030
|
+
}
|
|
1031
|
+
if (spec.list.query) applyFields(cmd, spec.list.query);
|
|
1032
|
+
cmd.action(async (opts) => {
|
|
1033
|
+
try {
|
|
1034
|
+
const query = {};
|
|
1035
|
+
if (spec.list?.paginated) {
|
|
1036
|
+
if (opts.limit) query.limit = Number(opts.limit);
|
|
1037
|
+
if (opts.cursor) query.cursor = opts.cursor;
|
|
1038
|
+
}
|
|
1039
|
+
if (spec.list?.query) Object.assign(query, buildQuery(spec.list.query, opts));
|
|
1040
|
+
const { data } = await mgmt.get(await basePath(spec, opts.project), query);
|
|
1041
|
+
emit(data);
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
fail(err);
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
if (spec.get) {
|
|
1048
|
+
const cmd = withProject(
|
|
1049
|
+
group.command("get").description(`Get a ${spec.name} by ${idName}`).argument(`<${idName}>`, idName),
|
|
1050
|
+
spec.scoped
|
|
1051
|
+
);
|
|
1052
|
+
cmd.action(async (idValue, opts) => {
|
|
1053
|
+
try {
|
|
1054
|
+
const path3 = `${await basePath(spec, opts.project)}/${encodeURIComponent(idValue)}`;
|
|
1055
|
+
const { data } = await mgmt.get(path3);
|
|
1056
|
+
emit(data);
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
fail(err);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
if (spec.create) {
|
|
1063
|
+
const cmd = withProject(
|
|
1064
|
+
group.command("create").description(`Create a ${spec.name}`),
|
|
1065
|
+
spec.scoped
|
|
1066
|
+
);
|
|
1067
|
+
applyFields(cmd, spec.create);
|
|
1068
|
+
cmd.action(async (opts) => {
|
|
1069
|
+
try {
|
|
1070
|
+
const body = await buildBody(spec.create, opts);
|
|
1071
|
+
const { data } = await mgmt.post(await basePath(spec, opts.project), body);
|
|
1072
|
+
emit(data);
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
fail(err);
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
if (spec.update) {
|
|
1079
|
+
const cmd = withProject(
|
|
1080
|
+
group.command("update").description(`Update a ${spec.name}`).argument(`<${idName}>`, idName),
|
|
1081
|
+
spec.scoped
|
|
1082
|
+
);
|
|
1083
|
+
applyFields(cmd, spec.update);
|
|
1084
|
+
cmd.action(async (idValue, opts) => {
|
|
1085
|
+
try {
|
|
1086
|
+
const body = await buildBody(spec.update, opts);
|
|
1087
|
+
const path3 = `${await basePath(spec, opts.project)}/${encodeURIComponent(idValue)}`;
|
|
1088
|
+
const { data } = await mgmt.patch(path3, body);
|
|
1089
|
+
emit(data);
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
fail(err);
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
if (spec.remove) {
|
|
1096
|
+
const cmd = withProject(
|
|
1097
|
+
group.command("delete").description(`Delete a ${spec.name}`).argument(`<${idName}>`, idName),
|
|
1098
|
+
spec.scoped
|
|
1099
|
+
);
|
|
1100
|
+
cmd.action(async (idValue, opts) => {
|
|
1101
|
+
try {
|
|
1102
|
+
const path3 = `${await basePath(spec, opts.project)}/${encodeURIComponent(idValue)}`;
|
|
1103
|
+
await mgmt.delete(path3);
|
|
1104
|
+
printSuccess(`Deleted ${spec.name.replace(/s$/, "")} ${idValue}.`);
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
fail(err);
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
for (const action of spec.actions ?? []) {
|
|
1111
|
+
const needsId = action.needsId ?? true;
|
|
1112
|
+
const cmd = withProject(
|
|
1113
|
+
needsId ? group.command(action.name).description(action.describe).argument(`<${idName}>`, idName) : group.command(action.name).description(action.describe),
|
|
1114
|
+
spec.scoped
|
|
1115
|
+
);
|
|
1116
|
+
if (action.fields) applyFields(cmd, action.fields);
|
|
1117
|
+
if (action.query) applyFields(cmd, action.query);
|
|
1118
|
+
const handler = async (...args) => {
|
|
1119
|
+
const opts = args[args.length - 2];
|
|
1120
|
+
const idValue = needsId ? args[0] : void 0;
|
|
1121
|
+
try {
|
|
1122
|
+
let path3 = await basePath(spec, opts.project);
|
|
1123
|
+
if (needsId) path3 += `/${encodeURIComponent(idValue)}`;
|
|
1124
|
+
path3 += action.suffix;
|
|
1125
|
+
if (action.method === "get") {
|
|
1126
|
+
const query = action.query ? buildQuery(action.query, opts) : void 0;
|
|
1127
|
+
const { data: data2 } = await mgmt.get(path3, query);
|
|
1128
|
+
emit(data2);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (action.multipart && action.fields) {
|
|
1132
|
+
const form = await buildForm(action.fields, opts);
|
|
1133
|
+
const { data: data2, status: status2 } = await mgmt.request(
|
|
1134
|
+
action.method.toUpperCase(),
|
|
1135
|
+
path3,
|
|
1136
|
+
{ form }
|
|
1137
|
+
);
|
|
1138
|
+
if (status2 === 204) printSuccess(`${action.name} ok.`);
|
|
1139
|
+
else emit(data2);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
const body = action.fields ? await buildBody(action.fields, opts) : void 0;
|
|
1143
|
+
const { data, status } = await mgmt.request(
|
|
1144
|
+
action.method.toUpperCase(),
|
|
1145
|
+
path3,
|
|
1146
|
+
{ body }
|
|
1147
|
+
);
|
|
1148
|
+
if (status === 204) printSuccess(`${action.name} ok.`);
|
|
1149
|
+
else emit(data);
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
fail(err);
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
cmd.action(handler);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
var cursor = { paginated: true };
|
|
1158
|
+
var SPECS = [
|
|
1159
|
+
// -- Workspace ------------------------------------------------------------
|
|
1160
|
+
{
|
|
1161
|
+
name: "projects",
|
|
1162
|
+
describe: "Manage projects",
|
|
1163
|
+
scoped: false,
|
|
1164
|
+
idName: "slug",
|
|
1165
|
+
list: {},
|
|
1166
|
+
get: true,
|
|
1167
|
+
create: [
|
|
1168
|
+
{ flag: "--name <name>", key: "name" },
|
|
1169
|
+
{ flag: "--timezone <tz>", key: "timezone" }
|
|
1170
|
+
],
|
|
1171
|
+
update: [
|
|
1172
|
+
{ flag: "--name <name>", key: "name" },
|
|
1173
|
+
{ flag: "--timezone <tz>", key: "timezone" }
|
|
1174
|
+
],
|
|
1175
|
+
remove: true
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
name: "members",
|
|
1179
|
+
describe: "Manage workspace members",
|
|
1180
|
+
scoped: false,
|
|
1181
|
+
idName: "userId",
|
|
1182
|
+
list: {},
|
|
1183
|
+
create: [
|
|
1184
|
+
{ flag: "--email <email>", key: "email" },
|
|
1185
|
+
{ flag: "--role <role>", key: "role", description: "member | admin" }
|
|
1186
|
+
],
|
|
1187
|
+
remove: true
|
|
1188
|
+
},
|
|
1189
|
+
{
|
|
1190
|
+
name: "invitations",
|
|
1191
|
+
describe: "Manage workspace invitations",
|
|
1192
|
+
scoped: false,
|
|
1193
|
+
list: {},
|
|
1194
|
+
create: [
|
|
1195
|
+
{ flag: "--email <email>", key: "email" },
|
|
1196
|
+
{ flag: "--role <role>", key: "role", description: "member | admin" }
|
|
1197
|
+
],
|
|
1198
|
+
remove: true
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
// Personal access tokens (lt_pat_*) authenticate the CLI / Management API.
|
|
1202
|
+
// The dashboard calls these "API keys"; `tokens` stays as a hidden alias.
|
|
1203
|
+
name: "api-keys",
|
|
1204
|
+
alias: "tokens",
|
|
1205
|
+
segment: "tokens",
|
|
1206
|
+
describe: "Manage your personal access tokens (lt_pat_*, the CLI/API credential)",
|
|
1207
|
+
scoped: false,
|
|
1208
|
+
list: {},
|
|
1209
|
+
create: [
|
|
1210
|
+
{ flag: "--name <name>", key: "name" },
|
|
1211
|
+
{
|
|
1212
|
+
flag: "--expires-at <iso>",
|
|
1213
|
+
key: "expires_at",
|
|
1214
|
+
description: "Expiry as an ISO 8601 datetime (default: never)"
|
|
1215
|
+
}
|
|
1216
|
+
],
|
|
1217
|
+
remove: true
|
|
1218
|
+
},
|
|
1219
|
+
// -- Contacts & data ------------------------------------------------------
|
|
1220
|
+
{
|
|
1221
|
+
name: "contacts",
|
|
1222
|
+
describe: "Manage contacts",
|
|
1223
|
+
scoped: true,
|
|
1224
|
+
idName: "externalId",
|
|
1225
|
+
list: { ...cursor },
|
|
1226
|
+
get: true,
|
|
1227
|
+
actions: [
|
|
1228
|
+
{
|
|
1229
|
+
name: "suppress",
|
|
1230
|
+
describe: "Suppress a contact",
|
|
1231
|
+
method: "post",
|
|
1232
|
+
suffix: "/suppress"
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
name: "resubscribe",
|
|
1236
|
+
describe: "Resubscribe a contact",
|
|
1237
|
+
method: "post",
|
|
1238
|
+
suffix: "/resubscribe"
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
name: "import",
|
|
1242
|
+
describe: "Upload a CSV import (--file <path> --mapping <json|@file>)",
|
|
1243
|
+
method: "post",
|
|
1244
|
+
needsId: false,
|
|
1245
|
+
suffix: "/imports",
|
|
1246
|
+
multipart: true,
|
|
1247
|
+
fields: [
|
|
1248
|
+
{ flag: "--file <path>", key: "file", file: true },
|
|
1249
|
+
{ flag: "--mapping <json>", key: "mapping", json: true },
|
|
1250
|
+
{ flag: "--dedupe <strategy>", key: "dedupe", description: "update | skip" },
|
|
1251
|
+
{ flag: "--row-count <n>", key: "rowCount" }
|
|
1252
|
+
]
|
|
1253
|
+
}
|
|
1254
|
+
]
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
name: "accounts",
|
|
1258
|
+
describe: "Inspect accounts",
|
|
1259
|
+
scoped: true,
|
|
1260
|
+
idName: "externalId",
|
|
1261
|
+
list: { ...cursor },
|
|
1262
|
+
get: true
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
name: "events",
|
|
1266
|
+
describe: "List events",
|
|
1267
|
+
scoped: true,
|
|
1268
|
+
list: { ...cursor, query: [{ flag: "--name <name>", key: "name" }] }
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
name: "suppressions",
|
|
1272
|
+
describe: "Manage the suppression list",
|
|
1273
|
+
scoped: true,
|
|
1274
|
+
idName: "email",
|
|
1275
|
+
list: { ...cursor },
|
|
1276
|
+
create: [{ flag: "--email <email>", key: "email" }],
|
|
1277
|
+
remove: true
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
name: "segments",
|
|
1281
|
+
describe: "Manage segments",
|
|
1282
|
+
scoped: true,
|
|
1283
|
+
list: {},
|
|
1284
|
+
get: true,
|
|
1285
|
+
create: [
|
|
1286
|
+
{ flag: "--name <name>", key: "name" },
|
|
1287
|
+
{ flag: "--filter <json>", key: "filter", json: true }
|
|
1288
|
+
],
|
|
1289
|
+
update: [
|
|
1290
|
+
{ flag: "--name <name>", key: "name" },
|
|
1291
|
+
{ flag: "--filter <json>", key: "filter", json: true }
|
|
1292
|
+
],
|
|
1293
|
+
remove: true,
|
|
1294
|
+
actions: [
|
|
1295
|
+
{
|
|
1296
|
+
name: "preview",
|
|
1297
|
+
describe: "Count contacts matching a filter (--filter <json|@file>)",
|
|
1298
|
+
method: "post",
|
|
1299
|
+
needsId: false,
|
|
1300
|
+
suffix: "/preview",
|
|
1301
|
+
fields: [{ flag: "--filter <json>", key: "filter", json: true }]
|
|
1302
|
+
}
|
|
1303
|
+
]
|
|
1304
|
+
},
|
|
1305
|
+
// -- Sequences ------------------------------------------------------------
|
|
1306
|
+
{
|
|
1307
|
+
name: "sequences",
|
|
1308
|
+
describe: "Manage drip sequences",
|
|
1309
|
+
scoped: true,
|
|
1310
|
+
list: {},
|
|
1311
|
+
get: true,
|
|
1312
|
+
create: [{ flag: "--name <name>", key: "name" }],
|
|
1313
|
+
update: [
|
|
1314
|
+
{ flag: "--name <name>", key: "name" },
|
|
1315
|
+
{ flag: "--status <status>", key: "status", description: "active | archived" }
|
|
1316
|
+
],
|
|
1317
|
+
remove: true,
|
|
1318
|
+
actions: [
|
|
1319
|
+
{
|
|
1320
|
+
name: "draft",
|
|
1321
|
+
describe: "Save the draft graph + trigger",
|
|
1322
|
+
method: "put",
|
|
1323
|
+
suffix: "/draft",
|
|
1324
|
+
fields: [
|
|
1325
|
+
{ flag: "--graph <json>", key: "graph", json: true },
|
|
1326
|
+
{ flag: "--trigger <json>", key: "trigger", json: true },
|
|
1327
|
+
{ flag: "--expected-revision <n>", key: "expected_revision", number: true }
|
|
1328
|
+
]
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
name: "publish",
|
|
1332
|
+
describe: "Publish the current draft",
|
|
1333
|
+
method: "post",
|
|
1334
|
+
suffix: "/publish"
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
name: "preview",
|
|
1338
|
+
describe: "Render an email node (--node-id <id>)",
|
|
1339
|
+
method: "post",
|
|
1340
|
+
suffix: "/preview",
|
|
1341
|
+
fields: [{ flag: "--node-id <id>", key: "nodeId" }]
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
name: "test-email",
|
|
1345
|
+
describe: "Send a test email (--node-id <id> --to <email>)",
|
|
1346
|
+
method: "post",
|
|
1347
|
+
suffix: "/test-email",
|
|
1348
|
+
fields: [
|
|
1349
|
+
{ flag: "--node-id <id>", key: "nodeId" },
|
|
1350
|
+
{ flag: "--to <email>", key: "to" }
|
|
1351
|
+
]
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
name: "activity",
|
|
1355
|
+
describe: "Show enrollment activity",
|
|
1356
|
+
method: "get",
|
|
1357
|
+
suffix: "/activity"
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
name: "versions",
|
|
1361
|
+
describe: "List published versions",
|
|
1362
|
+
method: "get",
|
|
1363
|
+
suffix: "/versions"
|
|
1364
|
+
}
|
|
1365
|
+
]
|
|
1366
|
+
},
|
|
1367
|
+
// -- Broadcasts -----------------------------------------------------------
|
|
1368
|
+
{
|
|
1369
|
+
name: "broadcasts",
|
|
1370
|
+
describe: "Manage broadcasts",
|
|
1371
|
+
scoped: true,
|
|
1372
|
+
list: {},
|
|
1373
|
+
get: true,
|
|
1374
|
+
create: [{ flag: "--name <name>", key: "name" }],
|
|
1375
|
+
update: [
|
|
1376
|
+
{ flag: "--name <name>", key: "name" },
|
|
1377
|
+
{ flag: "--subject <subject>", key: "subject" },
|
|
1378
|
+
{ flag: "--preview <preview>", key: "preview" },
|
|
1379
|
+
{ flag: "--audience <json>", key: "audience", json: true },
|
|
1380
|
+
{ flag: "--template-id <id>", key: "template_id" },
|
|
1381
|
+
{ flag: "--body-doc <json>", key: "body_doc", json: true }
|
|
1382
|
+
],
|
|
1383
|
+
remove: true,
|
|
1384
|
+
actions: [
|
|
1385
|
+
{
|
|
1386
|
+
name: "preflight",
|
|
1387
|
+
describe: "Validate before sending",
|
|
1388
|
+
method: "post",
|
|
1389
|
+
suffix: "/preflight"
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
name: "schedule",
|
|
1393
|
+
describe: "Schedule (--scheduled-at <iso>) or send now (omit)",
|
|
1394
|
+
method: "post",
|
|
1395
|
+
suffix: "/schedule",
|
|
1396
|
+
fields: [{ flag: "--scheduled-at <iso>", key: "scheduled_at" }]
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
name: "cancel",
|
|
1400
|
+
describe: "Cancel a scheduled or sending broadcast",
|
|
1401
|
+
method: "post",
|
|
1402
|
+
suffix: "/cancel"
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
name: "live",
|
|
1406
|
+
describe: "Live status, stats, and recent activity",
|
|
1407
|
+
method: "get",
|
|
1408
|
+
suffix: "/live"
|
|
1409
|
+
}
|
|
1410
|
+
]
|
|
1411
|
+
},
|
|
1412
|
+
// -- Templates ------------------------------------------------------------
|
|
1413
|
+
{
|
|
1414
|
+
name: "templates",
|
|
1415
|
+
describe: "Manage email templates",
|
|
1416
|
+
scoped: true,
|
|
1417
|
+
list: {},
|
|
1418
|
+
get: true,
|
|
1419
|
+
create: [
|
|
1420
|
+
{ flag: "--name <name>", key: "name" },
|
|
1421
|
+
{ flag: "--design <json>", key: "design", json: true }
|
|
1422
|
+
],
|
|
1423
|
+
update: [
|
|
1424
|
+
{ flag: "--name <name>", key: "name" },
|
|
1425
|
+
{ flag: "--design <json>", key: "design", json: true }
|
|
1426
|
+
],
|
|
1427
|
+
remove: true,
|
|
1428
|
+
actions: [
|
|
1429
|
+
{
|
|
1430
|
+
name: "default",
|
|
1431
|
+
describe: "Set as the project default",
|
|
1432
|
+
method: "post",
|
|
1433
|
+
suffix: "/default"
|
|
1434
|
+
},
|
|
1435
|
+
{
|
|
1436
|
+
name: "reset",
|
|
1437
|
+
describe: "Reset design to a preset (--preset plain|branded)",
|
|
1438
|
+
method: "post",
|
|
1439
|
+
suffix: "/reset",
|
|
1440
|
+
fields: [{ flag: "--preset <preset>", key: "preset" }]
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
name: "logo",
|
|
1444
|
+
describe: "Upload a logo (--file <path>)",
|
|
1445
|
+
method: "post",
|
|
1446
|
+
suffix: "/logo",
|
|
1447
|
+
multipart: true,
|
|
1448
|
+
fields: [{ flag: "--file <path>", key: "file", file: true }]
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
name: "remove-logo",
|
|
1452
|
+
describe: "Remove the template logo",
|
|
1453
|
+
method: "delete",
|
|
1454
|
+
suffix: "/logo"
|
|
1455
|
+
}
|
|
1456
|
+
]
|
|
1457
|
+
},
|
|
1458
|
+
// -- Settings -------------------------------------------------------------
|
|
1459
|
+
{
|
|
1460
|
+
name: "domains",
|
|
1461
|
+
describe: "Manage sending domains",
|
|
1462
|
+
scoped: true,
|
|
1463
|
+
list: {},
|
|
1464
|
+
create: [{ flag: "--domain <domain>", key: "domain" }],
|
|
1465
|
+
remove: true,
|
|
1466
|
+
actions: [
|
|
1467
|
+
{
|
|
1468
|
+
name: "verify",
|
|
1469
|
+
describe: "Re-check DKIM verification",
|
|
1470
|
+
method: "post",
|
|
1471
|
+
suffix: "/verify"
|
|
1472
|
+
}
|
|
1473
|
+
]
|
|
1474
|
+
},
|
|
1475
|
+
{
|
|
1476
|
+
// Project ingestion keys (lt_live_*) authenticate the SDKs / ingestion API.
|
|
1477
|
+
// The dashboard calls these "Project tokens"; `keys` stays as a hidden alias.
|
|
1478
|
+
name: "project-tokens",
|
|
1479
|
+
alias: "keys",
|
|
1480
|
+
segment: "keys",
|
|
1481
|
+
describe: "Manage project ingestion keys (lt_live_*, the SDK/ingestion credential)",
|
|
1482
|
+
scoped: true,
|
|
1483
|
+
list: {},
|
|
1484
|
+
create: [{ flag: "--name <name>", key: "name" }],
|
|
1485
|
+
remove: true
|
|
1486
|
+
}
|
|
1487
|
+
];
|
|
1488
|
+
function registerResourceCommands(program2) {
|
|
1489
|
+
for (const spec of SPECS) register(program2, spec);
|
|
1490
|
+
program2.command("me").description("Show the token's user, workspace, and role").action(async () => {
|
|
1491
|
+
try {
|
|
1492
|
+
const { data } = await mgmt.get("/v1/me");
|
|
1493
|
+
emit(data);
|
|
1494
|
+
} catch (err) {
|
|
1495
|
+
fail(err);
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
program2.command("sender-identity").description("Set From / From-name / Reply-To (--from-email, --from-name, --reply-to-email)").option("-p, --project <slug>", "Project slug (default: connected project)").option("--from-email <email>", "From address").option("--from-name <name>", "From display name").option("--reply-to-email <email>", "Reply-To address").action(async (opts) => {
|
|
1499
|
+
try {
|
|
1500
|
+
const slug = await resolveProjectSlug(opts.project);
|
|
1501
|
+
const body = {};
|
|
1502
|
+
if (opts.fromEmail !== void 0) body.from_email = opts.fromEmail;
|
|
1503
|
+
if (opts.fromName !== void 0) body.from_name = opts.fromName;
|
|
1504
|
+
if (opts.replyToEmail !== void 0) body.reply_to_email = opts.replyToEmail;
|
|
1505
|
+
const { data } = await mgmt.put(`/v1/projects/${slug}/sender-identity`, body);
|
|
1506
|
+
emit(data);
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
fail(err);
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
program2.command("sending-mode").description("Set the sending mode (letter_subdomain | byo_domain)").argument("<mode>", "letter_subdomain | byo_domain").option("-p, --project <slug>", "Project slug (default: connected project)").action(async (mode, opts) => {
|
|
1512
|
+
try {
|
|
1513
|
+
const slug = await resolveProjectSlug(opts.project);
|
|
1514
|
+
const { data } = await mgmt.put(`/v1/projects/${slug}/sending-mode`, {
|
|
1515
|
+
sending_mode: mode
|
|
1516
|
+
});
|
|
1517
|
+
emit(data);
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
fail(err);
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
|
|
530
1524
|
// src/index.ts
|
|
531
1525
|
var program = new Command();
|
|
532
|
-
program.name("letter").description("Connect your app to Letter, then manage it from the command line").version("0.
|
|
1526
|
+
program.name("letter").description("Connect your app to Letter, then manage it from the command line").version("0.3.0").option("--json", "Output raw JSON (for scripting / agents)").hook("preAction", (thisCommand) => {
|
|
533
1527
|
if (thisCommand.opts().json) setJsonMode(true);
|
|
534
1528
|
});
|
|
535
1529
|
registerLoginCommand(program);
|
|
536
1530
|
registerAuthCommands(program);
|
|
537
1531
|
registerStatusCommand(program);
|
|
538
1532
|
registerConfigCommands(program);
|
|
1533
|
+
registerResourceCommands(program);
|
|
539
1534
|
program.parseAsync();
|
|
540
1535
|
//# sourceMappingURL=index.js.map
|