@primitivedotdev/cli 0.26.2 → 0.26.3
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/oclif/index.js +12554 -178
- package/dist/oclif/proxy-auto-detect.js +38 -57
- package/package.json +7 -9
- package/dist/oclif/api-command.js +0 -799
- package/dist/oclif/auth.js +0 -223
- package/dist/oclif/commands/doctor.js +0 -361
- package/dist/oclif/commands/emails-latest.js +0 -184
- package/dist/oclif/commands/emails-poll.js +0 -121
- package/dist/oclif/commands/emails-wait.js +0 -171
- package/dist/oclif/commands/emails-watch.js +0 -165
- package/dist/oclif/commands/functions-deploy.js +0 -302
- package/dist/oclif/commands/functions-init.js +0 -374
- package/dist/oclif/commands/functions-redeploy.js +0 -240
- package/dist/oclif/commands/functions-set-secret.js +0 -212
- package/dist/oclif/commands/functions-test-function.js +0 -238
- package/dist/oclif/commands/login.js +0 -236
- package/dist/oclif/commands/logout.js +0 -87
- package/dist/oclif/commands/send.js +0 -221
- package/dist/oclif/commands/whoami.js +0 -94
- package/dist/oclif/endpoints-test-redirect.js +0 -94
- package/dist/oclif/fish-completion.js +0 -87
- package/dist/oclif/lint/raw-send-mail-fetch.js +0 -98
- package/dist/oclif/secret-flags.js +0 -59
- package/oclif.manifest.json +0 -4462
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { Command, Flags } from "@oclif/core";
|
|
2
|
-
import { listEmails, PrimitiveApiClient } from "@primitivedotdev/sdk/api";
|
|
3
|
-
import { extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
|
|
4
|
-
import { resolveCliAuth } from "../auth.js";
|
|
5
|
-
// `primitive emails:latest` is the agent-grade shortcut for "show me
|
|
6
|
-
// the most recent inbound emails as something I can read at a glance."
|
|
7
|
-
// `emails:list-emails` returns the full JSON envelope which is great
|
|
8
|
-
// for piping but blows out the screen for a quick triage. The AGX
|
|
9
|
-
// walkthrough flagged that an agent doing inbox triage had no compact
|
|
10
|
-
// view to reach for; `latest` is that view.
|
|
11
|
-
//
|
|
12
|
-
// Output is a fixed-width text table: short-id, received timestamp,
|
|
13
|
-
// from address, to address, and subject. Subject is truncated for
|
|
14
|
-
// display only; the underlying JSON is unchanged. For machine-readable
|
|
15
|
-
// output, callers should use `emails:list-emails` and pipe to jq.
|
|
16
|
-
const DEFAULT_LIMIT = 10;
|
|
17
|
-
const MAX_LIMIT = 100;
|
|
18
|
-
// Truncation widths chosen so a row fits in ~140 columns total. Long
|
|
19
|
-
// values wrap to "..." rather than blowing out terminal layout.
|
|
20
|
-
const SUBJECT_DISPLAY_WIDTH = 50;
|
|
21
|
-
const ADDRESS_DISPLAY_WIDTH = 32;
|
|
22
|
-
// Two ID widths: the short prefix is for human eyes (interactive
|
|
23
|
-
// TTY), the full UUID is for piped output (a script reading the row
|
|
24
|
-
// as a feed). The short prefix is useless when piped because every
|
|
25
|
-
// other operation requires the full UUID, so the AGX walkthrough
|
|
26
|
-
// kept producing a re-run with `--json` just to recover the id.
|
|
27
|
-
// Auto-switching by `process.stdout.isTTY` makes the common piped
|
|
28
|
-
// case a one-call workflow.
|
|
29
|
-
const ID_DISPLAY_WIDTH_SHORT = 8;
|
|
30
|
-
const ID_DISPLAY_WIDTH_FULL = 36;
|
|
31
|
-
const RECEIVED_DISPLAY_WIDTH = 19;
|
|
32
|
-
// Truncate to width with right-padding; values longer than width are
|
|
33
|
-
// cut to width-3 with a "..." suffix so the output is exactly `width`
|
|
34
|
-
// chars (3 of which are the ellipsis). Display-only; never mutates
|
|
35
|
-
// the underlying value the caller passed in.
|
|
36
|
-
//
|
|
37
|
-
// Width-exact output matters here: formatRow relies on each column
|
|
38
|
-
// being exactly its declared width so columns line up across rows.
|
|
39
|
-
// An overflowing truncate would shift every later column to the
|
|
40
|
-
// right whenever truncation fired (e.g. a row with both addresses
|
|
41
|
-
// truncated would push SUBJECT 4 chars off).
|
|
42
|
-
export function truncate(value, width) {
|
|
43
|
-
if (value.length <= width)
|
|
44
|
-
return value.padEnd(width);
|
|
45
|
-
return `${value.slice(0, width - 3)}...`;
|
|
46
|
-
}
|
|
47
|
-
// Compact ISO timestamp for display: `YYYY-MM-DD HH:MM:SS` in UTC.
|
|
48
|
-
// The full ISO string with milliseconds and `T`/`Z` markers is too
|
|
49
|
-
// dense to scan at a glance; this is the same shape git log uses.
|
|
50
|
-
export function formatReceivedAt(value) {
|
|
51
|
-
if (!value)
|
|
52
|
-
return "-".padEnd(RECEIVED_DISPLAY_WIDTH);
|
|
53
|
-
const d = new Date(value);
|
|
54
|
-
if (Number.isNaN(d.getTime()))
|
|
55
|
-
return value.padEnd(RECEIVED_DISPLAY_WIDTH);
|
|
56
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
57
|
-
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
|
58
|
-
}
|
|
59
|
-
// Decide whether to print the full UUID or the short 8-char prefix
|
|
60
|
-
// based on whether stdout is a TTY. Piped/redirected stdout (the
|
|
61
|
-
// caller is consuming the rows programmatically) gets full UUIDs;
|
|
62
|
-
// interactive terminals get the compact prefix. Pulled out as a
|
|
63
|
-
// helper so tests can drive the rendering branch without touching
|
|
64
|
-
// process.stdout.
|
|
65
|
-
export function pickIdWidth(isTty) {
|
|
66
|
-
return isTty ? ID_DISPLAY_WIDTH_SHORT : ID_DISPLAY_WIDTH_FULL;
|
|
67
|
-
}
|
|
68
|
-
export function formatRow(email, idWidth) {
|
|
69
|
-
// idWidth is one of ID_DISPLAY_WIDTH_SHORT or ID_DISPLAY_WIDTH_FULL.
|
|
70
|
-
// For SHORT, slice the UUID to the prefix length and pad. For FULL,
|
|
71
|
-
// pad to the full UUID width (UUIDs are already 36 chars, so this
|
|
72
|
-
// is effectively just an alignment guarantee for any malformed
|
|
73
|
-
// shorter id).
|
|
74
|
-
const id = truncate(email.id.slice(0, idWidth), idWidth);
|
|
75
|
-
const received = formatReceivedAt(email.received_at);
|
|
76
|
-
const from = truncate(email.sender ?? "", ADDRESS_DISPLAY_WIDTH);
|
|
77
|
-
const to = truncate(email.recipient ?? "", ADDRESS_DISPLAY_WIDTH);
|
|
78
|
-
const subject = (email.subject ?? "").replace(/\s+/g, " ");
|
|
79
|
-
const subjectCol = truncate(subject, SUBJECT_DISPLAY_WIDTH);
|
|
80
|
-
return `${id} ${received} ${from} ${to} ${subjectCol}`;
|
|
81
|
-
}
|
|
82
|
-
export function formatHeader(idWidth) {
|
|
83
|
-
return `${"ID".padEnd(idWidth)} ${"RECEIVED (UTC)".padEnd(RECEIVED_DISPLAY_WIDTH)} ${"FROM".padEnd(ADDRESS_DISPLAY_WIDTH)} ${"TO".padEnd(ADDRESS_DISPLAY_WIDTH)} SUBJECT`;
|
|
84
|
-
}
|
|
85
|
-
class EmailsLatestCommand extends Command {
|
|
86
|
-
static description = `Print the N most recent inbound emails as a one-line-per-row text table. Designed for quick triage and visual scanning. For programmatic access, use \`primitive emails:list-emails\` (full JSON envelope, cursor pagination, filters) or pass \`--json\` here for the same raw shape without pagination/filters.
|
|
87
|
-
|
|
88
|
-
ID display is TTY-aware. When STDOUT is a terminal, the table truncates each row's id to the first ${ID_DISPLAY_WIDTH_SHORT} characters for readability. When STDOUT is piped or redirected (the row stream is being consumed by another command), the full UUID is printed so the id can be fed straight back into \`emails:get-email\`, \`emails:delete-email\`, etc. without a separate \`--json\` round-trip.
|
|
89
|
-
|
|
90
|
-
Output streams: the column header line is written to STDERR so the row data on STDOUT stays grep/awk-friendly. \`--json\` writes everything (including the envelope) to STDOUT and is equivalent to running \`emails:list-emails --limit N\` for the same N.`;
|
|
91
|
-
static summary = "Show the most recent inbound emails as a compact table";
|
|
92
|
-
static examples = [
|
|
93
|
-
"<%= config.bin %> emails latest",
|
|
94
|
-
"<%= config.bin %> emails latest --limit 25",
|
|
95
|
-
"<%= config.bin %> emails latest | head -1 | awk '{print $1}' # full UUID since piped",
|
|
96
|
-
"<%= config.bin %> emails latest --json | jq '.data[0].id'",
|
|
97
|
-
];
|
|
98
|
-
static flags = {
|
|
99
|
-
"api-key": Flags.string({
|
|
100
|
-
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
101
|
-
env: "PRIMITIVE_API_KEY",
|
|
102
|
-
}),
|
|
103
|
-
"api-base-url-1": Flags.string({
|
|
104
|
-
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
105
|
-
env: "PRIMITIVE_API_BASE_URL_1",
|
|
106
|
-
hidden: true,
|
|
107
|
-
}),
|
|
108
|
-
"api-base-url-2": Flags.string({
|
|
109
|
-
description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
110
|
-
env: "PRIMITIVE_API_BASE_URL_2",
|
|
111
|
-
hidden: true,
|
|
112
|
-
}),
|
|
113
|
-
limit: Flags.integer({
|
|
114
|
-
description: `Number of rows to print (1-${MAX_LIMIT}, default ${DEFAULT_LIMIT}).`,
|
|
115
|
-
default: DEFAULT_LIMIT,
|
|
116
|
-
// oclif validates min/max at parse time and emits a consistent
|
|
117
|
-
// out-of-range error before run() is reached, so no manual
|
|
118
|
-
// bounds check is needed here.
|
|
119
|
-
min: 1,
|
|
120
|
-
max: MAX_LIMIT,
|
|
121
|
-
}),
|
|
122
|
-
json: Flags.boolean({
|
|
123
|
-
description: "Print the raw response envelope (with full UUIDs and meta) as JSON on STDOUT instead of the text table. Useful for piping into `jq`, capturing ids for follow-up commands, or scripting.",
|
|
124
|
-
}),
|
|
125
|
-
time: Flags.boolean({
|
|
126
|
-
description: TIME_FLAG_DESCRIPTION,
|
|
127
|
-
}),
|
|
128
|
-
};
|
|
129
|
-
async run() {
|
|
130
|
-
const { flags } = await this.parse(EmailsLatestCommand);
|
|
131
|
-
await runWithTiming(flags.time, async () => {
|
|
132
|
-
const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
|
|
133
|
-
flags["api-base-url-2"] !== undefined;
|
|
134
|
-
const auth = resolveCliAuth({
|
|
135
|
-
apiKey: flags["api-key"],
|
|
136
|
-
apiBaseUrl1: flags["api-base-url-1"],
|
|
137
|
-
apiBaseUrl2: flags["api-base-url-2"],
|
|
138
|
-
configDir: this.config.configDir,
|
|
139
|
-
});
|
|
140
|
-
const apiClient = new PrimitiveApiClient({
|
|
141
|
-
apiKey: auth.apiKey,
|
|
142
|
-
apiBaseUrl1: auth.apiBaseUrl1,
|
|
143
|
-
apiBaseUrl2: auth.apiBaseUrl2,
|
|
144
|
-
});
|
|
145
|
-
const result = await listEmails({
|
|
146
|
-
client: apiClient.client,
|
|
147
|
-
query: { limit: flags.limit },
|
|
148
|
-
responseStyle: "fields",
|
|
149
|
-
});
|
|
150
|
-
if (result.error) {
|
|
151
|
-
const errorPayload = extractErrorPayload(result.error);
|
|
152
|
-
writeErrorWithHints(errorPayload);
|
|
153
|
-
removeStaleSavedCredentialOnUnauthorized({
|
|
154
|
-
auth,
|
|
155
|
-
baseUrlOverridden,
|
|
156
|
-
configDir: this.config.configDir,
|
|
157
|
-
payload: errorPayload,
|
|
158
|
-
});
|
|
159
|
-
process.exitCode = 1;
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const envelope = result.data;
|
|
163
|
-
if (flags.json) {
|
|
164
|
-
// Raw envelope on stdout. Mirrors the shape `emails:list-emails`
|
|
165
|
-
// emits so callers can swap one for the other when they want
|
|
166
|
-
// table vs json without remembering different command names.
|
|
167
|
-
this.log(JSON.stringify(envelope ?? null, null, 2));
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
const rows = envelope?.data ?? [];
|
|
171
|
-
if (rows.length === 0) {
|
|
172
|
-
process.stderr.write("No inbound emails yet. Send an email to one of your verified domains to populate this list.\n");
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const idWidth = pickIdWidth(Boolean(process.stdout.isTTY));
|
|
176
|
-
// Header on stderr so the table itself stays grep-friendly.
|
|
177
|
-
process.stderr.write(`${formatHeader(idWidth)}\n`);
|
|
178
|
-
for (const row of rows) {
|
|
179
|
-
this.log(formatRow(row, idWidth));
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
export default EmailsLatestCommand;
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { searchEmails } from "@primitivedotdev/sdk/api";
|
|
2
|
-
export const DEFAULT_EMAIL_POLL_INTERVAL_SECONDS = 2;
|
|
3
|
-
export const DEFAULT_EMAIL_POLL_PAGE_SIZE = 50;
|
|
4
|
-
export const MAX_EMAIL_POLL_PAGE_SIZE = 100;
|
|
5
|
-
function quoteDslValue(value) {
|
|
6
|
-
if (/^[^\s"]+$/.test(value))
|
|
7
|
-
return value;
|
|
8
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
9
|
-
}
|
|
10
|
-
function combineQ(q, domain) {
|
|
11
|
-
const parts = [
|
|
12
|
-
q?.trim(),
|
|
13
|
-
domain ? `domain:${quoteDslValue(domain.trim())}` : undefined,
|
|
14
|
-
].filter((part) => Boolean(part));
|
|
15
|
-
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
16
|
-
}
|
|
17
|
-
export function normalizeIsoDate(value, label) {
|
|
18
|
-
const parsed = new Date(value);
|
|
19
|
-
if (Number.isNaN(parsed.getTime())) {
|
|
20
|
-
throw new Error(`${label} must be a valid date or ISO-8601 timestamp.`);
|
|
21
|
-
}
|
|
22
|
-
return parsed.toISOString();
|
|
23
|
-
}
|
|
24
|
-
export function filtersFromFlags(flags) {
|
|
25
|
-
return {
|
|
26
|
-
body: flags.body,
|
|
27
|
-
domain: flags.domain,
|
|
28
|
-
domainId: flags["domain-id"],
|
|
29
|
-
from: flags.from,
|
|
30
|
-
hasAttachment: flags["has-attachment"],
|
|
31
|
-
q: flags.q,
|
|
32
|
-
spamScoreGte: flags["spam-score-gte"],
|
|
33
|
-
spamScoreLt: flags["spam-score-lt"],
|
|
34
|
-
subject: flags.subject,
|
|
35
|
-
to: flags.to,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
export function sinceFromFlags(flags) {
|
|
39
|
-
if (flags.since)
|
|
40
|
-
return normalizeIsoDate(flags.since, "--since");
|
|
41
|
-
return flags["include-existing"] ? undefined : new Date().toISOString();
|
|
42
|
-
}
|
|
43
|
-
export function buildEmailSearchQuery(params) {
|
|
44
|
-
const query = {
|
|
45
|
-
include_facets: "false",
|
|
46
|
-
limit: params.pageSize,
|
|
47
|
-
snippet: "false",
|
|
48
|
-
sort: "received_at_asc",
|
|
49
|
-
};
|
|
50
|
-
const q = combineQ(params.filters.q, params.filters.domain);
|
|
51
|
-
if (q)
|
|
52
|
-
query.q = q;
|
|
53
|
-
if (params.filters.body)
|
|
54
|
-
query.body = params.filters.body;
|
|
55
|
-
if (params.filters.domainId)
|
|
56
|
-
query.domain_id = params.filters.domainId;
|
|
57
|
-
if (params.filters.from)
|
|
58
|
-
query.from = params.filters.from;
|
|
59
|
-
if (params.filters.hasAttachment !== undefined) {
|
|
60
|
-
query.has_attachment = params.filters.hasAttachment ? "true" : "false";
|
|
61
|
-
}
|
|
62
|
-
if (params.filters.spamScoreGte !== undefined) {
|
|
63
|
-
query.spam_score_gte = params.filters.spamScoreGte;
|
|
64
|
-
}
|
|
65
|
-
if (params.filters.spamScoreLt !== undefined) {
|
|
66
|
-
query.spam_score_lt = params.filters.spamScoreLt;
|
|
67
|
-
}
|
|
68
|
-
if (params.filters.subject)
|
|
69
|
-
query.subject = params.filters.subject;
|
|
70
|
-
if (params.filters.to)
|
|
71
|
-
query.to = params.filters.to;
|
|
72
|
-
if (params.since)
|
|
73
|
-
query.date_from = params.since;
|
|
74
|
-
if (params.cursor)
|
|
75
|
-
query.cursor = params.cursor;
|
|
76
|
-
return query;
|
|
77
|
-
}
|
|
78
|
-
export function encodeReceivedAtSearchCursor(email) {
|
|
79
|
-
const raw = `r|${new Date(email.received_at).toISOString()}|${email.id}`;
|
|
80
|
-
return Buffer.from(raw, "utf8").toString("base64url");
|
|
81
|
-
}
|
|
82
|
-
export function cursorFromRows(rows) {
|
|
83
|
-
const last = rows.at(-1);
|
|
84
|
-
return last ? encodeReceivedAtSearchCursor(last) : null;
|
|
85
|
-
}
|
|
86
|
-
export function collectNewAcceptedEmails(rows, seenIds) {
|
|
87
|
-
const fresh = [];
|
|
88
|
-
for (const row of rows) {
|
|
89
|
-
if (row.status !== "accepted" && row.status !== "completed")
|
|
90
|
-
continue;
|
|
91
|
-
if (seenIds.has(row.id))
|
|
92
|
-
continue;
|
|
93
|
-
seenIds.add(row.id);
|
|
94
|
-
fresh.push(row);
|
|
95
|
-
}
|
|
96
|
-
return fresh;
|
|
97
|
-
}
|
|
98
|
-
export async function fetchEmailSearchPage(params) {
|
|
99
|
-
const result = await searchEmails({
|
|
100
|
-
client: params.apiClient.client,
|
|
101
|
-
query: buildEmailSearchQuery({
|
|
102
|
-
cursor: params.cursor,
|
|
103
|
-
filters: params.filters,
|
|
104
|
-
pageSize: params.pageSize,
|
|
105
|
-
since: params.since,
|
|
106
|
-
}),
|
|
107
|
-
responseStyle: "fields",
|
|
108
|
-
});
|
|
109
|
-
if (result.error)
|
|
110
|
-
return { ok: false, error: result.error };
|
|
111
|
-
const envelope = result.data;
|
|
112
|
-
const rows = envelope?.data ?? [];
|
|
113
|
-
return {
|
|
114
|
-
ok: true,
|
|
115
|
-
cursor: envelope?.meta.cursor ?? cursorFromRows(rows),
|
|
116
|
-
rows,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
export function sleep(ms) {
|
|
120
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
121
|
-
}
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { Command, Errors, Flags } from "@oclif/core";
|
|
2
|
-
import { PrimitiveApiClient } from "@primitivedotdev/sdk/api";
|
|
3
|
-
import { extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, writeErrorWithHints, } from "../api-command.js";
|
|
4
|
-
import { resolveCliAuth } from "../auth.js";
|
|
5
|
-
import { formatHeader, formatRow, pickIdWidth } from "./emails-latest.js";
|
|
6
|
-
import { collectNewAcceptedEmails, DEFAULT_EMAIL_POLL_INTERVAL_SECONDS, DEFAULT_EMAIL_POLL_PAGE_SIZE, fetchEmailSearchPage, filtersFromFlags, MAX_EMAIL_POLL_PAGE_SIZE, sinceFromFlags, sleep, } from "./emails-poll.js";
|
|
7
|
-
const DEFAULT_WAIT_TIMEOUT_SECONDS = 300;
|
|
8
|
-
function cliError(message) {
|
|
9
|
-
return new Errors.CLIError(message, { exit: 1 });
|
|
10
|
-
}
|
|
11
|
-
class EmailsWaitCommand extends Command {
|
|
12
|
-
static description = "Poll until matching inbound emails arrive, printing each match as it is found.";
|
|
13
|
-
static summary = "Wait for matching inbound emails";
|
|
14
|
-
static examples = [
|
|
15
|
-
"<%= config.bin %> emails wait --to test@example.com",
|
|
16
|
-
"<%= config.bin %> emails wait --subject verify --number 5 --timeout 120",
|
|
17
|
-
"<%= config.bin %> emails wait --q 'domain:example.com' --table",
|
|
18
|
-
];
|
|
19
|
-
static flags = {
|
|
20
|
-
"api-key": Flags.string({
|
|
21
|
-
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
22
|
-
env: "PRIMITIVE_API_KEY",
|
|
23
|
-
}),
|
|
24
|
-
"api-base-url-1": Flags.string({
|
|
25
|
-
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
26
|
-
env: "PRIMITIVE_API_BASE_URL_1",
|
|
27
|
-
hidden: true,
|
|
28
|
-
}),
|
|
29
|
-
"api-base-url-2": Flags.string({
|
|
30
|
-
description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
31
|
-
env: "PRIMITIVE_API_BASE_URL_2",
|
|
32
|
-
hidden: true,
|
|
33
|
-
}),
|
|
34
|
-
body: Flags.string({
|
|
35
|
-
description: "Full-text body filter",
|
|
36
|
-
}),
|
|
37
|
-
domain: Flags.string({
|
|
38
|
-
description: "Filter by inbound email domain",
|
|
39
|
-
}),
|
|
40
|
-
"domain-id": Flags.string({
|
|
41
|
-
description: "Filter by domain UUID",
|
|
42
|
-
}),
|
|
43
|
-
from: Flags.string({
|
|
44
|
-
description: "Filter by sender address or domain",
|
|
45
|
-
}),
|
|
46
|
-
"has-attachment": Flags.boolean({
|
|
47
|
-
description: "Only match emails with one or more attachments",
|
|
48
|
-
}),
|
|
49
|
-
"include-existing": Flags.boolean({
|
|
50
|
-
description: "Start from existing matching emails instead of only new arrivals",
|
|
51
|
-
}),
|
|
52
|
-
interval: Flags.integer({
|
|
53
|
-
default: DEFAULT_EMAIL_POLL_INTERVAL_SECONDS,
|
|
54
|
-
description: "Seconds to wait between empty polls",
|
|
55
|
-
min: 1,
|
|
56
|
-
}),
|
|
57
|
-
number: Flags.integer({
|
|
58
|
-
char: "n",
|
|
59
|
-
default: 1,
|
|
60
|
-
description: "Exit successfully after this many matching emails",
|
|
61
|
-
min: 1,
|
|
62
|
-
}),
|
|
63
|
-
"page-size": Flags.integer({
|
|
64
|
-
default: DEFAULT_EMAIL_POLL_PAGE_SIZE,
|
|
65
|
-
description: `Emails to fetch per poll (1-${MAX_EMAIL_POLL_PAGE_SIZE})`,
|
|
66
|
-
max: MAX_EMAIL_POLL_PAGE_SIZE,
|
|
67
|
-
min: 1,
|
|
68
|
-
}),
|
|
69
|
-
q: Flags.string({
|
|
70
|
-
description: "Full-text search DSL query",
|
|
71
|
-
}),
|
|
72
|
-
since: Flags.string({
|
|
73
|
-
description: "Only match emails received on or after this date/time",
|
|
74
|
-
}),
|
|
75
|
-
"spam-score-gte": Flags.integer({
|
|
76
|
-
description: "Only match emails with spam score greater than or equal to this value",
|
|
77
|
-
}),
|
|
78
|
-
"spam-score-lt": Flags.integer({
|
|
79
|
-
description: "Only match emails with spam score below this value",
|
|
80
|
-
}),
|
|
81
|
-
subject: Flags.string({
|
|
82
|
-
description: "Full-text subject filter",
|
|
83
|
-
}),
|
|
84
|
-
table: Flags.boolean({
|
|
85
|
-
description: "Print a human-readable table instead of JSONL",
|
|
86
|
-
}),
|
|
87
|
-
timeout: Flags.integer({
|
|
88
|
-
default: DEFAULT_WAIT_TIMEOUT_SECONDS,
|
|
89
|
-
description: "Seconds to wait before exiting nonzero; 0 waits forever",
|
|
90
|
-
min: 0,
|
|
91
|
-
}),
|
|
92
|
-
to: Flags.string({
|
|
93
|
-
description: "Filter by recipient address or domain",
|
|
94
|
-
}),
|
|
95
|
-
};
|
|
96
|
-
async run() {
|
|
97
|
-
const { flags } = await this.parse(EmailsWaitCommand);
|
|
98
|
-
const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
|
|
99
|
-
flags["api-base-url-2"] !== undefined;
|
|
100
|
-
const auth = resolveCliAuth({
|
|
101
|
-
apiKey: flags["api-key"],
|
|
102
|
-
apiBaseUrl1: flags["api-base-url-1"],
|
|
103
|
-
apiBaseUrl2: flags["api-base-url-2"],
|
|
104
|
-
configDir: this.config.configDir,
|
|
105
|
-
});
|
|
106
|
-
const apiClient = new PrimitiveApiClient({
|
|
107
|
-
apiKey: auth.apiKey,
|
|
108
|
-
apiBaseUrl1: auth.apiBaseUrl1,
|
|
109
|
-
apiBaseUrl2: auth.apiBaseUrl2,
|
|
110
|
-
});
|
|
111
|
-
let since;
|
|
112
|
-
try {
|
|
113
|
-
since = sinceFromFlags(flags);
|
|
114
|
-
}
|
|
115
|
-
catch (error) {
|
|
116
|
-
throw cliError(error instanceof Error ? error.message : String(error));
|
|
117
|
-
}
|
|
118
|
-
const filters = filtersFromFlags(flags);
|
|
119
|
-
const deadline = flags.timeout === 0 ? null : Date.now() + flags.timeout * 1000;
|
|
120
|
-
const idWidth = pickIdWidth(Boolean(process.stdout.isTTY));
|
|
121
|
-
const seenIds = new Set();
|
|
122
|
-
let cursor = null;
|
|
123
|
-
let matched = 0;
|
|
124
|
-
let headerPrinted = false;
|
|
125
|
-
while (deadline === null || Date.now() < deadline) {
|
|
126
|
-
const page = await fetchEmailSearchPage({
|
|
127
|
-
apiClient,
|
|
128
|
-
cursor,
|
|
129
|
-
filters,
|
|
130
|
-
pageSize: flags["page-size"],
|
|
131
|
-
since,
|
|
132
|
-
});
|
|
133
|
-
if (!page.ok) {
|
|
134
|
-
const payload = extractErrorPayload(page.error);
|
|
135
|
-
writeErrorWithHints(payload);
|
|
136
|
-
removeStaleSavedCredentialOnUnauthorized({
|
|
137
|
-
auth,
|
|
138
|
-
baseUrlOverridden,
|
|
139
|
-
configDir: this.config.configDir,
|
|
140
|
-
payload,
|
|
141
|
-
});
|
|
142
|
-
process.exitCode = 1;
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
cursor = page.cursor ?? cursor;
|
|
146
|
-
for (const email of collectNewAcceptedEmails(page.rows, seenIds)) {
|
|
147
|
-
if (flags.table) {
|
|
148
|
-
if (!headerPrinted) {
|
|
149
|
-
process.stderr.write(`${formatHeader(idWidth)}\n`);
|
|
150
|
-
headerPrinted = true;
|
|
151
|
-
}
|
|
152
|
-
this.log(formatRow(email, idWidth));
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
this.log(JSON.stringify(email));
|
|
156
|
-
}
|
|
157
|
-
matched += 1;
|
|
158
|
-
if (matched >= flags.number)
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
if (page.rows.length > 0)
|
|
162
|
-
continue;
|
|
163
|
-
if (deadline !== null && Date.now() >= deadline)
|
|
164
|
-
break;
|
|
165
|
-
await sleep(flags.interval * 1000);
|
|
166
|
-
}
|
|
167
|
-
process.stderr.write(`Timed out waiting for ${flags.number} matching email${flags.number === 1 ? "" : "s"}; received ${matched}.\n`);
|
|
168
|
-
process.exitCode = 1;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
export default EmailsWaitCommand;
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { Command, Errors, Flags } from "@oclif/core";
|
|
2
|
-
import { PrimitiveApiClient } from "@primitivedotdev/sdk/api";
|
|
3
|
-
import { extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, writeErrorWithHints, } from "../api-command.js";
|
|
4
|
-
import { resolveCliAuth } from "../auth.js";
|
|
5
|
-
import { formatHeader, formatRow, pickIdWidth } from "./emails-latest.js";
|
|
6
|
-
import { collectNewAcceptedEmails, DEFAULT_EMAIL_POLL_INTERVAL_SECONDS, DEFAULT_EMAIL_POLL_PAGE_SIZE, fetchEmailSearchPage, filtersFromFlags, MAX_EMAIL_POLL_PAGE_SIZE, sinceFromFlags, sleep, } from "./emails-poll.js";
|
|
7
|
-
function cliError(message) {
|
|
8
|
-
return new Errors.CLIError(message, { exit: 1 });
|
|
9
|
-
}
|
|
10
|
-
class EmailsWatchCommand extends Command {
|
|
11
|
-
static description = "Poll for new inbound emails and print matching messages as they arrive.";
|
|
12
|
-
static summary = "Watch inbound emails with filters";
|
|
13
|
-
static examples = [
|
|
14
|
-
"<%= config.bin %> emails watch --to support@example.com",
|
|
15
|
-
"<%= config.bin %> emails watch --subject verify --seconds 300",
|
|
16
|
-
"<%= config.bin %> emails watch --number 20 --jsonl",
|
|
17
|
-
];
|
|
18
|
-
static flags = {
|
|
19
|
-
"api-key": Flags.string({
|
|
20
|
-
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
21
|
-
env: "PRIMITIVE_API_KEY",
|
|
22
|
-
}),
|
|
23
|
-
"api-base-url-1": Flags.string({
|
|
24
|
-
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
25
|
-
env: "PRIMITIVE_API_BASE_URL_1",
|
|
26
|
-
hidden: true,
|
|
27
|
-
}),
|
|
28
|
-
"api-base-url-2": Flags.string({
|
|
29
|
-
description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
30
|
-
env: "PRIMITIVE_API_BASE_URL_2",
|
|
31
|
-
hidden: true,
|
|
32
|
-
}),
|
|
33
|
-
body: Flags.string({
|
|
34
|
-
description: "Full-text body filter",
|
|
35
|
-
}),
|
|
36
|
-
domain: Flags.string({
|
|
37
|
-
description: "Filter by inbound email domain",
|
|
38
|
-
}),
|
|
39
|
-
"domain-id": Flags.string({
|
|
40
|
-
description: "Filter by domain UUID",
|
|
41
|
-
}),
|
|
42
|
-
from: Flags.string({
|
|
43
|
-
description: "Filter by sender address or domain",
|
|
44
|
-
}),
|
|
45
|
-
"has-attachment": Flags.boolean({
|
|
46
|
-
description: "Only show emails with one or more attachments",
|
|
47
|
-
}),
|
|
48
|
-
"include-existing": Flags.boolean({
|
|
49
|
-
description: "Start from existing matching emails instead of only new arrivals",
|
|
50
|
-
}),
|
|
51
|
-
interval: Flags.integer({
|
|
52
|
-
default: DEFAULT_EMAIL_POLL_INTERVAL_SECONDS,
|
|
53
|
-
description: "Seconds to wait between empty polls",
|
|
54
|
-
min: 1,
|
|
55
|
-
}),
|
|
56
|
-
jsonl: Flags.boolean({
|
|
57
|
-
description: "Print each email as one JSON object per line",
|
|
58
|
-
}),
|
|
59
|
-
number: Flags.integer({
|
|
60
|
-
description: "Exit after printing this many matching emails",
|
|
61
|
-
min: 1,
|
|
62
|
-
}),
|
|
63
|
-
"page-size": Flags.integer({
|
|
64
|
-
default: DEFAULT_EMAIL_POLL_PAGE_SIZE,
|
|
65
|
-
description: `Emails to fetch per poll (1-${MAX_EMAIL_POLL_PAGE_SIZE})`,
|
|
66
|
-
max: MAX_EMAIL_POLL_PAGE_SIZE,
|
|
67
|
-
min: 1,
|
|
68
|
-
}),
|
|
69
|
-
q: Flags.string({
|
|
70
|
-
description: "Full-text search DSL query",
|
|
71
|
-
}),
|
|
72
|
-
seconds: Flags.integer({
|
|
73
|
-
description: "Exit after this many seconds",
|
|
74
|
-
min: 1,
|
|
75
|
-
}),
|
|
76
|
-
since: Flags.string({
|
|
77
|
-
description: "Only show emails received on or after this date/time",
|
|
78
|
-
}),
|
|
79
|
-
"spam-score-gte": Flags.integer({
|
|
80
|
-
description: "Only show emails with spam score greater than or equal to this value",
|
|
81
|
-
}),
|
|
82
|
-
"spam-score-lt": Flags.integer({
|
|
83
|
-
description: "Only show emails with spam score below this value",
|
|
84
|
-
}),
|
|
85
|
-
subject: Flags.string({
|
|
86
|
-
description: "Full-text subject filter",
|
|
87
|
-
}),
|
|
88
|
-
to: Flags.string({
|
|
89
|
-
description: "Filter by recipient address or domain",
|
|
90
|
-
}),
|
|
91
|
-
};
|
|
92
|
-
async run() {
|
|
93
|
-
const { flags } = await this.parse(EmailsWatchCommand);
|
|
94
|
-
const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
|
|
95
|
-
flags["api-base-url-2"] !== undefined;
|
|
96
|
-
const auth = resolveCliAuth({
|
|
97
|
-
apiKey: flags["api-key"],
|
|
98
|
-
apiBaseUrl1: flags["api-base-url-1"],
|
|
99
|
-
apiBaseUrl2: flags["api-base-url-2"],
|
|
100
|
-
configDir: this.config.configDir,
|
|
101
|
-
});
|
|
102
|
-
const apiClient = new PrimitiveApiClient({
|
|
103
|
-
apiKey: auth.apiKey,
|
|
104
|
-
apiBaseUrl1: auth.apiBaseUrl1,
|
|
105
|
-
apiBaseUrl2: auth.apiBaseUrl2,
|
|
106
|
-
});
|
|
107
|
-
let since;
|
|
108
|
-
try {
|
|
109
|
-
since = sinceFromFlags(flags);
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
throw cliError(error instanceof Error ? error.message : String(error));
|
|
113
|
-
}
|
|
114
|
-
const filters = filtersFromFlags(flags);
|
|
115
|
-
const deadline = flags.seconds ? Date.now() + flags.seconds * 1000 : null;
|
|
116
|
-
const idWidth = pickIdWidth(Boolean(process.stdout.isTTY));
|
|
117
|
-
const seenIds = new Set();
|
|
118
|
-
let cursor = null;
|
|
119
|
-
let printed = 0;
|
|
120
|
-
let headerPrinted = false;
|
|
121
|
-
while (deadline === null || Date.now() < deadline) {
|
|
122
|
-
const page = await fetchEmailSearchPage({
|
|
123
|
-
apiClient,
|
|
124
|
-
cursor,
|
|
125
|
-
filters,
|
|
126
|
-
pageSize: flags["page-size"],
|
|
127
|
-
since,
|
|
128
|
-
});
|
|
129
|
-
if (!page.ok) {
|
|
130
|
-
const payload = extractErrorPayload(page.error);
|
|
131
|
-
writeErrorWithHints(payload);
|
|
132
|
-
removeStaleSavedCredentialOnUnauthorized({
|
|
133
|
-
auth,
|
|
134
|
-
baseUrlOverridden,
|
|
135
|
-
configDir: this.config.configDir,
|
|
136
|
-
payload,
|
|
137
|
-
});
|
|
138
|
-
process.exitCode = 1;
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
cursor = page.cursor ?? cursor;
|
|
142
|
-
for (const email of collectNewAcceptedEmails(page.rows, seenIds)) {
|
|
143
|
-
if (flags.jsonl) {
|
|
144
|
-
this.log(JSON.stringify(email));
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
if (!headerPrinted) {
|
|
148
|
-
process.stderr.write(`${formatHeader(idWidth)}\n`);
|
|
149
|
-
headerPrinted = true;
|
|
150
|
-
}
|
|
151
|
-
this.log(formatRow(email, idWidth));
|
|
152
|
-
}
|
|
153
|
-
printed += 1;
|
|
154
|
-
if (flags.number && printed >= flags.number)
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (page.rows.length > 0)
|
|
158
|
-
continue;
|
|
159
|
-
if (deadline !== null && Date.now() >= deadline)
|
|
160
|
-
break;
|
|
161
|
-
await sleep(flags.interval * 1000);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
export default EmailsWatchCommand;
|