@primitivedotdev/cli 0.25.0 → 0.25.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.
|
@@ -1,35 +1,147 @@
|
|
|
1
1
|
import { Command, Flags } from "@oclif/core";
|
|
2
|
-
import { createFunction, PrimitiveApiClient } from "@primitivedotdev/sdk/api";
|
|
2
|
+
import { createFunction, PrimitiveApiClient, setFunctionSecret, updateFunction, } from "@primitivedotdev/sdk/api";
|
|
3
3
|
import { extractErrorPayload, readTextFileFlag, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
|
|
4
4
|
import { resolveCliAuth } from "../auth.js";
|
|
5
5
|
import { emitRawSendMailFetchWarning } from "../lint/raw-send-mail-fetch.js";
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
6
|
+
import { parseSecretFlags, SECRET_FLAG_SECURITY_NOTE, } from "../secret-flags.js";
|
|
7
|
+
// Pure-ish orchestration of create + (optional secrets + redeploy).
|
|
8
|
+
// Mirrors runSetSecret in functions-set-secret.ts so the failure
|
|
9
|
+
// stages map directly onto stderr hints in run() below. Pulled out
|
|
10
|
+
// as a named export so the unit test can drive every branch with a
|
|
11
|
+
// fake DeployApiSurface, without spinning up a real client or the
|
|
12
|
+
// oclif command lifecycle.
|
|
13
|
+
export async function runDeployWithSecrets(api, params) {
|
|
14
|
+
const createResult = await api.createFunction({
|
|
15
|
+
code: params.code,
|
|
16
|
+
name: params.name,
|
|
17
|
+
...(params.sourceMap !== undefined ? { sourceMap: params.sourceMap } : {}),
|
|
18
|
+
});
|
|
19
|
+
if (createResult.error) {
|
|
20
|
+
return {
|
|
21
|
+
kind: "error",
|
|
22
|
+
payload: extractErrorPayload(createResult.error),
|
|
23
|
+
stage: "create",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const created = createResult.data?.data;
|
|
27
|
+
if (!created) {
|
|
28
|
+
return {
|
|
29
|
+
kind: "error",
|
|
30
|
+
payload: {
|
|
31
|
+
code: "client_error",
|
|
32
|
+
message: "Create function returned no data",
|
|
33
|
+
},
|
|
34
|
+
stage: "create",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Fast path: no secrets means no extra round-trips. The naked
|
|
38
|
+
// create result is exactly what the pre-secrets-flag command
|
|
39
|
+
// returned, so this branch is byte-identical to the previous
|
|
40
|
+
// behavior.
|
|
41
|
+
if (params.secrets.length === 0) {
|
|
42
|
+
return { kind: "ok", result: { created } };
|
|
43
|
+
}
|
|
44
|
+
const writtenSecrets = [];
|
|
45
|
+
const succeededKeys = [];
|
|
46
|
+
for (let i = 0; i < params.secrets.length; i++) {
|
|
47
|
+
const pair = params.secrets[i];
|
|
48
|
+
// Pre-compute the keys that come AFTER the current pair so a
|
|
49
|
+
// set-secret failure can surface every key that was never
|
|
50
|
+
// attempted, not just the one that failed. Without this, a user
|
|
51
|
+
// following the recovery hint verbatim would re-run set-secret
|
|
52
|
+
// only for the failed key and silently leave the trailing keys
|
|
53
|
+
// un-written.
|
|
54
|
+
const pendingKeys = params.secrets.slice(i + 1).map((p) => p.key);
|
|
55
|
+
const setResult = await api.setSecret({
|
|
56
|
+
id: created.id,
|
|
57
|
+
key: pair.key,
|
|
58
|
+
value: pair.value,
|
|
59
|
+
});
|
|
60
|
+
if (setResult.error) {
|
|
61
|
+
return {
|
|
62
|
+
created,
|
|
63
|
+
failedKey: pair.key,
|
|
64
|
+
kind: "error",
|
|
65
|
+
payload: extractErrorPayload(setResult.error),
|
|
66
|
+
pendingKeys,
|
|
67
|
+
stage: "set-secret",
|
|
68
|
+
succeededKeys,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const secret = setResult.data?.data;
|
|
72
|
+
if (!secret) {
|
|
73
|
+
return {
|
|
74
|
+
created,
|
|
75
|
+
failedKey: pair.key,
|
|
76
|
+
kind: "error",
|
|
77
|
+
payload: {
|
|
78
|
+
code: "client_error",
|
|
79
|
+
message: "Secret write returned no data",
|
|
80
|
+
},
|
|
81
|
+
pendingKeys,
|
|
82
|
+
stage: "set-secret",
|
|
83
|
+
succeededKeys,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
writtenSecrets.push(secret);
|
|
87
|
+
succeededKeys.push(pair.key);
|
|
88
|
+
}
|
|
89
|
+
const updateResult = await api.updateFunction({
|
|
90
|
+
code: params.code,
|
|
91
|
+
id: created.id,
|
|
92
|
+
...(params.sourceMap !== undefined ? { sourceMap: params.sourceMap } : {}),
|
|
93
|
+
});
|
|
94
|
+
if (updateResult.error) {
|
|
95
|
+
return {
|
|
96
|
+
created,
|
|
97
|
+
kind: "error",
|
|
98
|
+
payload: extractErrorPayload(updateResult.error),
|
|
99
|
+
stage: "redeploy",
|
|
100
|
+
succeededKeys,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const redeployed = updateResult.data?.data;
|
|
104
|
+
if (!redeployed) {
|
|
105
|
+
return {
|
|
106
|
+
created,
|
|
107
|
+
kind: "error",
|
|
108
|
+
payload: {
|
|
109
|
+
code: "client_error",
|
|
110
|
+
message: "Redeploy returned no data",
|
|
111
|
+
},
|
|
112
|
+
stage: "redeploy",
|
|
113
|
+
succeededKeys,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
kind: "ok",
|
|
118
|
+
result: { created, redeploy: redeployed, secrets: writtenSecrets },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
22
121
|
class FunctionsDeployCommand extends Command {
|
|
23
122
|
static description = `Deploy a new function from a bundled handler file. Agent-grade shortcut for functions:create-function.
|
|
24
123
|
|
|
25
124
|
Reads the bundle off disk (--file) instead of forcing the caller to
|
|
26
125
|
serialize the source into a JSON body. Use the underlying operation
|
|
27
126
|
\`functions:create-function\` if you need the full flag surface
|
|
28
|
-
(raw-body JSON, etc.)
|
|
127
|
+
(raw-body JSON, etc.).
|
|
128
|
+
|
|
129
|
+
Pass --secret KEY=VALUE (repeatable) to seed secret bindings in the
|
|
130
|
+
same command. Keys must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase
|
|
131
|
+
letters, digits, underscores; first character is a letter or
|
|
132
|
+
underscore). With one or more --secret flags the deploy fans out to
|
|
133
|
+
multiple API calls: create-function, set-secret per pair, then a
|
|
134
|
+
final update-function with the same bundle so the running handler
|
|
135
|
+
picks up the bindings. If a secret write fails after the create
|
|
136
|
+
step the function exists with whatever secrets succeeded and the
|
|
137
|
+
redeploy has NOT fired; re-run \`primitive functions:set-secret\`
|
|
138
|
+
for the missing keys, then \`primitive functions:redeploy\` to
|
|
139
|
+
push them live.`;
|
|
29
140
|
static summary = "Deploy a new function from a bundled handler file";
|
|
30
141
|
static examples = [
|
|
31
142
|
"<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js",
|
|
32
143
|
"<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
|
|
144
|
+
"<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
|
|
33
145
|
];
|
|
34
146
|
static flags = {
|
|
35
147
|
"api-key": Flags.string({
|
|
@@ -57,6 +169,10 @@ class FunctionsDeployCommand extends Command {
|
|
|
57
169
|
"source-map-file": Flags.string({
|
|
58
170
|
description: "Optional path to a source map for the bundle. Stored only on the runtime side and used to symbolicate stack traces.",
|
|
59
171
|
}),
|
|
172
|
+
secret: Flags.string({
|
|
173
|
+
description: `Secret KEY=VALUE to seed on the deployed function. Repeatable. KEY must match \`^[A-Z_][A-Z0-9_]*$\`; VALUE may contain \`=\` (only the first \`=\` is treated as a delimiter). Each KEY may only appear once per command. Passing one or more --secret flags fans out the deploy to create-function, set-secret per pair, then a final redeploy so the running handler picks up the bindings. ${SECRET_FLAG_SECURITY_NOTE}`,
|
|
174
|
+
multiple: true,
|
|
175
|
+
}),
|
|
60
176
|
time: Flags.boolean({
|
|
61
177
|
description: TIME_FLAG_DESCRIPTION,
|
|
62
178
|
}),
|
|
@@ -64,6 +180,17 @@ class FunctionsDeployCommand extends Command {
|
|
|
64
180
|
async run() {
|
|
65
181
|
const { flags } = await this.parse(FunctionsDeployCommand);
|
|
66
182
|
await runWithTiming(flags.time, async () => {
|
|
183
|
+
// Validate --secret pairs BEFORE any disk read or API call so
|
|
184
|
+
// a malformed input fails fast with a clear error and zero
|
|
185
|
+
// side effects. The fast path (no --secret flags) skips this
|
|
186
|
+
// entirely.
|
|
187
|
+
const rawSecrets = flags.secret ?? [];
|
|
188
|
+
const parsedSecrets = parseSecretFlags(rawSecrets);
|
|
189
|
+
if (parsedSecrets.kind === "error") {
|
|
190
|
+
process.stderr.write(`${parsedSecrets.message}\n`);
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
67
194
|
// Reads are inside the timed block so --time captures disk I/O
|
|
68
195
|
// alongside the API call. A pathological filesystem (NFS, slow
|
|
69
196
|
// FUSE mount) showing up here is exactly the kind of latency
|
|
@@ -96,27 +223,79 @@ class FunctionsDeployCommand extends Command {
|
|
|
96
223
|
baseUrlOverridden,
|
|
97
224
|
configDir: this.config.configDir,
|
|
98
225
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
226
|
+
// Adapter: thin wrappers around the generated SDK calls,
|
|
227
|
+
// routed through host 1 (apiClient.client). The function
|
|
228
|
+
// CRUD and secrets endpoints are not on host 2.
|
|
229
|
+
const apiSurface = {
|
|
230
|
+
createFunction: (p) => createFunction({
|
|
231
|
+
body: {
|
|
232
|
+
code: p.code,
|
|
233
|
+
name: p.name,
|
|
234
|
+
...(p.sourceMap !== undefined ? { sourceMap: p.sourceMap } : {}),
|
|
235
|
+
},
|
|
236
|
+
client: apiClient.client,
|
|
237
|
+
responseStyle: "fields",
|
|
238
|
+
}),
|
|
239
|
+
setSecret: (p) => setFunctionSecret({
|
|
240
|
+
body: { value: p.value },
|
|
241
|
+
client: apiClient.client,
|
|
242
|
+
path: { id: p.id, key: p.key },
|
|
243
|
+
responseStyle: "fields",
|
|
244
|
+
}),
|
|
245
|
+
updateFunction: (p) => updateFunction({
|
|
246
|
+
body: {
|
|
247
|
+
code: p.code,
|
|
248
|
+
...(p.sourceMap !== undefined ? { sourceMap: p.sourceMap } : {}),
|
|
249
|
+
},
|
|
250
|
+
client: apiClient.client,
|
|
251
|
+
path: { id: p.id },
|
|
252
|
+
responseStyle: "fields",
|
|
253
|
+
}),
|
|
254
|
+
};
|
|
255
|
+
const outcome = await runDeployWithSecrets(apiSurface, {
|
|
256
|
+
code,
|
|
257
|
+
name: flags.name,
|
|
258
|
+
secrets: parsedSecrets.secrets,
|
|
259
|
+
...(sourceMap !== undefined ? { sourceMap } : {}),
|
|
107
260
|
});
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
261
|
+
if (outcome.kind === "error") {
|
|
262
|
+
// Stage-specific framing on stderr so callers can tell
|
|
263
|
+
// whether the function was created before a downstream
|
|
264
|
+
// failure left it without secrets or without the
|
|
265
|
+
// redeploy. The JSON envelope still goes through
|
|
266
|
+
// writeErrorWithHints so any actionable hint (e.g.
|
|
267
|
+
// unauthorized) is surfaced.
|
|
268
|
+
if (outcome.stage === "set-secret") {
|
|
269
|
+
const succeeded = outcome.succeededKeys.length > 0
|
|
270
|
+
? outcome.succeededKeys.join(", ")
|
|
271
|
+
: "(none)";
|
|
272
|
+
const pending = outcome.pendingKeys.length > 0
|
|
273
|
+
? outcome.pendingKeys.join(", ")
|
|
274
|
+
: "(none)";
|
|
275
|
+
const allMissing = [outcome.failedKey, ...outcome.pendingKeys].join(", ");
|
|
276
|
+
process.stderr.write(`Function ${outcome.created.name} (${outcome.created.id}) was created, but writing secret ${outcome.failedKey} failed; succeeded keys so far: ${succeeded}; keys not yet attempted: ${pending}. The redeploy is NOT yet live. Re-run \`primitive functions:set-secret\` for each of [${allMissing}], then \`primitive functions:redeploy --id ${outcome.created.id} --file <bundle>\` to push them live.\n`);
|
|
277
|
+
}
|
|
278
|
+
else if (outcome.stage === "redeploy") {
|
|
279
|
+
const succeeded = outcome.succeededKeys.length > 0
|
|
280
|
+
? outcome.succeededKeys.join(", ")
|
|
281
|
+
: "(none)";
|
|
282
|
+
process.stderr.write(`Function ${outcome.created.name} (${outcome.created.id}) was created and secrets [${succeeded}] were written, but the final redeploy failed; the new bindings are NOT yet live. Re-run \`primitive functions:redeploy --id ${outcome.created.id} --file <bundle>\` once the cause is fixed.\n`);
|
|
283
|
+
}
|
|
284
|
+
writeErrorWithHints(outcome.payload);
|
|
111
285
|
removeStaleSavedCredentialOnUnauthorized({
|
|
112
286
|
...authFailureContext,
|
|
113
|
-
payload:
|
|
287
|
+
payload: outcome.payload,
|
|
114
288
|
});
|
|
115
289
|
process.exitCode = 1;
|
|
116
290
|
return;
|
|
117
291
|
}
|
|
118
|
-
|
|
119
|
-
|
|
292
|
+
// On the happy path, prefer the redeployed FunctionDetail
|
|
293
|
+
// (when secrets fired) over the bare CreateFunctionResult,
|
|
294
|
+
// since the redeploy is the state the user actually
|
|
295
|
+
// deployed. When no secrets were passed, fall back to the
|
|
296
|
+
// create payload for byte-identical pre-flag behavior.
|
|
297
|
+
const payload = outcome.result.redeploy ?? outcome.result.created;
|
|
298
|
+
this.log(JSON.stringify(payload, null, 2));
|
|
120
299
|
});
|
|
121
300
|
}
|
|
122
301
|
}
|
|
@@ -18,7 +18,18 @@ 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.25.0";
|
|
22
|
+
// The CLI version range that ships in the scaffolded devDependencies.
|
|
23
|
+
// Pinned separately from SDK_VERSION_RANGE because @primitivedotdev/cli
|
|
24
|
+
// and @primitivedotdev/sdk are independent packages on independent
|
|
25
|
+
// release cadences. Coupling them silently breaks `npm install` in
|
|
26
|
+
// every scaffolded project the day we bump one without publishing the
|
|
27
|
+
// other. Must include this CLI's own version: a `primitive
|
|
28
|
+
// functions:init` run from CLI v1.2.3 should scaffold a project that
|
|
29
|
+
// resolves at least v1.2.3, so the user does not silently downgrade
|
|
30
|
+
// the bin under themselves. The lockstep test in functions-init.test.ts
|
|
31
|
+
// enforces that invariant.
|
|
32
|
+
const CLI_VERSION_RANGE = "^0.25.0";
|
|
22
33
|
// esbuild version range. Pinned to the latest stable major used
|
|
23
34
|
// elsewhere in the Primitive codebase for bundling Workers-style
|
|
24
35
|
// handlers. Caret range so patch fixes flow in automatically.
|
|
@@ -40,10 +51,16 @@ export function renderHandler() {
|
|
|
40
51
|
return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
|
|
41
52
|
import { createPrimitiveClient } from "@primitivedotdev/sdk/api";
|
|
42
53
|
|
|
54
|
+
// TODO: replace with your verified sender address. Must be a domain
|
|
55
|
+
// you own or your managed *.primitive.email subdomain. The
|
|
56
|
+
// self-reply guard below compares incoming mail against this value
|
|
57
|
+
// to avoid the handler replying to its own outbound traffic.
|
|
58
|
+
const REPLY_FROM = "you@your-domain.primitive.email";
|
|
59
|
+
|
|
43
60
|
interface EmailReceivedEvent {
|
|
44
61
|
event: string;
|
|
45
62
|
email: {
|
|
46
|
-
headers: { from?: string; subject?: string };
|
|
63
|
+
headers: { from?: string; to?: string; subject?: string };
|
|
47
64
|
};
|
|
48
65
|
}
|
|
49
66
|
|
|
@@ -63,6 +80,20 @@ export default {
|
|
|
63
80
|
return Response.json({ ok: true, skipped: event.event });
|
|
64
81
|
}
|
|
65
82
|
|
|
83
|
+
// Self-reply guard: do not act on mail that this function itself
|
|
84
|
+
// sent. Without this, an outbound reply that gets forwarded back
|
|
85
|
+
// into the inbox would trigger another reply, and so on.
|
|
86
|
+
//
|
|
87
|
+
// event.email.headers.from is the raw RFC 2822 header value, so
|
|
88
|
+
// it may be either a bare address ("alice@example.com") or a
|
|
89
|
+
// display-name form ("Alice <alice@example.com>"). A substring
|
|
90
|
+
// check matches both. Tighten this predicate (e.g. parse the
|
|
91
|
+
// bracketed address) if you legitimately want to act on mail
|
|
92
|
+
// from your own domain.
|
|
93
|
+
if (event.email.headers.from?.includes(REPLY_FROM)) {
|
|
94
|
+
return Response.json({ ok: true, skipped: "self-reply" });
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
const client = createPrimitiveClient({ apiKey: env.PRIMITIVE_API_KEY });
|
|
67
98
|
|
|
68
99
|
// Recipient gate
|
|
@@ -73,9 +104,14 @@ export default {
|
|
|
73
104
|
// external addresses return 403 recipient_not_allowed with a
|
|
74
105
|
// structured gates[] array until the recipient has authenticated
|
|
75
106
|
// to you or support has enabled the gate.
|
|
107
|
+
|
|
108
|
+
// Recipient routing: a single function can handle multiple inbound
|
|
109
|
+
// addresses by branching on event.email.headers.to. For example,
|
|
110
|
+
// route "support@" to a ticketing flow and "sales@" to a lead
|
|
111
|
+
// capture flow before calling client.send.
|
|
76
112
|
const reply = await client.send({
|
|
77
|
-
from:
|
|
78
|
-
to: event.email.headers.from ??
|
|
113
|
+
from: REPLY_FROM,
|
|
114
|
+
to: event.email.headers.from ?? REPLY_FROM,
|
|
79
115
|
subject: \`Re: \${event.email.headers.subject ?? ""}\`,
|
|
80
116
|
bodyText: "Got your message.",
|
|
81
117
|
});
|
|
@@ -111,6 +147,15 @@ export function renderPackageJson(name) {
|
|
|
111
147
|
"@primitivedotdev/sdk": SDK_VERSION_RANGE,
|
|
112
148
|
},
|
|
113
149
|
devDependencies: {
|
|
150
|
+
// @primitivedotdev/cli ships the primitive bin. Including it as
|
|
151
|
+
// a devDep here means `node_modules/.bin/primitive` resolves to
|
|
152
|
+
// the real CLI inside the scaffolded project; otherwise the
|
|
153
|
+
// bin falls through to @primitivedotdev/sdk's deprecated CLI
|
|
154
|
+
// alias and every `npm run deploy` invocation prints the
|
|
155
|
+
// "CLI moved" stderr banner. Pinned via CLI_VERSION_RANGE, a
|
|
156
|
+
// dedicated constant so the version is decoupled from the SDK
|
|
157
|
+
// range and bumps are explicit on both ends.
|
|
158
|
+
"@primitivedotdev/cli": CLI_VERSION_RANGE,
|
|
114
159
|
esbuild: ESBUILD_VERSION_RANGE,
|
|
115
160
|
typescript: "^5.7.2",
|
|
116
161
|
},
|
|
@@ -1,26 +1,108 @@
|
|
|
1
1
|
import { Command, Flags } from "@oclif/core";
|
|
2
|
-
import { PrimitiveApiClient, updateFunction } from "@primitivedotdev/sdk/api";
|
|
2
|
+
import { PrimitiveApiClient, setFunctionSecret, updateFunction, } from "@primitivedotdev/sdk/api";
|
|
3
3
|
import { extractErrorPayload, readTextFileFlag, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
|
|
4
4
|
import { resolveCliAuth } from "../auth.js";
|
|
5
5
|
import { emitRawSendMailFetchWarning } from "../lint/raw-send-mail-fetch.js";
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
6
|
+
import { parseSecretFlags, SECRET_FLAG_SECURITY_NOTE, } from "../secret-flags.js";
|
|
7
|
+
// Pure-ish orchestration of (optional secrets +) update-function.
|
|
8
|
+
// Writes every secret first, then re-deploys with the new bundle so
|
|
9
|
+
// a single updateFunction call refreshes every binding the user
|
|
10
|
+
// wrote. Pulled out as a named export so the unit test can drive
|
|
11
|
+
// every branch with a fake RedeployApiSurface, without spinning up
|
|
12
|
+
// a real client or the oclif command lifecycle.
|
|
13
|
+
export async function runRedeployWithSecrets(api, params) {
|
|
14
|
+
const writtenSecrets = [];
|
|
15
|
+
const succeededKeys = [];
|
|
16
|
+
for (let i = 0; i < params.secrets.length; i++) {
|
|
17
|
+
const pair = params.secrets[i];
|
|
18
|
+
// Pre-compute the keys that come AFTER the current pair so a
|
|
19
|
+
// set-secret failure can surface every key that was never
|
|
20
|
+
// attempted, not just the one that failed.
|
|
21
|
+
const pendingKeys = params.secrets.slice(i + 1).map((p) => p.key);
|
|
22
|
+
const setResult = await api.setSecret({
|
|
23
|
+
id: params.id,
|
|
24
|
+
key: pair.key,
|
|
25
|
+
value: pair.value,
|
|
26
|
+
});
|
|
27
|
+
if (setResult.error) {
|
|
28
|
+
return {
|
|
29
|
+
failedKey: pair.key,
|
|
30
|
+
kind: "error",
|
|
31
|
+
payload: extractErrorPayload(setResult.error),
|
|
32
|
+
pendingKeys,
|
|
33
|
+
stage: "set-secret",
|
|
34
|
+
succeededKeys,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const secret = setResult.data?.data;
|
|
38
|
+
if (!secret) {
|
|
39
|
+
return {
|
|
40
|
+
failedKey: pair.key,
|
|
41
|
+
kind: "error",
|
|
42
|
+
payload: {
|
|
43
|
+
code: "client_error",
|
|
44
|
+
message: "Secret write returned no data",
|
|
45
|
+
},
|
|
46
|
+
pendingKeys,
|
|
47
|
+
stage: "set-secret",
|
|
48
|
+
succeededKeys,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
writtenSecrets.push(secret);
|
|
52
|
+
succeededKeys.push(pair.key);
|
|
53
|
+
}
|
|
54
|
+
const updateResult = await api.updateFunction({
|
|
55
|
+
code: params.code,
|
|
56
|
+
id: params.id,
|
|
57
|
+
...(params.sourceMap !== undefined ? { sourceMap: params.sourceMap } : {}),
|
|
58
|
+
});
|
|
59
|
+
if (updateResult.error) {
|
|
60
|
+
return {
|
|
61
|
+
kind: "error",
|
|
62
|
+
payload: extractErrorPayload(updateResult.error),
|
|
63
|
+
stage: "redeploy",
|
|
64
|
+
succeededKeys,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const redeployed = updateResult.data?.data;
|
|
68
|
+
if (!redeployed) {
|
|
69
|
+
return {
|
|
70
|
+
kind: "error",
|
|
71
|
+
payload: {
|
|
72
|
+
code: "client_error",
|
|
73
|
+
message: "Redeploy returned no data",
|
|
74
|
+
},
|
|
75
|
+
stage: "redeploy",
|
|
76
|
+
succeededKeys,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
kind: "ok",
|
|
81
|
+
result: {
|
|
82
|
+
redeploy: redeployed,
|
|
83
|
+
...(writtenSecrets.length > 0 ? { secrets: writtenSecrets } : {}),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
13
87
|
class FunctionsRedeployCommand extends Command {
|
|
14
88
|
static description = `Update or redeploy a function from a bundled handler file. Agent-grade shortcut for functions:update-function.
|
|
15
89
|
|
|
16
90
|
Use to push a new bundle OR to refresh secret bindings into the
|
|
17
91
|
running handler. The same file is fine for both: the deploy reads
|
|
18
92
|
the bindings table fresh on every call, so passing the existing
|
|
19
|
-
bundle picks up any secret writes since the last deploy
|
|
93
|
+
bundle picks up any secret writes since the last deploy.
|
|
94
|
+
|
|
95
|
+
Pass --secret KEY=VALUE (repeatable) to write secrets BEFORE the
|
|
96
|
+
redeploy fires; one update-function call then refreshes every new
|
|
97
|
+
binding. Keys must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters,
|
|
98
|
+
digits, underscores; first character is a letter or underscore).
|
|
99
|
+
With one or more --secret flags the redeploy fans out to multiple
|
|
100
|
+
API calls (set-secret per pair, then update-function).`;
|
|
20
101
|
static summary = "Redeploy a function from a bundled handler file";
|
|
21
102
|
static examples = [
|
|
22
103
|
"<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js",
|
|
23
104
|
"<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js --source-map-file ./bundle.js.map",
|
|
105
|
+
"<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
|
|
24
106
|
];
|
|
25
107
|
static flags = {
|
|
26
108
|
"api-key": Flags.string({
|
|
@@ -48,6 +130,10 @@ class FunctionsRedeployCommand extends Command {
|
|
|
48
130
|
"source-map-file": Flags.string({
|
|
49
131
|
description: "Optional path to a source map for the bundle. Used to symbolicate stack traces in the function's logs.",
|
|
50
132
|
}),
|
|
133
|
+
secret: Flags.string({
|
|
134
|
+
description: `Secret KEY=VALUE to write on the function before the redeploy fires. Repeatable. KEY must match \`^[A-Z_][A-Z0-9_]*$\`; VALUE may contain \`=\` (only the first \`=\` is treated as a delimiter). Each KEY may only appear once per command. Passing one or more --secret flags fans out to set-secret per pair then a single update-function call so the new bindings land in the same redeploy. ${SECRET_FLAG_SECURITY_NOTE}`,
|
|
135
|
+
multiple: true,
|
|
136
|
+
}),
|
|
51
137
|
time: Flags.boolean({
|
|
52
138
|
description: TIME_FLAG_DESCRIPTION,
|
|
53
139
|
}),
|
|
@@ -55,6 +141,17 @@ class FunctionsRedeployCommand extends Command {
|
|
|
55
141
|
async run() {
|
|
56
142
|
const { flags } = await this.parse(FunctionsRedeployCommand);
|
|
57
143
|
await runWithTiming(flags.time, async () => {
|
|
144
|
+
// Validate --secret pairs BEFORE any disk read or API call so
|
|
145
|
+
// a malformed input fails fast with a clear error and zero
|
|
146
|
+
// side effects. The fast path (no --secret flags) skips the
|
|
147
|
+
// secret-write loop entirely.
|
|
148
|
+
const rawSecrets = flags.secret ?? [];
|
|
149
|
+
const parsedSecrets = parseSecretFlags(rawSecrets);
|
|
150
|
+
if (parsedSecrets.kind === "error") {
|
|
151
|
+
process.stderr.write(`${parsedSecrets.message}\n`);
|
|
152
|
+
process.exitCode = 1;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
58
155
|
// Reads inside the timed block: --time captures disk I/O too,
|
|
59
156
|
// which is the latency the flag is meant to surface.
|
|
60
157
|
const code = readTextFileFlag(flags.file, "--file");
|
|
@@ -85,27 +182,58 @@ class FunctionsRedeployCommand extends Command {
|
|
|
85
182
|
baseUrlOverridden,
|
|
86
183
|
configDir: this.config.configDir,
|
|
87
184
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
185
|
+
// Adapter: thin wrappers around the generated SDK calls,
|
|
186
|
+
// routed through host 1 (apiClient.client). The function
|
|
187
|
+
// CRUD and secrets endpoints are not on host 2.
|
|
188
|
+
const apiSurface = {
|
|
189
|
+
setSecret: (p) => setFunctionSecret({
|
|
190
|
+
body: { value: p.value },
|
|
191
|
+
client: apiClient.client,
|
|
192
|
+
path: { id: p.id, key: p.key },
|
|
193
|
+
responseStyle: "fields",
|
|
194
|
+
}),
|
|
195
|
+
updateFunction: (p) => updateFunction({
|
|
196
|
+
body: {
|
|
197
|
+
code: p.code,
|
|
198
|
+
...(p.sourceMap !== undefined ? { sourceMap: p.sourceMap } : {}),
|
|
199
|
+
},
|
|
200
|
+
client: apiClient.client,
|
|
201
|
+
path: { id: p.id },
|
|
202
|
+
responseStyle: "fields",
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
205
|
+
const outcome = await runRedeployWithSecrets(apiSurface, {
|
|
206
|
+
code,
|
|
207
|
+
id: flags.id,
|
|
208
|
+
secrets: parsedSecrets.secrets,
|
|
209
|
+
...(sourceMap !== undefined ? { sourceMap } : {}),
|
|
96
210
|
});
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
211
|
+
if (outcome.kind === "error") {
|
|
212
|
+
if (outcome.stage === "set-secret") {
|
|
213
|
+
const succeeded = outcome.succeededKeys.length > 0
|
|
214
|
+
? outcome.succeededKeys.join(", ")
|
|
215
|
+
: "(none)";
|
|
216
|
+
const pending = outcome.pendingKeys.length > 0
|
|
217
|
+
? outcome.pendingKeys.join(", ")
|
|
218
|
+
: "(none)";
|
|
219
|
+
const allMissing = [outcome.failedKey, ...outcome.pendingKeys].join(", ");
|
|
220
|
+
process.stderr.write(`Writing secret ${outcome.failedKey} failed before the redeploy; succeeded keys so far: ${succeeded}; keys not yet attempted: ${pending}. The new bundle has NOT been deployed. Re-run \`primitive functions:set-secret\` for each of [${allMissing}], then \`primitive functions:redeploy --id ${flags.id} --file <bundle>\` to push them live.\n`);
|
|
221
|
+
}
|
|
222
|
+
else if (outcome.stage === "redeploy") {
|
|
223
|
+
const succeeded = outcome.succeededKeys.length > 0
|
|
224
|
+
? outcome.succeededKeys.join(", ")
|
|
225
|
+
: "(none)";
|
|
226
|
+
process.stderr.write(`Secrets [${succeeded}] were written, but the redeploy step failed; the new bindings are NOT yet live. Re-run \`primitive functions:redeploy --id ${flags.id} --file <bundle>\` once the cause is fixed.\n`);
|
|
227
|
+
}
|
|
228
|
+
writeErrorWithHints(outcome.payload);
|
|
100
229
|
removeStaleSavedCredentialOnUnauthorized({
|
|
101
230
|
...authFailureContext,
|
|
102
|
-
payload:
|
|
231
|
+
payload: outcome.payload,
|
|
103
232
|
});
|
|
104
233
|
process.exitCode = 1;
|
|
105
234
|
return;
|
|
106
235
|
}
|
|
107
|
-
|
|
108
|
-
this.log(JSON.stringify(envelope?.data ?? null, null, 2));
|
|
236
|
+
this.log(JSON.stringify(outcome.result.redeploy, null, 2));
|
|
109
237
|
});
|
|
110
238
|
}
|
|
111
239
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Shared parsing for the `--secret KEY=VALUE` flag used by both
|
|
2
|
+
// functions:deploy and functions:redeploy. Lives in its own module so
|
|
3
|
+
// neither command implicitly depends on the other's file path.
|
|
4
|
+
// Server-side constraint on secret keys. Mirrored client-side so
|
|
5
|
+
// malformed input is rejected before any side-effecting API call.
|
|
6
|
+
export const SECRET_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
7
|
+
// Split each `--secret KEY=VALUE` on the FIRST `=`. KEY must match
|
|
8
|
+
// `^[A-Z_][A-Z0-9_]*$`; VALUE may contain `=` (only the first one
|
|
9
|
+
// is treated as a delimiter). Duplicate KEYs are rejected: silently
|
|
10
|
+
// accepting two pairs with the same key would fan out to two
|
|
11
|
+
// setFunctionSecret writes where only the second wins, which is
|
|
12
|
+
// almost always a typo and never the intent.
|
|
13
|
+
export function parseSecretFlags(raw) {
|
|
14
|
+
const secrets = [];
|
|
15
|
+
const seenKeys = new Set();
|
|
16
|
+
for (const entry of raw) {
|
|
17
|
+
const eq = entry.indexOf("=");
|
|
18
|
+
if (eq === -1) {
|
|
19
|
+
return {
|
|
20
|
+
kind: "error",
|
|
21
|
+
message: `--secret expects KEY=VALUE (got ${JSON.stringify(entry)}). Example: --secret API_TOKEN=abc123`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const key = entry.slice(0, eq);
|
|
25
|
+
const value = entry.slice(eq + 1);
|
|
26
|
+
if (key.length === 0) {
|
|
27
|
+
return {
|
|
28
|
+
kind: "error",
|
|
29
|
+
message: `--secret is missing a KEY before '=' (got ${JSON.stringify(entry)}). Example: --secret API_TOKEN=abc123`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (!SECRET_KEY_RE.test(key)) {
|
|
33
|
+
return {
|
|
34
|
+
kind: "error",
|
|
35
|
+
message: `--secret KEY ${JSON.stringify(key)} does not match ${SECRET_KEY_RE.source} (uppercase letters, digits, underscores; first character is a letter or underscore).`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (seenKeys.has(key)) {
|
|
39
|
+
return {
|
|
40
|
+
kind: "error",
|
|
41
|
+
message: `--secret KEY ${JSON.stringify(key)} was passed more than once. Each key may only appear once per command.`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
seenKeys.add(key);
|
|
45
|
+
secrets.push({ key, value });
|
|
46
|
+
}
|
|
47
|
+
return { kind: "ok", secrets };
|
|
48
|
+
}
|
|
49
|
+
// Shared flag-description copy so both functions:deploy and
|
|
50
|
+
// functions:redeploy advertise the same security caveat and KEY
|
|
51
|
+
// constraints. The shell-history note is the load-bearing piece:
|
|
52
|
+
// CLI flag values land in ~/.bash_history, `ps aux`, and
|
|
53
|
+
// /proc/[pid]/cmdline, so callers handling sensitive values
|
|
54
|
+
// should set them via a shell variable (ideally read via `read -s`
|
|
55
|
+
// or piped from a secrets manager) and reference the variable on
|
|
56
|
+
// the command line. The variable still appears in `ps`-visible
|
|
57
|
+
// argv, but at least the literal value does not get archived in
|
|
58
|
+
// the user's shell history.
|
|
59
|
+
export const SECRET_FLAG_SECURITY_NOTE = 'Note: values passed on the command line are visible in shell history (e.g. ~/.bash_history) and to other users via `ps aux` / /proc/[pid]/cmdline. For sensitive values prefer `--secret KEY="$VAR"` where `$VAR` is set out-of-band (read -s, a secrets manager, etc.).';
|
package/oclif.manifest.json
CHANGED
|
@@ -791,10 +791,11 @@
|
|
|
791
791
|
"functions:deploy": {
|
|
792
792
|
"aliases": [],
|
|
793
793
|
"args": {},
|
|
794
|
-
"description": "Deploy a new function from a bundled handler file. Agent-grade shortcut for functions:create-function.\n\n Reads the bundle off disk (--file) instead of forcing the caller to\n serialize the source into a JSON body. Use the underlying operation\n `functions:create-function` if you need the full flag surface\n (raw-body JSON, etc.).",
|
|
794
|
+
"description": "Deploy a new function from a bundled handler file. Agent-grade shortcut for functions:create-function.\n\n Reads the bundle off disk (--file) instead of forcing the caller to\n serialize the source into a JSON body. Use the underlying operation\n `functions:create-function` if you need the full flag surface\n (raw-body JSON, etc.).\n\n Pass --secret KEY=VALUE (repeatable) to seed secret bindings in the\n same command. Keys must match `^[A-Z_][A-Z0-9_]*$` (uppercase\n letters, digits, underscores; first character is a letter or\n underscore). With one or more --secret flags the deploy fans out to\n multiple API calls: create-function, set-secret per pair, then a\n final update-function with the same bundle so the running handler\n picks up the bindings. If a secret write fails after the create\n step the function exists with whatever secrets succeeded and the\n redeploy has NOT fired; re-run `primitive functions:set-secret`\n for the missing keys, then `primitive functions:redeploy` to\n push them live.",
|
|
795
795
|
"examples": [
|
|
796
796
|
"<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js",
|
|
797
|
-
"<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map"
|
|
797
|
+
"<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
|
|
798
|
+
"<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com"
|
|
798
799
|
],
|
|
799
800
|
"flags": {
|
|
800
801
|
"api-key": {
|
|
@@ -846,6 +847,13 @@
|
|
|
846
847
|
"multiple": false,
|
|
847
848
|
"type": "option"
|
|
848
849
|
},
|
|
850
|
+
"secret": {
|
|
851
|
+
"description": "Secret KEY=VALUE to seed on the deployed function. Repeatable. KEY must match `^[A-Z_][A-Z0-9_]*$`; VALUE may contain `=` (only the first `=` is treated as a delimiter). Each KEY may only appear once per command. Passing one or more --secret flags fans out the deploy to create-function, set-secret per pair, then a final redeploy so the running handler picks up the bindings. Note: values passed on the command line are visible in shell history (e.g. ~/.bash_history) and to other users via `ps aux` / /proc/[pid]/cmdline. For sensitive values prefer `--secret KEY=\"$VAR\"` where `$VAR` is set out-of-band (read -s, a secrets manager, etc.).",
|
|
852
|
+
"name": "secret",
|
|
853
|
+
"hasDynamicHelp": false,
|
|
854
|
+
"multiple": true,
|
|
855
|
+
"type": "option"
|
|
856
|
+
},
|
|
849
857
|
"time": {
|
|
850
858
|
"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.",
|
|
851
859
|
"name": "time",
|
|
@@ -866,10 +874,11 @@
|
|
|
866
874
|
"functions:redeploy": {
|
|
867
875
|
"aliases": [],
|
|
868
876
|
"args": {},
|
|
869
|
-
"description": "Update or redeploy a function from a bundled handler file. Agent-grade shortcut for functions:update-function.\n\n Use to push a new bundle OR to refresh secret bindings into the\n running handler. The same file is fine for both: the deploy reads\n the bindings table fresh on every call, so passing the existing\n bundle picks up any secret writes since the last deploy.",
|
|
877
|
+
"description": "Update or redeploy a function from a bundled handler file. Agent-grade shortcut for functions:update-function.\n\n Use to push a new bundle OR to refresh secret bindings into the\n running handler. The same file is fine for both: the deploy reads\n the bindings table fresh on every call, so passing the existing\n bundle picks up any secret writes since the last deploy.\n\n Pass --secret KEY=VALUE (repeatable) to write secrets BEFORE the\n redeploy fires; one update-function call then refreshes every new\n binding. Keys must match `^[A-Z_][A-Z0-9_]*$` (uppercase letters,\n digits, underscores; first character is a letter or underscore).\n With one or more --secret flags the redeploy fans out to multiple\n API calls (set-secret per pair, then update-function).",
|
|
870
878
|
"examples": [
|
|
871
879
|
"<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js",
|
|
872
|
-
"<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js --source-map-file ./bundle.js.map"
|
|
880
|
+
"<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js --source-map-file ./bundle.js.map",
|
|
881
|
+
"<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com"
|
|
873
882
|
],
|
|
874
883
|
"flags": {
|
|
875
884
|
"api-key": {
|
|
@@ -921,6 +930,13 @@
|
|
|
921
930
|
"multiple": false,
|
|
922
931
|
"type": "option"
|
|
923
932
|
},
|
|
933
|
+
"secret": {
|
|
934
|
+
"description": "Secret KEY=VALUE to write on the function before the redeploy fires. Repeatable. KEY must match `^[A-Z_][A-Z0-9_]*$`; VALUE may contain `=` (only the first `=` is treated as a delimiter). Each KEY may only appear once per command. Passing one or more --secret flags fans out to set-secret per pair then a single update-function call so the new bindings land in the same redeploy. Note: values passed on the command line are visible in shell history (e.g. ~/.bash_history) and to other users via `ps aux` / /proc/[pid]/cmdline. For sensitive values prefer `--secret KEY=\"$VAR\"` where `$VAR` is set out-of-band (read -s, a secrets manager, etc.).",
|
|
935
|
+
"name": "secret",
|
|
936
|
+
"hasDynamicHelp": false,
|
|
937
|
+
"multiple": true,
|
|
938
|
+
"type": "option"
|
|
939
|
+
},
|
|
924
940
|
"time": {
|
|
925
941
|
"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.",
|
|
926
942
|
"name": "time",
|
|
@@ -3611,7 +3627,7 @@
|
|
|
3611
3627
|
"functions:test-function": {
|
|
3612
3628
|
"aliases": [],
|
|
3613
3629
|
"args": {},
|
|
3614
|
-
"description": "Sends a real test email from a Primitive-controlled sender to a\
|
|
3630
|
+
"description": "Sends a real test email from a Primitive-controlled sender to a\nlocal-part on one of the org's verified inbound domains. By\ndefault the recipient is a synthetic\n`__primitive_function_test+<random>@<domain>` address that\nevery handler's catch-all routing receives identically; pass\n`local_part` to override and exercise routing logic that\nbranches on a specific recipient (the common pattern when one\nfunction handles multiple inboxes like `summarize@` and\n`action@`). The function fires through the normal MX delivery\npath, so reply / send-mail calls from inside the handler\nagainst the inbound's `email.id` work the same as in\nproduction. Returns immediately after the send is queued; the\ninvocation appears on the function's invocations list within a\nfew seconds.\n\nRequires that the function is currently `deployed`. Returns 422\nif the function is in `pending` or `failed` state, or if the\norg has no verified inbound domain to receive the test mail.\nReturns 400 if `local_part` is set to a value that does not\nmatch the local-part character set.\n",
|
|
3615
3631
|
"flags": {
|
|
3616
3632
|
"api-key": {
|
|
3617
3633
|
"description": "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
@@ -3652,6 +3668,27 @@
|
|
|
3652
3668
|
"hasDynamicHelp": false,
|
|
3653
3669
|
"multiple": false,
|
|
3654
3670
|
"type": "option"
|
|
3671
|
+
},
|
|
3672
|
+
"raw-body": {
|
|
3673
|
+
"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
|
+
"name": "raw-body",
|
|
3675
|
+
"hasDynamicHelp": false,
|
|
3676
|
+
"multiple": false,
|
|
3677
|
+
"type": "option"
|
|
3678
|
+
},
|
|
3679
|
+
"body-file": {
|
|
3680
|
+
"description": "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload.",
|
|
3681
|
+
"name": "body-file",
|
|
3682
|
+
"hasDynamicHelp": false,
|
|
3683
|
+
"multiple": false,
|
|
3684
|
+
"type": "option"
|
|
3685
|
+
},
|
|
3686
|
+
"local-part": {
|
|
3687
|
+
"description": "Override the synthetic local-part. When set, the test email is sent to `<local_part>@<picked-domain>` instead of the default `__primitive_function_test+<random>@<picked-domain>`. Must start with an alphanumeric and contain only letters, digits, dots, plus signs, hyphens, or underscores; 1-64 characters total.",
|
|
3688
|
+
"name": "local-part",
|
|
3689
|
+
"hasDynamicHelp": false,
|
|
3690
|
+
"multiple": false,
|
|
3691
|
+
"type": "option"
|
|
3655
3692
|
}
|
|
3656
3693
|
},
|
|
3657
3694
|
"hasDynamicHelp": false,
|
|
@@ -4329,5 +4366,5 @@
|
|
|
4329
4366
|
"enableJsonFlag": false
|
|
4330
4367
|
}
|
|
4331
4368
|
},
|
|
4332
|
-
"version": "0.25.
|
|
4369
|
+
"version": "0.25.2"
|
|
4333
4370
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitivedotdev/cli",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.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,
|
|
@@ -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.25.0"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"@biomejs/biome": "^2.4.10",
|