@primitivedotdev/cli 0.26.0 → 0.26.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.
|
@@ -81,6 +81,10 @@ function checkProxy() {
|
|
|
81
81
|
return { status: "ok", message: present.join(", ") };
|
|
82
82
|
}
|
|
83
83
|
function checkApiKey(opts) {
|
|
84
|
+
// Take env explicitly so the unit test can inject a clean
|
|
85
|
+
// environment without mutating process.env across cases. Default to
|
|
86
|
+
// the live process env for real runs.
|
|
87
|
+
const env = opts.env ?? process.env;
|
|
84
88
|
if (opts.apiKey?.startsWith("prim_")) {
|
|
85
89
|
return { status: "ok", message: "provided via flag/env (prim_ prefix)" };
|
|
86
90
|
}
|
|
@@ -91,6 +95,25 @@ function checkApiKey(opts) {
|
|
|
91
95
|
hint: "Verify the key is a Primitive API key, not a value from another service.",
|
|
92
96
|
};
|
|
93
97
|
}
|
|
98
|
+
// PRIMITIVE_KEY rename detection. AGX feedback: users on older docs
|
|
99
|
+
// (or coming from other tools) set PRIMITIVE_KEY and then can't
|
|
100
|
+
// figure out why the CLI says "no API key found". The CLI reads
|
|
101
|
+
// PRIMITIVE_API_KEY only. Surface the rename hint when PRIMITIVE_KEY
|
|
102
|
+
// is set but PRIMITIVE_API_KEY is not, before falling through to
|
|
103
|
+
// the credentials.json / no-key checks. The hint runs before the
|
|
104
|
+
// credentials.json branch on purpose: if both PRIMITIVE_KEY and a
|
|
105
|
+
// valid credentials file are present, the credentials file wins
|
|
106
|
+
// silently and the user never sees the rename suggestion, which is
|
|
107
|
+
// the same trap by another name.
|
|
108
|
+
const primitiveKey = env.PRIMITIVE_KEY;
|
|
109
|
+
const primitiveApiKey = env.PRIMITIVE_API_KEY;
|
|
110
|
+
if ((primitiveKey?.length ?? 0) > 0 && (primitiveApiKey?.length ?? 0) === 0) {
|
|
111
|
+
return {
|
|
112
|
+
status: "fail",
|
|
113
|
+
message: "PRIMITIVE_KEY is set but the CLI reads PRIMITIVE_API_KEY",
|
|
114
|
+
hint: "Rename your env var, or re-run with PRIMITIVE_API_KEY=$PRIMITIVE_KEY.",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
94
117
|
const credsPath = join(opts.configDir, "credentials.json");
|
|
95
118
|
if (existsSync(credsPath)) {
|
|
96
119
|
let parsed = null;
|
|
@@ -18,7 +18,7 @@ import { Args, Command, Errors, Flags } from "@oclif/core";
|
|
|
18
18
|
// the CLI's own @primitivedotdev/sdk dep range in cli-node/package.json
|
|
19
19
|
// so scaffolded projects use the same SDK version the CLI was built
|
|
20
20
|
// and tested against.
|
|
21
|
-
const SDK_VERSION_RANGE = "^0.
|
|
21
|
+
const SDK_VERSION_RANGE = "^0.26.0";
|
|
22
22
|
// The CLI version range that ships in the scaffolded devDependencies.
|
|
23
23
|
// Pinned separately from SDK_VERSION_RANGE because @primitivedotdev/cli
|
|
24
24
|
// and @primitivedotdev/sdk are independent packages on independent
|
|
@@ -184,11 +184,9 @@ export function renderPackageJson(name) {
|
|
|
184
184
|
devDependencies: {
|
|
185
185
|
// @primitivedotdev/cli ships the primitive bin. Including it as
|
|
186
186
|
// a devDep here means `node_modules/.bin/primitive` resolves to
|
|
187
|
-
// the real CLI inside the scaffolded project
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
// "CLI moved" stderr banner. Pinned via CLI_VERSION_RANGE, a
|
|
191
|
-
// dedicated constant so the version is decoupled from the SDK
|
|
187
|
+
// the real CLI inside the scaffolded project so `npm run deploy`
|
|
188
|
+
// works without a global install. Pinned via CLI_VERSION_RANGE,
|
|
189
|
+
// a dedicated constant so the version is decoupled from the SDK
|
|
192
190
|
// range and bumps are explicit on both ends.
|
|
193
191
|
"@primitivedotdev/cli": CLI_VERSION_RANGE,
|
|
194
192
|
esbuild: ESBUILD_VERSION_RANGE,
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Command, Flags } from "@oclif/core";
|
|
2
|
+
import { getEmail, PrimitiveApiClient, testFunction, } from "@primitivedotdev/sdk/api";
|
|
3
|
+
import { API_BASE_URL_1_FLAG_DESCRIPTION, API_BASE_URL_2_FLAG_DESCRIPTION, baseUrlOverriddenFromFlags, extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
|
|
4
|
+
import { resolveCliAuth } from "../auth.js";
|
|
5
|
+
import { DEFAULT_EMAIL_POLL_INTERVAL_SECONDS, fetchEmailSearchPage, sleep, } from "./emails-poll.js";
|
|
6
|
+
// `primitive functions:test-function` is the agent-grade shortcut for
|
|
7
|
+
// triggering a real round-trip and (optionally) waiting for the
|
|
8
|
+
// function to actually run before exiting. The underlying
|
|
9
|
+
// `POST /functions/{id}/test` operation only kicks off a synthetic
|
|
10
|
+
// inbound through MX and returns the queued send id; AGX walkthroughs
|
|
11
|
+
// flagged the missing wait-and-show-sends step as the single biggest
|
|
12
|
+
// time-sink in the verification loop.
|
|
13
|
+
//
|
|
14
|
+
// Shapes:
|
|
15
|
+
// primitive functions:test-function --id <fn-id>
|
|
16
|
+
// Fire-and-forget. Returns the TestInvocationResult JSON
|
|
17
|
+
// (recipient, poll_since, watch_url). Same behavior as the
|
|
18
|
+
// auto-generated functions:test-function it replaces.
|
|
19
|
+
//
|
|
20
|
+
// primitive functions:test-function --id <fn-id> --wait
|
|
21
|
+
// Blocks until the test inbound has arrived AND the function's
|
|
22
|
+
// webhook has fired (or --timeout elapses). Exits non-zero on
|
|
23
|
+
// timeout or on exhausted retries.
|
|
24
|
+
//
|
|
25
|
+
// primitive functions:test-function --id <fn-id> --wait --show-sends
|
|
26
|
+
// Same as --wait, plus prints the inbound's `replies` array
|
|
27
|
+
// (every outbound the function emitted while processing the
|
|
28
|
+
// test inbound), with each send's id, status, recipient,
|
|
29
|
+
// subject, and queue id.
|
|
30
|
+
//
|
|
31
|
+
// The auto-generated functions:test-function entry is filtered out
|
|
32
|
+
// of the generated-command set in oclif/index.ts so this hand-rolled
|
|
33
|
+
// version owns the id.
|
|
34
|
+
const DEFAULT_WAIT_TIMEOUT_SECONDS = 60;
|
|
35
|
+
// Terminal states from the EmailWebhookStatus enum. `fired` means the
|
|
36
|
+
// function returned 2xx; `exhausted` means all retries are spent and
|
|
37
|
+
// the delivery is permanently failed. `pending` / `in_flight` /
|
|
38
|
+
// `failed` are intermediate (`failed` is a temporary failure that may
|
|
39
|
+
// retry into `fired` or eventually `exhausted`), so we keep polling.
|
|
40
|
+
const TERMINAL_WEBHOOK_STATUSES = new Set(["fired", "exhausted"]);
|
|
41
|
+
class FunctionsTestFunctionCommand extends Command {
|
|
42
|
+
static description = "Send a real test email through MX to trigger this function. With --wait, blocks until the function has processed the inbound; with --show-sends, also prints any outbound sends the function emitted in response.";
|
|
43
|
+
static summary = "Trigger a test invocation; with --wait, watch it land";
|
|
44
|
+
static examples = [
|
|
45
|
+
"<%= config.bin %> functions:test-function --id <fn-id>",
|
|
46
|
+
"<%= config.bin %> functions:test-function --id <fn-id> --local-part summarize",
|
|
47
|
+
"<%= config.bin %> functions:test-function --id <fn-id> --wait --show-sends",
|
|
48
|
+
"<%= config.bin %> functions:test-function --id <fn-id> --local-part summarize --wait --timeout 120",
|
|
49
|
+
];
|
|
50
|
+
static flags = {
|
|
51
|
+
"api-key": Flags.string({
|
|
52
|
+
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
53
|
+
env: "PRIMITIVE_API_KEY",
|
|
54
|
+
}),
|
|
55
|
+
"api-base-url-1": Flags.string({
|
|
56
|
+
description: API_BASE_URL_1_FLAG_DESCRIPTION,
|
|
57
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
58
|
+
hidden: true,
|
|
59
|
+
}),
|
|
60
|
+
"api-base-url-2": Flags.string({
|
|
61
|
+
description: API_BASE_URL_2_FLAG_DESCRIPTION,
|
|
62
|
+
env: "PRIMITIVE_API_BASE_URL_2",
|
|
63
|
+
hidden: true,
|
|
64
|
+
}),
|
|
65
|
+
id: Flags.string({
|
|
66
|
+
description: "Function id (UUID).",
|
|
67
|
+
required: true,
|
|
68
|
+
}),
|
|
69
|
+
"local-part": Flags.string({
|
|
70
|
+
description: "Override the synthetic local-part the test inbound is addressed to. Otherwise the runtime picks `__primitive_function_test+<random>`.",
|
|
71
|
+
}),
|
|
72
|
+
wait: Flags.boolean({
|
|
73
|
+
description: "Block until the function has processed the test inbound (webhook status is `fired` or `exhausted`) or --timeout elapses. Exits non-zero on timeout or on exhausted retries.",
|
|
74
|
+
}),
|
|
75
|
+
"show-sends": Flags.boolean({
|
|
76
|
+
description: "When the wait resolves, also print the outbound emails the function emitted while processing the test inbound (id, status, to, subject). Implies --wait.",
|
|
77
|
+
}),
|
|
78
|
+
timeout: Flags.integer({
|
|
79
|
+
default: DEFAULT_WAIT_TIMEOUT_SECONDS,
|
|
80
|
+
description: "Seconds to wait before exiting non-zero when --wait is set; 0 waits forever.",
|
|
81
|
+
min: 0,
|
|
82
|
+
}),
|
|
83
|
+
"poll-interval": Flags.integer({
|
|
84
|
+
default: DEFAULT_EMAIL_POLL_INTERVAL_SECONDS,
|
|
85
|
+
description: "Seconds between polls while waiting.",
|
|
86
|
+
min: 1,
|
|
87
|
+
}),
|
|
88
|
+
time: Flags.boolean({
|
|
89
|
+
description: TIME_FLAG_DESCRIPTION,
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
async run() {
|
|
93
|
+
const { flags } = await this.parse(FunctionsTestFunctionCommand);
|
|
94
|
+
// --show-sends implies --wait. You can't print what was sent
|
|
95
|
+
// until the function has actually run.
|
|
96
|
+
const shouldWait = flags.wait || flags["show-sends"];
|
|
97
|
+
const shouldShowSends = flags["show-sends"];
|
|
98
|
+
const baseUrlOverridden = baseUrlOverriddenFromFlags(flags);
|
|
99
|
+
const auth = resolveCliAuth({
|
|
100
|
+
apiKey: flags["api-key"],
|
|
101
|
+
apiBaseUrl1: flags["api-base-url-1"],
|
|
102
|
+
apiBaseUrl2: flags["api-base-url-2"],
|
|
103
|
+
configDir: this.config.configDir,
|
|
104
|
+
});
|
|
105
|
+
const apiClient = new PrimitiveApiClient({
|
|
106
|
+
apiKey: auth.apiKey,
|
|
107
|
+
apiBaseUrl1: auth.apiBaseUrl1,
|
|
108
|
+
apiBaseUrl2: auth.apiBaseUrl2,
|
|
109
|
+
});
|
|
110
|
+
await runWithTiming(flags.time, async () => {
|
|
111
|
+
// 1. Trigger the test send.
|
|
112
|
+
const triggerResult = await testFunction({
|
|
113
|
+
client: apiClient.client,
|
|
114
|
+
path: { id: flags.id },
|
|
115
|
+
body: flags["local-part"]
|
|
116
|
+
? { local_part: flags["local-part"] }
|
|
117
|
+
: undefined,
|
|
118
|
+
responseStyle: "fields",
|
|
119
|
+
});
|
|
120
|
+
if (triggerResult.error) {
|
|
121
|
+
const payload = extractErrorPayload(triggerResult.error);
|
|
122
|
+
writeErrorWithHints(payload);
|
|
123
|
+
removeStaleSavedCredentialOnUnauthorized({
|
|
124
|
+
auth,
|
|
125
|
+
baseUrlOverridden,
|
|
126
|
+
configDir: this.config.configDir,
|
|
127
|
+
payload,
|
|
128
|
+
});
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const invocation = triggerResult.data
|
|
133
|
+
.data;
|
|
134
|
+
if (!shouldWait) {
|
|
135
|
+
// Fire-and-forget path: print the TestInvocationResult JSON
|
|
136
|
+
// unchanged. Same shape the auto-generated command emitted.
|
|
137
|
+
this.log(JSON.stringify(invocation, null, 2));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const startedAt = Date.now();
|
|
141
|
+
const timeoutMs = flags.timeout * 1000;
|
|
142
|
+
const pollIntervalMs = flags["poll-interval"] * 1000;
|
|
143
|
+
const isExpired = () => flags.timeout > 0 && Date.now() - startedAt > timeoutMs;
|
|
144
|
+
// 2. Wait for the test inbound to arrive. The synthetic
|
|
145
|
+
// recipient is unique per call (random suffix in the local-part
|
|
146
|
+
// unless --local-part overrides), so `to` + `since` uniquely
|
|
147
|
+
// identifies the test inbound row.
|
|
148
|
+
this.log(`Waiting for test inbound to arrive at ${invocation.to}...`);
|
|
149
|
+
let inboundId;
|
|
150
|
+
while (!isExpired()) {
|
|
151
|
+
const page = await fetchEmailSearchPage({
|
|
152
|
+
apiClient,
|
|
153
|
+
filters: { to: invocation.to },
|
|
154
|
+
pageSize: 25,
|
|
155
|
+
since: invocation.poll_since,
|
|
156
|
+
});
|
|
157
|
+
if (!page.ok) {
|
|
158
|
+
const payload = extractErrorPayload(page.error);
|
|
159
|
+
writeErrorWithHints(payload);
|
|
160
|
+
removeStaleSavedCredentialOnUnauthorized({
|
|
161
|
+
auth,
|
|
162
|
+
baseUrlOverridden,
|
|
163
|
+
configDir: this.config.configDir,
|
|
164
|
+
payload,
|
|
165
|
+
});
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const found = page.rows[0];
|
|
170
|
+
if (found) {
|
|
171
|
+
inboundId = found.id;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
await sleep(pollIntervalMs);
|
|
175
|
+
}
|
|
176
|
+
if (!inboundId) {
|
|
177
|
+
this.error(`Timed out after ${flags.timeout}s waiting for test inbound ${invocation.to} to land. Browse ${invocation.watch_url} for the live view.`, { exit: 2 });
|
|
178
|
+
}
|
|
179
|
+
// 3. Wait for the function (webhook) to actually run. We poll
|
|
180
|
+
// the email-detail endpoint because it already carries both the
|
|
181
|
+
// webhook_status terminal state and the `replies` array we'll
|
|
182
|
+
// print under --show-sends. No second endpoint needed.
|
|
183
|
+
this.log(`Inbound landed (${inboundId}). Waiting for function to run...`);
|
|
184
|
+
let detail;
|
|
185
|
+
while (!isExpired()) {
|
|
186
|
+
const result = await getEmail({
|
|
187
|
+
client: apiClient.client,
|
|
188
|
+
path: { id: inboundId },
|
|
189
|
+
responseStyle: "fields",
|
|
190
|
+
});
|
|
191
|
+
if (result.error) {
|
|
192
|
+
const payload = extractErrorPayload(result.error);
|
|
193
|
+
writeErrorWithHints(payload);
|
|
194
|
+
removeStaleSavedCredentialOnUnauthorized({
|
|
195
|
+
auth,
|
|
196
|
+
baseUrlOverridden,
|
|
197
|
+
configDir: this.config.configDir,
|
|
198
|
+
payload,
|
|
199
|
+
});
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const fetched = result.data.data;
|
|
204
|
+
if (fetched.webhook_status &&
|
|
205
|
+
TERMINAL_WEBHOOK_STATUSES.has(fetched.webhook_status)) {
|
|
206
|
+
detail = fetched;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
await sleep(pollIntervalMs);
|
|
210
|
+
}
|
|
211
|
+
if (!detail) {
|
|
212
|
+
this.error(`Timed out after ${flags.timeout}s waiting for function webhook to fire for inbound ${inboundId}. Browse ${invocation.watch_url} for the live view.`, { exit: 2 });
|
|
213
|
+
}
|
|
214
|
+
// 4. Emit the outcome.
|
|
215
|
+
const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000);
|
|
216
|
+
const outcome = {
|
|
217
|
+
function_id: flags.id,
|
|
218
|
+
inbound_id: inboundId,
|
|
219
|
+
inbound_to: invocation.to,
|
|
220
|
+
webhook_status: detail.webhook_status,
|
|
221
|
+
webhook_attempt_count: detail.webhook_attempt_count,
|
|
222
|
+
webhook_last_status_code: detail.webhook_last_status_code,
|
|
223
|
+
webhook_last_error: detail.webhook_last_error,
|
|
224
|
+
elapsed_seconds: elapsedSeconds,
|
|
225
|
+
};
|
|
226
|
+
if (shouldShowSends) {
|
|
227
|
+
outcome.sent_emails = detail.replies;
|
|
228
|
+
}
|
|
229
|
+
this.log(JSON.stringify(outcome, null, 2));
|
|
230
|
+
// Exit non-zero when the function failed permanently so CI
|
|
231
|
+
// scripts can gate on the exit code.
|
|
232
|
+
if (detail.webhook_status === "exhausted") {
|
|
233
|
+
process.exitCode = 1;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export default FunctionsTestFunctionCommand;
|
package/dist/oclif/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import FunctionsDeployCommand from "./commands/functions-deploy.js";
|
|
|
9
9
|
import FunctionsInitCommand from "./commands/functions-init.js";
|
|
10
10
|
import FunctionsRedeployCommand from "./commands/functions-redeploy.js";
|
|
11
11
|
import FunctionsSetSecretCommand from "./commands/functions-set-secret.js";
|
|
12
|
+
import FunctionsTestFunctionCommand from "./commands/functions-test-function.js";
|
|
12
13
|
import LoginCommand from "./commands/login.js";
|
|
13
14
|
import LogoutCommand from "./commands/logout.js";
|
|
14
15
|
import SendCommand from "./commands/send.js";
|
|
@@ -108,7 +109,17 @@ class CompletionCommand extends Command {
|
|
|
108
109
|
function commandId(operation) {
|
|
109
110
|
return `${operation.tagCommand}:${operation.command}`;
|
|
110
111
|
}
|
|
111
|
-
|
|
112
|
+
// Operation ids whose surface is owned by a hand-rolled command in
|
|
113
|
+
// COMMANDS below. The auto-generated wrapper is filtered out so the
|
|
114
|
+
// hand-rolled command owns the id without a name collision.
|
|
115
|
+
const OVERRIDDEN_OPERATION_IDS = new Set([
|
|
116
|
+
// `functions:test-function` is hand-rolled to add --wait, --show-sends,
|
|
117
|
+
// and --timeout flags on top of the auto-generated POST /functions/{id}/test.
|
|
118
|
+
"functions:test-function",
|
|
119
|
+
]);
|
|
120
|
+
const generatedCommands = Object.fromEntries(operationManifest
|
|
121
|
+
.filter((operation) => !OVERRIDDEN_OPERATION_IDS.has(commandId(operation)))
|
|
122
|
+
.map((operation) => [
|
|
112
123
|
commandId(operation),
|
|
113
124
|
createOperationCommand(operation),
|
|
114
125
|
]));
|
|
@@ -171,5 +182,12 @@ export const COMMANDS = {
|
|
|
171
182
|
// visible to the running handler requires a separate redeploy,
|
|
172
183
|
// which this shortcut folds in via --redeploy.
|
|
173
184
|
"functions:set-secret": FunctionsSetSecretCommand,
|
|
185
|
+
// `functions:test-function` is hand-rolled to add --wait, --show-sends,
|
|
186
|
+
// and --timeout on top of POST /functions/{id}/test. Without those
|
|
187
|
+
// flags, agents had to manually thread queued-send + emails:wait +
|
|
188
|
+
// emails:get-email + sending:list-sent-emails to verify a function
|
|
189
|
+
// ran and see what it emitted; AGX walkthroughs flagged that loop as
|
|
190
|
+
// the single biggest verification time-sink.
|
|
191
|
+
"functions:test-function": FunctionsTestFunctionCommand,
|
|
174
192
|
...generatedCommands,
|
|
175
193
|
};
|
package/oclif.manifest.json
CHANGED
|
@@ -1036,6 +1036,103 @@
|
|
|
1036
1036
|
"summary": "Write a function secret (optionally redeploying to push it live)",
|
|
1037
1037
|
"enableJsonFlag": false
|
|
1038
1038
|
},
|
|
1039
|
+
"functions:test-function": {
|
|
1040
|
+
"aliases": [],
|
|
1041
|
+
"args": {},
|
|
1042
|
+
"description": "Send a real test email through MX to trigger this function. With --wait, blocks until the function has processed the inbound; with --show-sends, also prints any outbound sends the function emitted in response.",
|
|
1043
|
+
"examples": [
|
|
1044
|
+
"<%= config.bin %> functions:test-function --id <fn-id>",
|
|
1045
|
+
"<%= config.bin %> functions:test-function --id <fn-id> --local-part summarize",
|
|
1046
|
+
"<%= config.bin %> functions:test-function --id <fn-id> --wait --show-sends",
|
|
1047
|
+
"<%= config.bin %> functions:test-function --id <fn-id> --local-part summarize --wait --timeout 120"
|
|
1048
|
+
],
|
|
1049
|
+
"flags": {
|
|
1050
|
+
"api-key": {
|
|
1051
|
+
"description": "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
1052
|
+
"env": "PRIMITIVE_API_KEY",
|
|
1053
|
+
"name": "api-key",
|
|
1054
|
+
"hasDynamicHelp": false,
|
|
1055
|
+
"multiple": false,
|
|
1056
|
+
"type": "option"
|
|
1057
|
+
},
|
|
1058
|
+
"api-base-url-1": {
|
|
1059
|
+
"description": "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
1060
|
+
"env": "PRIMITIVE_API_BASE_URL_1",
|
|
1061
|
+
"hidden": true,
|
|
1062
|
+
"name": "api-base-url-1",
|
|
1063
|
+
"hasDynamicHelp": false,
|
|
1064
|
+
"multiple": false,
|
|
1065
|
+
"type": "option"
|
|
1066
|
+
},
|
|
1067
|
+
"api-base-url-2": {
|
|
1068
|
+
"description": "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
1069
|
+
"env": "PRIMITIVE_API_BASE_URL_2",
|
|
1070
|
+
"hidden": true,
|
|
1071
|
+
"name": "api-base-url-2",
|
|
1072
|
+
"hasDynamicHelp": false,
|
|
1073
|
+
"multiple": false,
|
|
1074
|
+
"type": "option"
|
|
1075
|
+
},
|
|
1076
|
+
"id": {
|
|
1077
|
+
"description": "Function id (UUID).",
|
|
1078
|
+
"name": "id",
|
|
1079
|
+
"required": true,
|
|
1080
|
+
"hasDynamicHelp": false,
|
|
1081
|
+
"multiple": false,
|
|
1082
|
+
"type": "option"
|
|
1083
|
+
},
|
|
1084
|
+
"local-part": {
|
|
1085
|
+
"description": "Override the synthetic local-part the test inbound is addressed to. Otherwise the runtime picks `__primitive_function_test+<random>`.",
|
|
1086
|
+
"name": "local-part",
|
|
1087
|
+
"hasDynamicHelp": false,
|
|
1088
|
+
"multiple": false,
|
|
1089
|
+
"type": "option"
|
|
1090
|
+
},
|
|
1091
|
+
"wait": {
|
|
1092
|
+
"description": "Block until the function has processed the test inbound (webhook status is `fired` or `exhausted`) or --timeout elapses. Exits non-zero on timeout or on exhausted retries.",
|
|
1093
|
+
"name": "wait",
|
|
1094
|
+
"allowNo": false,
|
|
1095
|
+
"type": "boolean"
|
|
1096
|
+
},
|
|
1097
|
+
"show-sends": {
|
|
1098
|
+
"description": "When the wait resolves, also print the outbound emails the function emitted while processing the test inbound (id, status, to, subject). Implies --wait.",
|
|
1099
|
+
"name": "show-sends",
|
|
1100
|
+
"allowNo": false,
|
|
1101
|
+
"type": "boolean"
|
|
1102
|
+
},
|
|
1103
|
+
"timeout": {
|
|
1104
|
+
"description": "Seconds to wait before exiting non-zero when --wait is set; 0 waits forever.",
|
|
1105
|
+
"name": "timeout",
|
|
1106
|
+
"default": 60,
|
|
1107
|
+
"hasDynamicHelp": false,
|
|
1108
|
+
"multiple": false,
|
|
1109
|
+
"type": "option"
|
|
1110
|
+
},
|
|
1111
|
+
"poll-interval": {
|
|
1112
|
+
"description": "Seconds between polls while waiting.",
|
|
1113
|
+
"name": "poll-interval",
|
|
1114
|
+
"default": 2,
|
|
1115
|
+
"hasDynamicHelp": false,
|
|
1116
|
+
"multiple": false,
|
|
1117
|
+
"type": "option"
|
|
1118
|
+
},
|
|
1119
|
+
"time": {
|
|
1120
|
+
"description": "Print the wall-clock duration of this command to stderr after it completes (e.g. `[time: 1.34s]`). Useful for measuring `--wait` send latency, comparing CLI overhead, or capturing timing in scripts.",
|
|
1121
|
+
"name": "time",
|
|
1122
|
+
"allowNo": false,
|
|
1123
|
+
"type": "boolean"
|
|
1124
|
+
}
|
|
1125
|
+
},
|
|
1126
|
+
"hasDynamicHelp": false,
|
|
1127
|
+
"hiddenAliases": [],
|
|
1128
|
+
"id": "functions:test-function",
|
|
1129
|
+
"pluginAlias": "@primitivedotdev/cli",
|
|
1130
|
+
"pluginName": "@primitivedotdev/cli",
|
|
1131
|
+
"pluginType": "core",
|
|
1132
|
+
"strict": true,
|
|
1133
|
+
"summary": "Trigger a test invocation; with --wait, watch it land",
|
|
1134
|
+
"enableJsonFlag": false
|
|
1135
|
+
},
|
|
1039
1136
|
"account:get-account": {
|
|
1040
1137
|
"aliases": [],
|
|
1041
1138
|
"args": {},
|
|
@@ -3095,7 +3192,7 @@
|
|
|
3095
3192
|
"functions:create-function": {
|
|
3096
3193
|
"aliases": [],
|
|
3097
3194
|
"args": {},
|
|
3098
|
-
"description": "Creates and deploys a new function. The handler must be a single\nESM module
|
|
3195
|
+
"description": "Creates and deploys a new function. The handler must be a single\nESM module whose default export is an object with an async\n`fetch(request, env)` method (Workers-style). The gateway\nHMAC-verifies the POST against the org's webhook secret before\ninvoking the handler; the request body parses to an\n`email.received` event (see `EmailReceivedEvent` and the\nWebhook payload section for the full schema). Code is bundled\nbefore being uploaded; ship a single self-contained file rather\nthan relying on external imports.\n\n**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`\n(optional) is capped at 5 MiB UTF-8 and is stored only on the\nedge runtime side; it is not persisted in Primitive's database.\n\n**Auto-wiring.** On successful deploy, Primitive automatically\ncreates a webhook endpoint that delivers inbound mail to the\nfunction. There is nothing to configure on the Endpoints API\nfor this to work; the gateway URL returned here is for\nreference only and is not directly callable from outside.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`) already\nbound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n\n\nTip: prefer `primitive functions:deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
|
|
3099
3196
|
"flags": {
|
|
3100
3197
|
"api-key": {
|
|
3101
3198
|
"description": "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
@@ -3435,10 +3532,10 @@
|
|
|
3435
3532
|
"summary": "Get a function",
|
|
3436
3533
|
"enableJsonFlag": false
|
|
3437
3534
|
},
|
|
3438
|
-
"functions:list-function-
|
|
3535
|
+
"functions:list-function-logs": {
|
|
3439
3536
|
"aliases": [],
|
|
3440
3537
|
"args": {},
|
|
3441
|
-
"description": "Returns
|
|
3538
|
+
"description": "Returns the most recent `function_logs` rows for the function,\nnewest first. Each row is a single `console.log` / `console.error`\ninvocation captured from the running handler.\n\nPage through history with the opaque `cursor` returned as\n`next_cursor`; pass it back as the `cursor` query param on the\nnext call. `next_cursor` is `null` when there are no further\nrows. The cursor format is an implementation detail and should\nnot be parsed by callers.\n",
|
|
3442
3539
|
"flags": {
|
|
3443
3540
|
"api-key": {
|
|
3444
3541
|
"description": "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
@@ -3479,22 +3576,38 @@
|
|
|
3479
3576
|
"hasDynamicHelp": false,
|
|
3480
3577
|
"multiple": false,
|
|
3481
3578
|
"type": "option"
|
|
3579
|
+
},
|
|
3580
|
+
"limit": {
|
|
3581
|
+
"description": "Maximum number of rows to return. Clamped to 1..200; default\n50.\n",
|
|
3582
|
+
"name": "limit",
|
|
3583
|
+
"required": false,
|
|
3584
|
+
"hasDynamicHelp": false,
|
|
3585
|
+
"multiple": false,
|
|
3586
|
+
"type": "option"
|
|
3587
|
+
},
|
|
3588
|
+
"cursor": {
|
|
3589
|
+
"description": "Opaque pagination cursor from a previous response's\n`next_cursor`. Omit on the first call.\n",
|
|
3590
|
+
"name": "cursor",
|
|
3591
|
+
"required": false,
|
|
3592
|
+
"hasDynamicHelp": false,
|
|
3593
|
+
"multiple": false,
|
|
3594
|
+
"type": "option"
|
|
3482
3595
|
}
|
|
3483
3596
|
},
|
|
3484
3597
|
"hasDynamicHelp": false,
|
|
3485
3598
|
"hiddenAliases": [],
|
|
3486
|
-
"id": "functions:list-function-
|
|
3599
|
+
"id": "functions:list-function-logs",
|
|
3487
3600
|
"pluginAlias": "@primitivedotdev/cli",
|
|
3488
3601
|
"pluginName": "@primitivedotdev/cli",
|
|
3489
3602
|
"pluginType": "core",
|
|
3490
3603
|
"strict": true,
|
|
3491
|
-
"summary": "List a function's
|
|
3604
|
+
"summary": "List a function's execution logs",
|
|
3492
3605
|
"enableJsonFlag": false
|
|
3493
3606
|
},
|
|
3494
|
-
"functions:list-
|
|
3607
|
+
"functions:list-function-secrets": {
|
|
3495
3608
|
"aliases": [],
|
|
3496
3609
|
"args": {},
|
|
3497
|
-
"description": "Returns every
|
|
3610
|
+
"description": "Returns metadata for every secret bound to the function, with\nmanaged entries (provisioned by Primitive) listed first and\nuser-set entries listed alphabetically after. **Values are\nnever returned.** Secret writes are write-only.\n\nManaged entries (e.g. `PRIMITIVE_WEBHOOK_SECRET`,\n`PRIMITIVE_API_KEY`) carry a `description` instead of\n`created_at` / `updated_at`. They cannot be created, updated,\nor deleted via this API.\n",
|
|
3498
3611
|
"flags": {
|
|
3499
3612
|
"api-key": {
|
|
3500
3613
|
"description": "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
@@ -3527,22 +3640,30 @@
|
|
|
3527
3640
|
"name": "time",
|
|
3528
3641
|
"allowNo": false,
|
|
3529
3642
|
"type": "boolean"
|
|
3643
|
+
},
|
|
3644
|
+
"id": {
|
|
3645
|
+
"description": "Resource UUID",
|
|
3646
|
+
"name": "id",
|
|
3647
|
+
"required": true,
|
|
3648
|
+
"hasDynamicHelp": false,
|
|
3649
|
+
"multiple": false,
|
|
3650
|
+
"type": "option"
|
|
3530
3651
|
}
|
|
3531
3652
|
},
|
|
3532
3653
|
"hasDynamicHelp": false,
|
|
3533
3654
|
"hiddenAliases": [],
|
|
3534
|
-
"id": "functions:list-
|
|
3655
|
+
"id": "functions:list-function-secrets",
|
|
3535
3656
|
"pluginAlias": "@primitivedotdev/cli",
|
|
3536
3657
|
"pluginName": "@primitivedotdev/cli",
|
|
3537
3658
|
"pluginType": "core",
|
|
3538
3659
|
"strict": true,
|
|
3539
|
-
"summary": "List
|
|
3660
|
+
"summary": "List a function's secrets",
|
|
3540
3661
|
"enableJsonFlag": false
|
|
3541
3662
|
},
|
|
3542
|
-
"functions:
|
|
3663
|
+
"functions:list-functions": {
|
|
3543
3664
|
"aliases": [],
|
|
3544
3665
|
"args": {},
|
|
3545
|
-
"description": "
|
|
3666
|
+
"description": "Returns every active (non-deleted) function in the org, newest\nfirst. Each entry carries the deploy status and the gateway URL\nthat the platform's webhook delivery loop posts to. To inspect\nthe source code or deploy errors, use `GET /functions/{id}`.\n",
|
|
3546
3667
|
"flags": {
|
|
3547
3668
|
"api-key": {
|
|
3548
3669
|
"description": "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
@@ -3575,59 +3696,22 @@
|
|
|
3575
3696
|
"name": "time",
|
|
3576
3697
|
"allowNo": false,
|
|
3577
3698
|
"type": "boolean"
|
|
3578
|
-
},
|
|
3579
|
-
"id": {
|
|
3580
|
-
"description": "Resource UUID",
|
|
3581
|
-
"name": "id",
|
|
3582
|
-
"required": true,
|
|
3583
|
-
"hasDynamicHelp": false,
|
|
3584
|
-
"multiple": false,
|
|
3585
|
-
"type": "option"
|
|
3586
|
-
},
|
|
3587
|
-
"key": {
|
|
3588
|
-
"description": "Secret key. Must match `^[A-Z_][A-Z0-9_]*$`.",
|
|
3589
|
-
"name": "key",
|
|
3590
|
-
"required": true,
|
|
3591
|
-
"hasDynamicHelp": false,
|
|
3592
|
-
"multiple": false,
|
|
3593
|
-
"type": "option"
|
|
3594
|
-
},
|
|
3595
|
-
"raw-body": {
|
|
3596
|
-
"description": "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available.",
|
|
3597
|
-
"name": "raw-body",
|
|
3598
|
-
"hasDynamicHelp": false,
|
|
3599
|
-
"multiple": false,
|
|
3600
|
-
"type": "option"
|
|
3601
|
-
},
|
|
3602
|
-
"body-file": {
|
|
3603
|
-
"description": "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload.",
|
|
3604
|
-
"name": "body-file",
|
|
3605
|
-
"hasDynamicHelp": false,
|
|
3606
|
-
"multiple": false,
|
|
3607
|
-
"type": "option"
|
|
3608
|
-
},
|
|
3609
|
-
"value": {
|
|
3610
|
-
"description": "value",
|
|
3611
|
-
"name": "value",
|
|
3612
|
-
"hasDynamicHelp": false,
|
|
3613
|
-
"multiple": false,
|
|
3614
|
-
"type": "option"
|
|
3615
3699
|
}
|
|
3616
3700
|
},
|
|
3617
3701
|
"hasDynamicHelp": false,
|
|
3618
3702
|
"hiddenAliases": [],
|
|
3619
|
-
"id": "functions:
|
|
3703
|
+
"id": "functions:list-functions",
|
|
3620
3704
|
"pluginAlias": "@primitivedotdev/cli",
|
|
3621
3705
|
"pluginName": "@primitivedotdev/cli",
|
|
3622
3706
|
"pluginType": "core",
|
|
3623
3707
|
"strict": true,
|
|
3624
|
-
"summary": "
|
|
3708
|
+
"summary": "List functions",
|
|
3625
3709
|
"enableJsonFlag": false
|
|
3626
3710
|
},
|
|
3627
|
-
"functions:
|
|
3711
|
+
"functions:set-function-secret": {
|
|
3628
3712
|
"aliases": [],
|
|
3629
3713
|
"args": {},
|
|
3630
|
-
"description": "
|
|
3714
|
+
"description": "Path-keyed companion to `POST /functions/{id}/secrets`.\nIdempotent: returns 201 the first time the key is set, 200 on\nsubsequent updates. Same validation rules and same write-only\nguarantees as the POST verb; the new value lands in the running\nhandler on the next deploy.\n\n\nTip: prefer `primitive functions:set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
|
|
3631
3715
|
"flags": {
|
|
3632
3716
|
"api-key": {
|
|
3633
3717
|
"description": "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
@@ -3669,6 +3753,14 @@
|
|
|
3669
3753
|
"multiple": false,
|
|
3670
3754
|
"type": "option"
|
|
3671
3755
|
},
|
|
3756
|
+
"key": {
|
|
3757
|
+
"description": "Secret key. Must match `^[A-Z_][A-Z0-9_]*$`.",
|
|
3758
|
+
"name": "key",
|
|
3759
|
+
"required": true,
|
|
3760
|
+
"hasDynamicHelp": false,
|
|
3761
|
+
"multiple": false,
|
|
3762
|
+
"type": "option"
|
|
3763
|
+
},
|
|
3672
3764
|
"raw-body": {
|
|
3673
3765
|
"description": "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available.",
|
|
3674
3766
|
"name": "raw-body",
|
|
@@ -3683,9 +3775,9 @@
|
|
|
3683
3775
|
"multiple": false,
|
|
3684
3776
|
"type": "option"
|
|
3685
3777
|
},
|
|
3686
|
-
"
|
|
3687
|
-
"description": "
|
|
3688
|
-
"name": "
|
|
3778
|
+
"value": {
|
|
3779
|
+
"description": "value",
|
|
3780
|
+
"name": "value",
|
|
3689
3781
|
"hasDynamicHelp": false,
|
|
3690
3782
|
"multiple": false,
|
|
3691
3783
|
"type": "option"
|
|
@@ -3693,12 +3785,12 @@
|
|
|
3693
3785
|
},
|
|
3694
3786
|
"hasDynamicHelp": false,
|
|
3695
3787
|
"hiddenAliases": [],
|
|
3696
|
-
"id": "functions:
|
|
3788
|
+
"id": "functions:set-function-secret",
|
|
3697
3789
|
"pluginAlias": "@primitivedotdev/cli",
|
|
3698
3790
|
"pluginName": "@primitivedotdev/cli",
|
|
3699
3791
|
"pluginType": "core",
|
|
3700
3792
|
"strict": true,
|
|
3701
|
-
"summary": "
|
|
3793
|
+
"summary": "Set a secret by key",
|
|
3702
3794
|
"enableJsonFlag": false
|
|
3703
3795
|
},
|
|
3704
3796
|
"functions:update-function": {
|
|
@@ -4366,5 +4458,5 @@
|
|
|
4366
4458
|
"enableJsonFlag": false
|
|
4367
4459
|
}
|
|
4368
4460
|
},
|
|
4369
|
-
"version": "0.26.
|
|
4461
|
+
"version": "0.26.2"
|
|
4370
4462
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitivedotdev/cli",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.2",
|
|
4
4
|
"description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"description": "Claim, verify, and manage email domains"
|
|
36
36
|
},
|
|
37
37
|
"emails": {
|
|
38
|
-
"description": "List, inspect, and
|
|
38
|
+
"description": "List, inspect, and wait for received emails. `primitive emails:latest` lists the most recent inbound, `primitive emails:wait` blocks until matching inbound arrives (filter with --to/--from/--subject/--q; bounded by --timeout and --number; ideal for agents and CI), and `primitive emails:watch` streams new matches indefinitely for long-running terminals."
|
|
39
39
|
},
|
|
40
40
|
"sending": {
|
|
41
41
|
"description": "Send outbound emails. For replies to inbound mail, use `sending:reply-to-email --id <inbound-id>` (threading and Re: subject derived server-side); for fresh sends, use `sending:send-email` or the `primitive send` shortcut."
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"@oclif/core": "^4.10.5",
|
|
93
93
|
"@oclif/plugin-autocomplete": "^3.2.45",
|
|
94
94
|
"@oclif/plugin-help": "^6.2.44",
|
|
95
|
-
"@primitivedotdev/sdk": "^0.
|
|
95
|
+
"@primitivedotdev/sdk": "^0.26.0"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"@biomejs/biome": "^2.4.10",
|