@letterapp/cli 0.2.0 → 0.3.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 +53 -18
- package/dist/index.js +885 -28
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -17,6 +17,9 @@ function isJsonMode() {
|
|
|
17
17
|
function printJson(data) {
|
|
18
18
|
console.log(JSON.stringify(data, null, 2));
|
|
19
19
|
}
|
|
20
|
+
function cliCommand() {
|
|
21
|
+
return (process.argv[1] ?? "").includes("_npx") ? "npx @letterapp/cli" : "letter";
|
|
22
|
+
}
|
|
20
23
|
function log(msg = "") {
|
|
21
24
|
if (jsonMode) return;
|
|
22
25
|
console.log(msg);
|
|
@@ -60,6 +63,30 @@ function prompt(question) {
|
|
|
60
63
|
});
|
|
61
64
|
});
|
|
62
65
|
}
|
|
66
|
+
function emit(data) {
|
|
67
|
+
if (jsonMode) {
|
|
68
|
+
printJson(data);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (data && typeof data === "object" && Array.isArray(data.data)) {
|
|
72
|
+
const env = data;
|
|
73
|
+
if (env.data.length === 0) {
|
|
74
|
+
console.log(chalk.dim("(none)"));
|
|
75
|
+
} else {
|
|
76
|
+
for (const row of env.data) {
|
|
77
|
+
const id = String(row.id ?? row.slug ?? row.external_id ?? "");
|
|
78
|
+
const label = row.name ?? row.email ?? row.domain ?? row.subject ?? "";
|
|
79
|
+
console.log(`${chalk.bold(id)}${label ? " " + label : ""}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (env.next_cursor) {
|
|
83
|
+
console.log(chalk.dim(`
|
|
84
|
+
\u203A more: --cursor ${env.next_cursor}`));
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
printJson(data);
|
|
89
|
+
}
|
|
63
90
|
var c = chalk;
|
|
64
91
|
|
|
65
92
|
// src/config.ts
|
|
@@ -87,6 +114,15 @@ function getConfigPath() {
|
|
|
87
114
|
function resetConfig() {
|
|
88
115
|
config.clear();
|
|
89
116
|
}
|
|
117
|
+
async function resolveProjectSlug(flag) {
|
|
118
|
+
if (flag) return flag;
|
|
119
|
+
if (process.env.LETTER_PROJECT) return process.env.LETTER_PROJECT;
|
|
120
|
+
const cred = await readCredential();
|
|
121
|
+
if (cred?.project?.slug) return cred.project.slug;
|
|
122
|
+
throw new Error(
|
|
123
|
+
`No project. Pass --project <slug>, set LETTER_PROJECT, or run \`${cliCommand()} login\`.`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
90
126
|
function credentialsPath() {
|
|
91
127
|
return path.join(homedir(), ".letter", "credentials.json");
|
|
92
128
|
}
|
|
@@ -146,7 +182,7 @@ var LetterClient = class {
|
|
|
146
182
|
const token = process.env.LETTER_API_KEY || cred?.apiKey || "";
|
|
147
183
|
if (!token) {
|
|
148
184
|
throw new Error(
|
|
149
|
-
|
|
185
|
+
`Not connected. Run \`${cliCommand()} login\` (or set LETTER_API_KEY) first.`
|
|
150
186
|
);
|
|
151
187
|
}
|
|
152
188
|
const base = getBaseUrl(process.env.LETTER_API_KEY ? void 0 : cred?.baseUrl);
|
|
@@ -181,6 +217,78 @@ var LetterClient = class {
|
|
|
181
217
|
}
|
|
182
218
|
};
|
|
183
219
|
var client = new LetterClient();
|
|
220
|
+
var ManagementClient = class {
|
|
221
|
+
maxRetries = 3;
|
|
222
|
+
async resolveAuth() {
|
|
223
|
+
const cred = await readCredential();
|
|
224
|
+
const token = process.env.LETTER_PAT || cred?.pat || "";
|
|
225
|
+
if (!token) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Not connected for management. Run \`${cliCommand()} login\` (or set LETTER_PAT) first.`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const base = getBaseUrl(process.env.LETTER_PAT ? void 0 : cred?.baseUrl);
|
|
231
|
+
return { base, token };
|
|
232
|
+
}
|
|
233
|
+
async request(method, path3, init = {}, attempt = 1) {
|
|
234
|
+
const { base, token } = await this.resolveAuth();
|
|
235
|
+
let url = `${base}${path3}`;
|
|
236
|
+
if (init.query) {
|
|
237
|
+
const qs = new URLSearchParams();
|
|
238
|
+
for (const [k, v] of Object.entries(init.query)) {
|
|
239
|
+
if (v !== void 0 && v !== "") qs.set(k, String(v));
|
|
240
|
+
}
|
|
241
|
+
const s = qs.toString();
|
|
242
|
+
if (s) url += `?${s}`;
|
|
243
|
+
}
|
|
244
|
+
const headers = {
|
|
245
|
+
authorization: `Bearer ${token}`,
|
|
246
|
+
accept: "application/json",
|
|
247
|
+
"user-agent": USER_AGENT
|
|
248
|
+
};
|
|
249
|
+
let body;
|
|
250
|
+
if (init.form) {
|
|
251
|
+
body = init.form;
|
|
252
|
+
} else if (init.body !== void 0) {
|
|
253
|
+
headers["content-type"] = "application/json";
|
|
254
|
+
body = JSON.stringify(init.body);
|
|
255
|
+
}
|
|
256
|
+
const res = await fetch(url, { method, headers, body });
|
|
257
|
+
if (res.status === 429 && attempt <= this.maxRetries) {
|
|
258
|
+
const retryAfter = Number(res.headers.get("retry-after") || 2);
|
|
259
|
+
await new Promise((r) => setTimeout(r, retryAfter * 1e3 * attempt));
|
|
260
|
+
return this.request(method, path3, init, attempt + 1);
|
|
261
|
+
}
|
|
262
|
+
const text = await res.text();
|
|
263
|
+
const data = text ? JSON.parse(text) : {};
|
|
264
|
+
if (!res.ok) {
|
|
265
|
+
const msg = data?.error?.message || `HTTP ${res.status}`;
|
|
266
|
+
const err = new Error(msg);
|
|
267
|
+
err.status = res.status;
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
return { status: res.status, data };
|
|
271
|
+
}
|
|
272
|
+
get(path3, query) {
|
|
273
|
+
return this.request("GET", path3, { query });
|
|
274
|
+
}
|
|
275
|
+
post(path3, body) {
|
|
276
|
+
return this.request("POST", path3, { body });
|
|
277
|
+
}
|
|
278
|
+
postForm(path3, form) {
|
|
279
|
+
return this.request("POST", path3, { form });
|
|
280
|
+
}
|
|
281
|
+
patch(path3, body) {
|
|
282
|
+
return this.request("PATCH", path3, { body });
|
|
283
|
+
}
|
|
284
|
+
put(path3, body) {
|
|
285
|
+
return this.request("PUT", path3, { body });
|
|
286
|
+
}
|
|
287
|
+
delete(path3) {
|
|
288
|
+
return this.request("DELETE", path3);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
var mgmt = new ManagementClient();
|
|
184
292
|
|
|
185
293
|
// src/browser.ts
|
|
186
294
|
import { spawn } from "child_process";
|
|
@@ -249,7 +357,7 @@ function escapeRegExp(s) {
|
|
|
249
357
|
|
|
250
358
|
// src/pm.ts
|
|
251
359
|
import { spawn as spawn2 } from "child_process";
|
|
252
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
360
|
+
import { readFile as readFile3, readdir } from "fs/promises";
|
|
253
361
|
async function detectPackageManager(cwd) {
|
|
254
362
|
if (await fileExists(cwd, "pnpm-lock.yaml")) return "pnpm";
|
|
255
363
|
if (await fileExists(cwd, "yarn.lock")) return "yarn";
|
|
@@ -277,6 +385,121 @@ async function detectFramework(cwd) {
|
|
|
277
385
|
return null;
|
|
278
386
|
}
|
|
279
387
|
}
|
|
388
|
+
async function detectStack(cwd) {
|
|
389
|
+
if (await fileExists(cwd, "package.json")) {
|
|
390
|
+
return { runtime: "node", framework: await detectFramework(cwd) };
|
|
391
|
+
}
|
|
392
|
+
if (await fileExists(cwd, "pyproject.toml") || await fileExists(cwd, "requirements.txt") || await fileExists(cwd, "Pipfile") || await fileExists(cwd, "setup.py")) {
|
|
393
|
+
return { runtime: "python", framework: await detectPythonFramework(cwd) };
|
|
394
|
+
}
|
|
395
|
+
if (await fileExists(cwd, "Gemfile") || await hasGemspec(cwd)) {
|
|
396
|
+
return { runtime: "ruby", framework: await detectRubyFramework(cwd) };
|
|
397
|
+
}
|
|
398
|
+
if (await fileExists(cwd, "go.mod")) return { runtime: "go", framework: null };
|
|
399
|
+
if (await fileExists(cwd, "composer.json")) {
|
|
400
|
+
return { runtime: "php", framework: await detectPhpFramework(cwd) };
|
|
401
|
+
}
|
|
402
|
+
return { runtime: "other", framework: null };
|
|
403
|
+
}
|
|
404
|
+
function stackLabel(stack) {
|
|
405
|
+
if (stack.framework) return stack.framework;
|
|
406
|
+
switch (stack.runtime) {
|
|
407
|
+
case "node":
|
|
408
|
+
return "a Node.js project";
|
|
409
|
+
case "python":
|
|
410
|
+
return "a Python project";
|
|
411
|
+
case "ruby":
|
|
412
|
+
return "a Ruby project";
|
|
413
|
+
case "go":
|
|
414
|
+
return "a Go project";
|
|
415
|
+
case "php":
|
|
416
|
+
return "a PHP project";
|
|
417
|
+
default:
|
|
418
|
+
return "your project";
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function languageName(stack) {
|
|
422
|
+
switch (stack.runtime) {
|
|
423
|
+
case "node":
|
|
424
|
+
return "TypeScript";
|
|
425
|
+
case "python":
|
|
426
|
+
return "Python";
|
|
427
|
+
case "ruby":
|
|
428
|
+
return "Ruby";
|
|
429
|
+
case "go":
|
|
430
|
+
return "Go";
|
|
431
|
+
case "php":
|
|
432
|
+
return "PHP";
|
|
433
|
+
default:
|
|
434
|
+
return "your language";
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function readIf(cwd, name) {
|
|
438
|
+
try {
|
|
439
|
+
return await readFile3(`${cwd}/${name}`, "utf8");
|
|
440
|
+
} catch {
|
|
441
|
+
return "";
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async function hasGemspec(cwd) {
|
|
445
|
+
try {
|
|
446
|
+
return (await readdir(cwd)).some((f) => f.endsWith(".gemspec"));
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function detectPythonFramework(cwd) {
|
|
452
|
+
const txt = (await readIf(cwd, "requirements.txt") + await readIf(cwd, "pyproject.toml") + await readIf(cwd, "Pipfile")).toLowerCase();
|
|
453
|
+
if (txt.includes("django")) return "Django";
|
|
454
|
+
if (txt.includes("fastapi")) return "FastAPI";
|
|
455
|
+
if (txt.includes("flask")) return "Flask";
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
async function detectRubyFramework(cwd) {
|
|
459
|
+
const txt = (await readIf(cwd, "Gemfile")).toLowerCase();
|
|
460
|
+
if (txt.includes("rails")) return "Rails";
|
|
461
|
+
if (txt.includes("sinatra")) return "Sinatra";
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
async function detectPhpFramework(cwd) {
|
|
465
|
+
const txt = (await readIf(cwd, "composer.json")).toLowerCase();
|
|
466
|
+
if (txt.includes("laravel")) return "Laravel";
|
|
467
|
+
if (txt.includes("symfony")) return "Symfony";
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
var SDK_PYPI = "letterapp";
|
|
471
|
+
var SDK_GEM = "letterapp";
|
|
472
|
+
function sdkInstall(cmd, args) {
|
|
473
|
+
return { cmd, args, display: `${cmd} ${args.join(" ")}` };
|
|
474
|
+
}
|
|
475
|
+
async function pythonSdkInstall(cwd) {
|
|
476
|
+
if (await fileExists(cwd, "uv.lock")) return sdkInstall("uv", ["add", SDK_PYPI]);
|
|
477
|
+
const pyproject = (await readIf(cwd, "pyproject.toml")).toLowerCase();
|
|
478
|
+
if (await fileExists(cwd, "poetry.lock") || pyproject.includes("[tool.poetry]")) {
|
|
479
|
+
return sdkInstall("poetry", ["add", SDK_PYPI]);
|
|
480
|
+
}
|
|
481
|
+
if (await fileExists(cwd, "Pipfile")) {
|
|
482
|
+
return sdkInstall("pipenv", ["install", SDK_PYPI]);
|
|
483
|
+
}
|
|
484
|
+
return sdkInstall("pip", ["install", SDK_PYPI]);
|
|
485
|
+
}
|
|
486
|
+
async function rubySdkInstall(cwd) {
|
|
487
|
+
if (await fileExists(cwd, "Gemfile")) {
|
|
488
|
+
return sdkInstall("bundle", ["add", SDK_GEM]);
|
|
489
|
+
}
|
|
490
|
+
return sdkInstall("gem", ["install", SDK_GEM]);
|
|
491
|
+
}
|
|
492
|
+
function runSdkInstall(install, cwd) {
|
|
493
|
+
return new Promise((resolve) => {
|
|
494
|
+
const child = spawn2(install.cmd, install.args, {
|
|
495
|
+
cwd,
|
|
496
|
+
stdio: "inherit",
|
|
497
|
+
shell: process.platform === "win32"
|
|
498
|
+
});
|
|
499
|
+
child.on("error", () => resolve(1));
|
|
500
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
501
|
+
});
|
|
502
|
+
}
|
|
280
503
|
function installCommand(pm, pkg) {
|
|
281
504
|
switch (pm) {
|
|
282
505
|
case "pnpm":
|
|
@@ -304,8 +527,10 @@ function runInstall(pm, pkg, cwd) {
|
|
|
304
527
|
|
|
305
528
|
// src/commands/login.ts
|
|
306
529
|
var SDK_PACKAGE = "@letterapp/node";
|
|
307
|
-
var ENV_FILE = ".env.local";
|
|
308
530
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
531
|
+
function envFileFor(stack) {
|
|
532
|
+
return stack.runtime === "node" ? ".env.local" : ".env";
|
|
533
|
+
}
|
|
309
534
|
function registerLoginCommand(program2) {
|
|
310
535
|
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
536
|
"--api-key <key>",
|
|
@@ -321,11 +546,14 @@ async function runLogin(opts) {
|
|
|
321
546
|
const interactive = process.stdin.isTTY && !opts.yes;
|
|
322
547
|
banner();
|
|
323
548
|
if (opts.apiKey) {
|
|
549
|
+
const envFile = envFileFor(await detectStack(cwd));
|
|
324
550
|
const entries = { LETTER_API_KEY: opts.apiKey };
|
|
325
551
|
if (base !== DEFAULT_BASE_URL) entries.LETTER_BASE_URL = base;
|
|
326
|
-
const file = await upsertEnv(cwd,
|
|
552
|
+
const file = await upsertEnv(cwd, envFile, entries);
|
|
327
553
|
printSuccess(`Saved LETTER_API_KEY to ${rel(cwd, file)} (--api-key).`);
|
|
328
|
-
printWarning(
|
|
554
|
+
printWarning(
|
|
555
|
+
`--api-key is for CI. For interactive setup, run \`${cliCommand()} login\`.`
|
|
556
|
+
);
|
|
329
557
|
return 0;
|
|
330
558
|
}
|
|
331
559
|
let flow;
|
|
@@ -371,51 +599,99 @@ async function runLogin(opts) {
|
|
|
371
599
|
printError(new Error("This login expired. Run the command again to retry."));
|
|
372
600
|
return 1;
|
|
373
601
|
}
|
|
374
|
-
return finish(res
|
|
602
|
+
return finish(res, cwd, opts.install);
|
|
375
603
|
}
|
|
376
604
|
printError(new Error("Timed out waiting for approval. Run the command again."));
|
|
377
605
|
return 1;
|
|
378
606
|
}
|
|
379
|
-
async function finish(
|
|
607
|
+
async function finish(approved, cwd, doInstall) {
|
|
608
|
+
const { api_key: apiKey, pat, base_url: baseUrl, project, workspace } = approved;
|
|
380
609
|
log();
|
|
381
610
|
printSuccess(`Approved for project ${c.bold(project.name)}.`);
|
|
611
|
+
const stack = await detectStack(cwd);
|
|
612
|
+
const isNode = stack.runtime === "node";
|
|
613
|
+
const envFile = envFileFor(stack);
|
|
382
614
|
const entries = { LETTER_API_KEY: apiKey };
|
|
383
615
|
if (baseUrl && baseUrl !== DEFAULT_BASE_URL) entries.LETTER_BASE_URL = baseUrl;
|
|
384
|
-
const
|
|
385
|
-
printSuccess(`Saved LETTER_API_KEY to ${rel(cwd,
|
|
616
|
+
const written = await upsertEnv(cwd, envFile, entries);
|
|
617
|
+
printSuccess(`Saved LETTER_API_KEY to ${rel(cwd, written)}.`);
|
|
386
618
|
try {
|
|
387
619
|
const credFile = await saveCredential({
|
|
388
620
|
apiKey,
|
|
621
|
+
pat,
|
|
389
622
|
baseUrl: baseUrl || DEFAULT_BASE_URL,
|
|
390
623
|
project,
|
|
624
|
+
workspace,
|
|
391
625
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
392
626
|
});
|
|
393
|
-
printSuccess(`Stored credentials in ${tildify(credFile)} for tooling (MCP).`);
|
|
627
|
+
printSuccess(`Stored credentials in ${tildify(credFile)} for tooling (MCP + CLI).`);
|
|
394
628
|
} catch {
|
|
395
629
|
printWarning("Could not write ~/.letter/credentials.json (continuing).");
|
|
396
630
|
}
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
631
|
+
let sdkNote;
|
|
632
|
+
if (isNode) {
|
|
633
|
+
const pm = await detectPackageManager(cwd);
|
|
634
|
+
if (doInstall) {
|
|
635
|
+
printInfo(`Installing ${SDK_PACKAGE} with ${pm}\u2026`);
|
|
636
|
+
const code = await runInstall(pm, SDK_PACKAGE, cwd);
|
|
637
|
+
if (code === 0) printSuccess(`Installed ${SDK_PACKAGE}.`);
|
|
638
|
+
else printWarning(`Install failed. Run: ${installCommand(pm, SDK_PACKAGE)}`);
|
|
639
|
+
} else {
|
|
640
|
+
printInfo(`Skipped install. Run: ${installCommand(pm, SDK_PACKAGE)}`);
|
|
641
|
+
}
|
|
642
|
+
sdkNote = `the ${SDK_PACKAGE} SDK is installed`;
|
|
643
|
+
} else if (stack.runtime === "python" || stack.runtime === "ruby") {
|
|
644
|
+
const install = stack.runtime === "python" ? await pythonSdkInstall(cwd) : await rubySdkInstall(cwd);
|
|
645
|
+
await installSdk(install, doInstall);
|
|
646
|
+
sdkNote = stack.runtime === "python" ? "the letterapp Python SDK is installed" : "the letterapp Ruby gem is installed";
|
|
403
647
|
} else {
|
|
404
|
-
printInfo(
|
|
648
|
+
printInfo(
|
|
649
|
+
`Detected ${stackLabel(stack)}. No ${languageName(stack)} SDK yet, so the agent should use the HTTP API.`
|
|
650
|
+
);
|
|
651
|
+
sdkNote = `there's no official ${languageName(stack)} SDK, so call the HTTP ingestion API directly (POST /v1/identify and /v1/track with Authorization: Bearer LETTER_API_KEY and Content-Type: application/json)`;
|
|
405
652
|
}
|
|
406
|
-
|
|
653
|
+
printAgentHandoff(sdkNote, envFile, cliCommand());
|
|
407
654
|
return 0;
|
|
408
655
|
}
|
|
409
|
-
function
|
|
656
|
+
async function installSdk(install, doInstall) {
|
|
657
|
+
if (doInstall) {
|
|
658
|
+
printInfo(`Installing letterapp (${install.display})\u2026`);
|
|
659
|
+
const code = await runSdkInstall(install, process.cwd());
|
|
660
|
+
if (code === 0) printSuccess("Installed letterapp.");
|
|
661
|
+
else printWarning(`Install failed. Run: ${install.display}`);
|
|
662
|
+
} else {
|
|
663
|
+
printInfo(`Skipped install. Run: ${install.display}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
var AGENT_DOCS = "https://letter.app/docs/agent-setup";
|
|
667
|
+
function printAgentHandoff(sdkNote, envFile, verifyCmd) {
|
|
668
|
+
const rule = " " + "\u2500".repeat(68);
|
|
669
|
+
log();
|
|
670
|
+
log(c.bold("Next: finish setup with your coding agent"));
|
|
410
671
|
log();
|
|
411
|
-
log(c.
|
|
412
|
-
|
|
413
|
-
log(
|
|
414
|
-
log(`
|
|
415
|
-
log(
|
|
672
|
+
log(c.dim(" Paste this to your agent (Cursor, Claude Code, Codex, \u2026):"));
|
|
673
|
+
log(c.dim(rule));
|
|
674
|
+
log(" Integrate Letter (product email automation) into this app. The CLI");
|
|
675
|
+
log(` already did setup: LETTER_API_KEY is in ${envFile} and ${sdkNote}.`);
|
|
676
|
+
log(" Using the right idioms for this codebase:");
|
|
677
|
+
log(" - create a shared, server-side Letter client that reads");
|
|
678
|
+
log(" LETTER_API_KEY from the environment (only set baseUrl/base_url");
|
|
679
|
+
log(" when LETTER_BASE_URL is present),");
|
|
680
|
+
log(" - call identify({ userId, email }) where users sign up or log in,");
|
|
681
|
+
log(" - call track({ userId, event }) on the 2-3 most important actions,");
|
|
682
|
+
log(" - flush before serverless handlers or scripts exit.");
|
|
683
|
+
log(" Never print or commit the key. Then verify with the command below.");
|
|
684
|
+
log(` Full guide: ${AGENT_DOCS}`);
|
|
685
|
+
log(c.dim(rule));
|
|
416
686
|
log();
|
|
417
|
-
log(c.dim(
|
|
418
|
-
log(
|
|
687
|
+
log(c.dim(` Prefer to wire it up yourself? Same guide: ${AGENT_DOCS}`));
|
|
688
|
+
log();
|
|
689
|
+
log(c.dim("Verify it landed: ") + c.bold(`${verifyCmd} status`));
|
|
690
|
+
log(
|
|
691
|
+
c.dim(
|
|
692
|
+
`Your API key is in ${envFile} - keep it out of source control, and don't echo its value.`
|
|
693
|
+
)
|
|
694
|
+
);
|
|
419
695
|
log();
|
|
420
696
|
}
|
|
421
697
|
function rel(cwd, file) {
|
|
@@ -437,7 +713,7 @@ function registerAuthCommands(program2) {
|
|
|
437
713
|
const envKey = process.env.LETTER_API_KEY;
|
|
438
714
|
if (!cred && !envKey) {
|
|
439
715
|
if (isJsonMode()) printJson({ connected: false });
|
|
440
|
-
else printInfo("Not connected. Run " + c.bold(
|
|
716
|
+
else printInfo("Not connected. Run " + c.bold(`${cliCommand()} login`) + " to set up.");
|
|
441
717
|
return;
|
|
442
718
|
}
|
|
443
719
|
if (isJsonMode()) {
|
|
@@ -527,14 +803,595 @@ function registerConfigCommands(program2) {
|
|
|
527
803
|
});
|
|
528
804
|
}
|
|
529
805
|
|
|
806
|
+
// src/commands/resources.ts
|
|
807
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
808
|
+
async function readValue(raw) {
|
|
809
|
+
if (raw.startsWith("@")) return (await readFile4(raw.slice(1), "utf8")).trim();
|
|
810
|
+
return raw;
|
|
811
|
+
}
|
|
812
|
+
async function buildBody(fields, opts) {
|
|
813
|
+
const body = {};
|
|
814
|
+
for (const f of fields) {
|
|
815
|
+
const v = opts[camel(f.key)];
|
|
816
|
+
if (v === void 0) continue;
|
|
817
|
+
if (f.json) {
|
|
818
|
+
body[f.key] = JSON.parse(await readValue(String(v)));
|
|
819
|
+
} else if (f.number) {
|
|
820
|
+
body[f.key] = Number(v);
|
|
821
|
+
} else if (f.boolean) {
|
|
822
|
+
body[f.key] = v === true || v === "true";
|
|
823
|
+
} else {
|
|
824
|
+
body[f.key] = await readValue(String(v));
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return body;
|
|
828
|
+
}
|
|
829
|
+
async function buildForm(fields, opts) {
|
|
830
|
+
const form = new FormData();
|
|
831
|
+
for (const f of fields) {
|
|
832
|
+
const v = opts[camel(f.key)];
|
|
833
|
+
if (v === void 0) continue;
|
|
834
|
+
if (f.file) {
|
|
835
|
+
const path3 = String(v);
|
|
836
|
+
const bytes = await readFile4(path3);
|
|
837
|
+
const name = path3.split("/").pop() || f.key;
|
|
838
|
+
form.append(f.key, new Blob([new Uint8Array(bytes)]), name);
|
|
839
|
+
} else if (f.json) {
|
|
840
|
+
form.append(f.key, await readValue(String(v)));
|
|
841
|
+
} else {
|
|
842
|
+
form.append(f.key, String(v));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return form;
|
|
846
|
+
}
|
|
847
|
+
function buildQuery(fields, opts) {
|
|
848
|
+
const q = {};
|
|
849
|
+
for (const f of fields) {
|
|
850
|
+
const v = opts[camel(f.key)];
|
|
851
|
+
if (v === void 0) continue;
|
|
852
|
+
q[f.key] = f.number ? Number(v) : String(v);
|
|
853
|
+
}
|
|
854
|
+
return q;
|
|
855
|
+
}
|
|
856
|
+
function camel(key) {
|
|
857
|
+
return key.replace(/[-_]([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
858
|
+
}
|
|
859
|
+
function applyFields(cmd, fields) {
|
|
860
|
+
for (const f of fields) {
|
|
861
|
+
cmd.option(f.flag, f.description ?? f.key);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async function basePath(spec, project) {
|
|
865
|
+
const seg = spec.segment ?? spec.name;
|
|
866
|
+
if (!spec.scoped) return `/v1/${seg}`;
|
|
867
|
+
const slug = await resolveProjectSlug(project);
|
|
868
|
+
return `/v1/projects/${slug}/${seg}`;
|
|
869
|
+
}
|
|
870
|
+
function withProject(cmd, scoped) {
|
|
871
|
+
if (scoped) {
|
|
872
|
+
cmd.option("-p, --project <slug>", "Project slug (default: connected project)");
|
|
873
|
+
}
|
|
874
|
+
return cmd;
|
|
875
|
+
}
|
|
876
|
+
function fail(err) {
|
|
877
|
+
printError(err);
|
|
878
|
+
process.exitCode = 1;
|
|
879
|
+
}
|
|
880
|
+
function register(program2, spec) {
|
|
881
|
+
const group = program2.command(spec.name).description(spec.describe);
|
|
882
|
+
if (spec.alias) group.alias(spec.alias);
|
|
883
|
+
const idName = spec.idName ?? "id";
|
|
884
|
+
if (spec.list) {
|
|
885
|
+
const cmd = withProject(
|
|
886
|
+
group.command("list").description(`List ${spec.name}`),
|
|
887
|
+
spec.scoped
|
|
888
|
+
);
|
|
889
|
+
if (spec.list.paginated) {
|
|
890
|
+
cmd.option("--limit <n>", "Max items (1-100)");
|
|
891
|
+
cmd.option("--cursor <cursor>", "Pagination cursor");
|
|
892
|
+
}
|
|
893
|
+
if (spec.list.query) applyFields(cmd, spec.list.query);
|
|
894
|
+
cmd.action(async (opts) => {
|
|
895
|
+
try {
|
|
896
|
+
const query = {};
|
|
897
|
+
if (spec.list?.paginated) {
|
|
898
|
+
if (opts.limit) query.limit = Number(opts.limit);
|
|
899
|
+
if (opts.cursor) query.cursor = opts.cursor;
|
|
900
|
+
}
|
|
901
|
+
if (spec.list?.query) Object.assign(query, buildQuery(spec.list.query, opts));
|
|
902
|
+
const { data } = await mgmt.get(await basePath(spec, opts.project), query);
|
|
903
|
+
emit(data);
|
|
904
|
+
} catch (err) {
|
|
905
|
+
fail(err);
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
if (spec.get) {
|
|
910
|
+
const cmd = withProject(
|
|
911
|
+
group.command("get").description(`Get a ${spec.name} by ${idName}`).argument(`<${idName}>`, idName),
|
|
912
|
+
spec.scoped
|
|
913
|
+
);
|
|
914
|
+
cmd.action(async (idValue, opts) => {
|
|
915
|
+
try {
|
|
916
|
+
const path3 = `${await basePath(spec, opts.project)}/${encodeURIComponent(idValue)}`;
|
|
917
|
+
const { data } = await mgmt.get(path3);
|
|
918
|
+
emit(data);
|
|
919
|
+
} catch (err) {
|
|
920
|
+
fail(err);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
if (spec.create) {
|
|
925
|
+
const cmd = withProject(
|
|
926
|
+
group.command("create").description(`Create a ${spec.name}`),
|
|
927
|
+
spec.scoped
|
|
928
|
+
);
|
|
929
|
+
applyFields(cmd, spec.create);
|
|
930
|
+
cmd.action(async (opts) => {
|
|
931
|
+
try {
|
|
932
|
+
const body = await buildBody(spec.create, opts);
|
|
933
|
+
const { data } = await mgmt.post(await basePath(spec, opts.project), body);
|
|
934
|
+
emit(data);
|
|
935
|
+
} catch (err) {
|
|
936
|
+
fail(err);
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
if (spec.update) {
|
|
941
|
+
const cmd = withProject(
|
|
942
|
+
group.command("update").description(`Update a ${spec.name}`).argument(`<${idName}>`, idName),
|
|
943
|
+
spec.scoped
|
|
944
|
+
);
|
|
945
|
+
applyFields(cmd, spec.update);
|
|
946
|
+
cmd.action(async (idValue, opts) => {
|
|
947
|
+
try {
|
|
948
|
+
const body = await buildBody(spec.update, opts);
|
|
949
|
+
const path3 = `${await basePath(spec, opts.project)}/${encodeURIComponent(idValue)}`;
|
|
950
|
+
const { data } = await mgmt.patch(path3, body);
|
|
951
|
+
emit(data);
|
|
952
|
+
} catch (err) {
|
|
953
|
+
fail(err);
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
if (spec.remove) {
|
|
958
|
+
const cmd = withProject(
|
|
959
|
+
group.command("delete").description(`Delete a ${spec.name}`).argument(`<${idName}>`, idName),
|
|
960
|
+
spec.scoped
|
|
961
|
+
);
|
|
962
|
+
cmd.action(async (idValue, opts) => {
|
|
963
|
+
try {
|
|
964
|
+
const path3 = `${await basePath(spec, opts.project)}/${encodeURIComponent(idValue)}`;
|
|
965
|
+
await mgmt.delete(path3);
|
|
966
|
+
printSuccess(`Deleted ${spec.name.replace(/s$/, "")} ${idValue}.`);
|
|
967
|
+
} catch (err) {
|
|
968
|
+
fail(err);
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
for (const action of spec.actions ?? []) {
|
|
973
|
+
const needsId = action.needsId ?? true;
|
|
974
|
+
const cmd = withProject(
|
|
975
|
+
needsId ? group.command(action.name).description(action.describe).argument(`<${idName}>`, idName) : group.command(action.name).description(action.describe),
|
|
976
|
+
spec.scoped
|
|
977
|
+
);
|
|
978
|
+
if (action.fields) applyFields(cmd, action.fields);
|
|
979
|
+
if (action.query) applyFields(cmd, action.query);
|
|
980
|
+
const handler = async (...args) => {
|
|
981
|
+
const opts = args[args.length - 2];
|
|
982
|
+
const idValue = needsId ? args[0] : void 0;
|
|
983
|
+
try {
|
|
984
|
+
let path3 = await basePath(spec, opts.project);
|
|
985
|
+
if (needsId) path3 += `/${encodeURIComponent(idValue)}`;
|
|
986
|
+
path3 += action.suffix;
|
|
987
|
+
if (action.method === "get") {
|
|
988
|
+
const query = action.query ? buildQuery(action.query, opts) : void 0;
|
|
989
|
+
const { data: data2 } = await mgmt.get(path3, query);
|
|
990
|
+
emit(data2);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (action.multipart && action.fields) {
|
|
994
|
+
const form = await buildForm(action.fields, opts);
|
|
995
|
+
const { data: data2, status: status2 } = await mgmt.request(
|
|
996
|
+
action.method.toUpperCase(),
|
|
997
|
+
path3,
|
|
998
|
+
{ form }
|
|
999
|
+
);
|
|
1000
|
+
if (status2 === 204) printSuccess(`${action.name} ok.`);
|
|
1001
|
+
else emit(data2);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const body = action.fields ? await buildBody(action.fields, opts) : void 0;
|
|
1005
|
+
const { data, status } = await mgmt.request(
|
|
1006
|
+
action.method.toUpperCase(),
|
|
1007
|
+
path3,
|
|
1008
|
+
{ body }
|
|
1009
|
+
);
|
|
1010
|
+
if (status === 204) printSuccess(`${action.name} ok.`);
|
|
1011
|
+
else emit(data);
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
fail(err);
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
cmd.action(handler);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
var cursor = { paginated: true };
|
|
1020
|
+
var SPECS = [
|
|
1021
|
+
// -- Workspace ------------------------------------------------------------
|
|
1022
|
+
{
|
|
1023
|
+
name: "projects",
|
|
1024
|
+
describe: "Manage projects",
|
|
1025
|
+
scoped: false,
|
|
1026
|
+
idName: "slug",
|
|
1027
|
+
list: {},
|
|
1028
|
+
get: true,
|
|
1029
|
+
create: [
|
|
1030
|
+
{ flag: "--name <name>", key: "name" },
|
|
1031
|
+
{ flag: "--timezone <tz>", key: "timezone" }
|
|
1032
|
+
],
|
|
1033
|
+
update: [
|
|
1034
|
+
{ flag: "--name <name>", key: "name" },
|
|
1035
|
+
{ flag: "--timezone <tz>", key: "timezone" }
|
|
1036
|
+
],
|
|
1037
|
+
remove: true
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
name: "members",
|
|
1041
|
+
describe: "Manage workspace members",
|
|
1042
|
+
scoped: false,
|
|
1043
|
+
idName: "userId",
|
|
1044
|
+
list: {},
|
|
1045
|
+
create: [
|
|
1046
|
+
{ flag: "--email <email>", key: "email" },
|
|
1047
|
+
{ flag: "--role <role>", key: "role", description: "member | admin" }
|
|
1048
|
+
],
|
|
1049
|
+
remove: true
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
name: "invitations",
|
|
1053
|
+
describe: "Manage workspace invitations",
|
|
1054
|
+
scoped: false,
|
|
1055
|
+
list: {},
|
|
1056
|
+
create: [
|
|
1057
|
+
{ flag: "--email <email>", key: "email" },
|
|
1058
|
+
{ flag: "--role <role>", key: "role", description: "member | admin" }
|
|
1059
|
+
],
|
|
1060
|
+
remove: true
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
// Personal access tokens (lt_pat_*) authenticate the CLI / Management API.
|
|
1064
|
+
// The dashboard calls these "API keys"; `tokens` stays as a hidden alias.
|
|
1065
|
+
name: "api-keys",
|
|
1066
|
+
alias: "tokens",
|
|
1067
|
+
segment: "tokens",
|
|
1068
|
+
describe: "Manage your personal access tokens (lt_pat_*, the CLI/API credential)",
|
|
1069
|
+
scoped: false,
|
|
1070
|
+
list: {},
|
|
1071
|
+
create: [
|
|
1072
|
+
{ flag: "--name <name>", key: "name" },
|
|
1073
|
+
{
|
|
1074
|
+
flag: "--expires-at <iso>",
|
|
1075
|
+
key: "expires_at",
|
|
1076
|
+
description: "Expiry as an ISO 8601 datetime (default: never)"
|
|
1077
|
+
}
|
|
1078
|
+
],
|
|
1079
|
+
remove: true
|
|
1080
|
+
},
|
|
1081
|
+
// -- Contacts & data ------------------------------------------------------
|
|
1082
|
+
{
|
|
1083
|
+
name: "contacts",
|
|
1084
|
+
describe: "Manage contacts",
|
|
1085
|
+
scoped: true,
|
|
1086
|
+
idName: "externalId",
|
|
1087
|
+
list: { ...cursor },
|
|
1088
|
+
get: true,
|
|
1089
|
+
actions: [
|
|
1090
|
+
{
|
|
1091
|
+
name: "suppress",
|
|
1092
|
+
describe: "Suppress a contact",
|
|
1093
|
+
method: "post",
|
|
1094
|
+
suffix: "/suppress"
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
name: "resubscribe",
|
|
1098
|
+
describe: "Resubscribe a contact",
|
|
1099
|
+
method: "post",
|
|
1100
|
+
suffix: "/resubscribe"
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
name: "import",
|
|
1104
|
+
describe: "Upload a CSV import (--file <path> --mapping <json|@file>)",
|
|
1105
|
+
method: "post",
|
|
1106
|
+
needsId: false,
|
|
1107
|
+
suffix: "/imports",
|
|
1108
|
+
multipart: true,
|
|
1109
|
+
fields: [
|
|
1110
|
+
{ flag: "--file <path>", key: "file", file: true },
|
|
1111
|
+
{ flag: "--mapping <json>", key: "mapping", json: true },
|
|
1112
|
+
{ flag: "--dedupe <strategy>", key: "dedupe", description: "update | skip" },
|
|
1113
|
+
{ flag: "--row-count <n>", key: "rowCount" }
|
|
1114
|
+
]
|
|
1115
|
+
}
|
|
1116
|
+
]
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
name: "accounts",
|
|
1120
|
+
describe: "Inspect accounts",
|
|
1121
|
+
scoped: true,
|
|
1122
|
+
idName: "externalId",
|
|
1123
|
+
list: { ...cursor },
|
|
1124
|
+
get: true
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
name: "events",
|
|
1128
|
+
describe: "List events",
|
|
1129
|
+
scoped: true,
|
|
1130
|
+
list: { ...cursor, query: [{ flag: "--name <name>", key: "name" }] }
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
name: "suppressions",
|
|
1134
|
+
describe: "Manage the suppression list",
|
|
1135
|
+
scoped: true,
|
|
1136
|
+
idName: "email",
|
|
1137
|
+
list: { ...cursor },
|
|
1138
|
+
create: [{ flag: "--email <email>", key: "email" }],
|
|
1139
|
+
remove: true
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
name: "segments",
|
|
1143
|
+
describe: "Manage segments",
|
|
1144
|
+
scoped: true,
|
|
1145
|
+
list: {},
|
|
1146
|
+
get: true,
|
|
1147
|
+
create: [
|
|
1148
|
+
{ flag: "--name <name>", key: "name" },
|
|
1149
|
+
{ flag: "--filter <json>", key: "filter", json: true }
|
|
1150
|
+
],
|
|
1151
|
+
update: [
|
|
1152
|
+
{ flag: "--name <name>", key: "name" },
|
|
1153
|
+
{ flag: "--filter <json>", key: "filter", json: true }
|
|
1154
|
+
],
|
|
1155
|
+
remove: true,
|
|
1156
|
+
actions: [
|
|
1157
|
+
{
|
|
1158
|
+
name: "preview",
|
|
1159
|
+
describe: "Count contacts matching a filter (--filter <json|@file>)",
|
|
1160
|
+
method: "post",
|
|
1161
|
+
needsId: false,
|
|
1162
|
+
suffix: "/preview",
|
|
1163
|
+
fields: [{ flag: "--filter <json>", key: "filter", json: true }]
|
|
1164
|
+
}
|
|
1165
|
+
]
|
|
1166
|
+
},
|
|
1167
|
+
// -- Sequences ------------------------------------------------------------
|
|
1168
|
+
{
|
|
1169
|
+
name: "sequences",
|
|
1170
|
+
describe: "Manage drip sequences",
|
|
1171
|
+
scoped: true,
|
|
1172
|
+
list: {},
|
|
1173
|
+
get: true,
|
|
1174
|
+
create: [{ flag: "--name <name>", key: "name" }],
|
|
1175
|
+
update: [
|
|
1176
|
+
{ flag: "--name <name>", key: "name" },
|
|
1177
|
+
{ flag: "--status <status>", key: "status", description: "active | archived" }
|
|
1178
|
+
],
|
|
1179
|
+
remove: true,
|
|
1180
|
+
actions: [
|
|
1181
|
+
{
|
|
1182
|
+
name: "draft",
|
|
1183
|
+
describe: "Save the draft graph + trigger",
|
|
1184
|
+
method: "put",
|
|
1185
|
+
suffix: "/draft",
|
|
1186
|
+
fields: [
|
|
1187
|
+
{ flag: "--graph <json>", key: "graph", json: true },
|
|
1188
|
+
{ flag: "--trigger <json>", key: "trigger", json: true },
|
|
1189
|
+
{ flag: "--expected-revision <n>", key: "expected_revision", number: true }
|
|
1190
|
+
]
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
name: "publish",
|
|
1194
|
+
describe: "Publish the current draft",
|
|
1195
|
+
method: "post",
|
|
1196
|
+
suffix: "/publish"
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
name: "preview",
|
|
1200
|
+
describe: "Render an email node (--node-id <id>)",
|
|
1201
|
+
method: "post",
|
|
1202
|
+
suffix: "/preview",
|
|
1203
|
+
fields: [{ flag: "--node-id <id>", key: "nodeId" }]
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
name: "test-email",
|
|
1207
|
+
describe: "Send a test email (--node-id <id> --to <email>)",
|
|
1208
|
+
method: "post",
|
|
1209
|
+
suffix: "/test-email",
|
|
1210
|
+
fields: [
|
|
1211
|
+
{ flag: "--node-id <id>", key: "nodeId" },
|
|
1212
|
+
{ flag: "--to <email>", key: "to" }
|
|
1213
|
+
]
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
name: "activity",
|
|
1217
|
+
describe: "Show enrollment activity",
|
|
1218
|
+
method: "get",
|
|
1219
|
+
suffix: "/activity"
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
name: "versions",
|
|
1223
|
+
describe: "List published versions",
|
|
1224
|
+
method: "get",
|
|
1225
|
+
suffix: "/versions"
|
|
1226
|
+
}
|
|
1227
|
+
]
|
|
1228
|
+
},
|
|
1229
|
+
// -- Broadcasts -----------------------------------------------------------
|
|
1230
|
+
{
|
|
1231
|
+
name: "broadcasts",
|
|
1232
|
+
describe: "Manage broadcasts",
|
|
1233
|
+
scoped: true,
|
|
1234
|
+
list: {},
|
|
1235
|
+
get: true,
|
|
1236
|
+
create: [{ flag: "--name <name>", key: "name" }],
|
|
1237
|
+
update: [
|
|
1238
|
+
{ flag: "--name <name>", key: "name" },
|
|
1239
|
+
{ flag: "--subject <subject>", key: "subject" },
|
|
1240
|
+
{ flag: "--preview <preview>", key: "preview" },
|
|
1241
|
+
{ flag: "--audience <json>", key: "audience", json: true },
|
|
1242
|
+
{ flag: "--template-id <id>", key: "template_id" },
|
|
1243
|
+
{ flag: "--body-doc <json>", key: "body_doc", json: true }
|
|
1244
|
+
],
|
|
1245
|
+
remove: true,
|
|
1246
|
+
actions: [
|
|
1247
|
+
{
|
|
1248
|
+
name: "preflight",
|
|
1249
|
+
describe: "Validate before sending",
|
|
1250
|
+
method: "post",
|
|
1251
|
+
suffix: "/preflight"
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
name: "schedule",
|
|
1255
|
+
describe: "Schedule (--scheduled-at <iso>) or send now (omit)",
|
|
1256
|
+
method: "post",
|
|
1257
|
+
suffix: "/schedule",
|
|
1258
|
+
fields: [{ flag: "--scheduled-at <iso>", key: "scheduled_at" }]
|
|
1259
|
+
},
|
|
1260
|
+
{
|
|
1261
|
+
name: "cancel",
|
|
1262
|
+
describe: "Cancel a scheduled or sending broadcast",
|
|
1263
|
+
method: "post",
|
|
1264
|
+
suffix: "/cancel"
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
name: "live",
|
|
1268
|
+
describe: "Live status, stats, and recent activity",
|
|
1269
|
+
method: "get",
|
|
1270
|
+
suffix: "/live"
|
|
1271
|
+
}
|
|
1272
|
+
]
|
|
1273
|
+
},
|
|
1274
|
+
// -- Templates ------------------------------------------------------------
|
|
1275
|
+
{
|
|
1276
|
+
name: "templates",
|
|
1277
|
+
describe: "Manage email templates",
|
|
1278
|
+
scoped: true,
|
|
1279
|
+
list: {},
|
|
1280
|
+
get: true,
|
|
1281
|
+
create: [
|
|
1282
|
+
{ flag: "--name <name>", key: "name" },
|
|
1283
|
+
{ flag: "--design <json>", key: "design", json: true }
|
|
1284
|
+
],
|
|
1285
|
+
update: [
|
|
1286
|
+
{ flag: "--name <name>", key: "name" },
|
|
1287
|
+
{ flag: "--design <json>", key: "design", json: true }
|
|
1288
|
+
],
|
|
1289
|
+
remove: true,
|
|
1290
|
+
actions: [
|
|
1291
|
+
{
|
|
1292
|
+
name: "default",
|
|
1293
|
+
describe: "Set as the project default",
|
|
1294
|
+
method: "post",
|
|
1295
|
+
suffix: "/default"
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
name: "reset",
|
|
1299
|
+
describe: "Reset design to a preset (--preset plain|branded)",
|
|
1300
|
+
method: "post",
|
|
1301
|
+
suffix: "/reset",
|
|
1302
|
+
fields: [{ flag: "--preset <preset>", key: "preset" }]
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
name: "logo",
|
|
1306
|
+
describe: "Upload a logo (--file <path>)",
|
|
1307
|
+
method: "post",
|
|
1308
|
+
suffix: "/logo",
|
|
1309
|
+
multipart: true,
|
|
1310
|
+
fields: [{ flag: "--file <path>", key: "file", file: true }]
|
|
1311
|
+
},
|
|
1312
|
+
{
|
|
1313
|
+
name: "remove-logo",
|
|
1314
|
+
describe: "Remove the template logo",
|
|
1315
|
+
method: "delete",
|
|
1316
|
+
suffix: "/logo"
|
|
1317
|
+
}
|
|
1318
|
+
]
|
|
1319
|
+
},
|
|
1320
|
+
// -- Settings -------------------------------------------------------------
|
|
1321
|
+
{
|
|
1322
|
+
name: "domains",
|
|
1323
|
+
describe: "Manage sending domains",
|
|
1324
|
+
scoped: true,
|
|
1325
|
+
list: {},
|
|
1326
|
+
create: [{ flag: "--domain <domain>", key: "domain" }],
|
|
1327
|
+
remove: true,
|
|
1328
|
+
actions: [
|
|
1329
|
+
{
|
|
1330
|
+
name: "verify",
|
|
1331
|
+
describe: "Re-check DKIM verification",
|
|
1332
|
+
method: "post",
|
|
1333
|
+
suffix: "/verify"
|
|
1334
|
+
}
|
|
1335
|
+
]
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
// Project ingestion keys (lt_live_*) authenticate the SDKs / ingestion API.
|
|
1339
|
+
// The dashboard calls these "Project tokens"; `keys` stays as a hidden alias.
|
|
1340
|
+
name: "project-tokens",
|
|
1341
|
+
alias: "keys",
|
|
1342
|
+
segment: "keys",
|
|
1343
|
+
describe: "Manage project ingestion keys (lt_live_*, the SDK/ingestion credential)",
|
|
1344
|
+
scoped: true,
|
|
1345
|
+
list: {},
|
|
1346
|
+
create: [{ flag: "--name <name>", key: "name" }],
|
|
1347
|
+
remove: true
|
|
1348
|
+
}
|
|
1349
|
+
];
|
|
1350
|
+
function registerResourceCommands(program2) {
|
|
1351
|
+
for (const spec of SPECS) register(program2, spec);
|
|
1352
|
+
program2.command("me").description("Show the token's user, workspace, and role").action(async () => {
|
|
1353
|
+
try {
|
|
1354
|
+
const { data } = await mgmt.get("/v1/me");
|
|
1355
|
+
emit(data);
|
|
1356
|
+
} catch (err) {
|
|
1357
|
+
fail(err);
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
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) => {
|
|
1361
|
+
try {
|
|
1362
|
+
const slug = await resolveProjectSlug(opts.project);
|
|
1363
|
+
const body = {};
|
|
1364
|
+
if (opts.fromEmail !== void 0) body.from_email = opts.fromEmail;
|
|
1365
|
+
if (opts.fromName !== void 0) body.from_name = opts.fromName;
|
|
1366
|
+
if (opts.replyToEmail !== void 0) body.reply_to_email = opts.replyToEmail;
|
|
1367
|
+
const { data } = await mgmt.put(`/v1/projects/${slug}/sender-identity`, body);
|
|
1368
|
+
emit(data);
|
|
1369
|
+
} catch (err) {
|
|
1370
|
+
fail(err);
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
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) => {
|
|
1374
|
+
try {
|
|
1375
|
+
const slug = await resolveProjectSlug(opts.project);
|
|
1376
|
+
const { data } = await mgmt.put(`/v1/projects/${slug}/sending-mode`, {
|
|
1377
|
+
sending_mode: mode
|
|
1378
|
+
});
|
|
1379
|
+
emit(data);
|
|
1380
|
+
} catch (err) {
|
|
1381
|
+
fail(err);
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
|
|
530
1386
|
// src/index.ts
|
|
531
1387
|
var program = new Command();
|
|
532
|
-
program.name("letter").description("Connect your app to Letter, then manage it from the command line").version("0.2
|
|
1388
|
+
program.name("letter").description("Connect your app to Letter, then manage it from the command line").version("0.3.2").option("--json", "Output raw JSON (for scripting / agents)").hook("preAction", (thisCommand) => {
|
|
533
1389
|
if (thisCommand.opts().json) setJsonMode(true);
|
|
534
1390
|
});
|
|
535
1391
|
registerLoginCommand(program);
|
|
536
1392
|
registerAuthCommands(program);
|
|
537
1393
|
registerStatusCommand(program);
|
|
538
1394
|
registerConfigCommands(program);
|
|
1395
|
+
registerResourceCommands(program);
|
|
539
1396
|
program.parseAsync();
|
|
540
1397
|
//# sourceMappingURL=index.js.map
|