@primitivedotdev/sdk 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -571,6 +571,25 @@ function collectValues(parameters, flags) {
|
|
|
571
571
|
}
|
|
572
572
|
return values;
|
|
573
573
|
}
|
|
574
|
+
// Discoverability hints for generated commands that have a
|
|
575
|
+
// hand-rolled ergonomic shortcut. Keyed by the manifest's
|
|
576
|
+
// `sdkName` (camelCase, matches the generated SDK function). The
|
|
577
|
+
// hint is appended to the operation's --help description so an
|
|
578
|
+
// agent reading `<command> --help` finds the shortcut without
|
|
579
|
+
// having to enumerate the full command list. AGX walkthrough:
|
|
580
|
+
// an agent reached for `functions:update-function` to redeploy
|
|
581
|
+
// (which forces a JSON-stringified `code` body) when
|
|
582
|
+
// `functions:redeploy --file <bundle>` was the intended path.
|
|
583
|
+
//
|
|
584
|
+
// Add an entry here whenever a hand-rolled shortcut shadows a
|
|
585
|
+
// generated operation; the COMMANDS map in `index.ts` is the
|
|
586
|
+
// authoritative list of shortcuts.
|
|
587
|
+
export const OPERATION_HINTS = {
|
|
588
|
+
createFunction: "Tip: prefer `primitive functions:deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
|
|
589
|
+
updateFunction: "Tip: prefer `primitive functions:redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
|
|
590
|
+
createFunctionSecret: "Tip: 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.",
|
|
591
|
+
setFunctionSecret: "Tip: 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.",
|
|
592
|
+
};
|
|
574
593
|
export function createOperationCommand(operation) {
|
|
575
594
|
const { flags, bodyFieldFlagToProperty } = buildFlags(operation);
|
|
576
595
|
// Append a "Body fields" summary to the description so agents
|
|
@@ -582,9 +601,13 @@ export function createOperationCommand(operation) {
|
|
|
582
601
|
const schemaSummary = operation.hasJsonBody
|
|
583
602
|
? renderRequestSchemaSummary(operation.requestSchema)
|
|
584
603
|
: null;
|
|
585
|
-
const
|
|
604
|
+
const hint = OPERATION_HINTS[operation.sdkName];
|
|
605
|
+
const descriptionWithSchema = schemaSummary
|
|
586
606
|
? `${baseDescription}\n\n${schemaSummary}`
|
|
587
607
|
: baseDescription;
|
|
608
|
+
const fullDescription = hint
|
|
609
|
+
? `${descriptionWithSchema}\n\n${hint}`
|
|
610
|
+
: descriptionWithSchema;
|
|
588
611
|
class OperationCommand extends Command {
|
|
589
612
|
static description = fullDescription;
|
|
590
613
|
static flags = flags;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { Args, Command, Errors, Flags } from "@oclif/core";
|
|
4
|
+
// `primitive functions:init <name>` stamps a deployable Function project
|
|
5
|
+
// into ./<name>/ so a new author can go from zero to a deployed handler
|
|
6
|
+
// in two commands: `npm install && npm run build` then
|
|
7
|
+
// `primitive functions:deploy --name <name> --file ./dist/handler.js`.
|
|
8
|
+
//
|
|
9
|
+
// The scaffolded handler imports `createPrimitiveClient` from
|
|
10
|
+
// `@primitivedotdev/sdk/api`, NOT from the package root. The root export
|
|
11
|
+
// pulls in webhook helpers that depend on `node:crypto`, which breaks
|
|
12
|
+
// Workers-style bundles. The `/api` subpath is the runtime-client
|
|
13
|
+
// surface and is the documented import for in-handler use.
|
|
14
|
+
// The SDK version range that ships in the scaffolded package.json's
|
|
15
|
+
// dependencies. Pinned to the current shipped minor with a caret so
|
|
16
|
+
// patch releases of the SDK pick up automatically. Update alongside
|
|
17
|
+
// any major version bump of the SDK.
|
|
18
|
+
const SDK_VERSION_RANGE = "^0.22.0";
|
|
19
|
+
// esbuild version range. Pinned to the latest stable major used
|
|
20
|
+
// elsewhere in the Primitive codebase for bundling Workers-style
|
|
21
|
+
// handlers. Caret range so patch fixes flow in automatically.
|
|
22
|
+
const ESBUILD_VERSION_RANGE = "^0.27.0";
|
|
23
|
+
// Validate a directory name passed as the positional argument.
|
|
24
|
+
// Matches a conservative slug shape: lowercase letters, digits,
|
|
25
|
+
// hyphens, underscores. Rejecting weirder names up front prevents
|
|
26
|
+
// surprises when the same string lands in package.json's `name`
|
|
27
|
+
// field (which has its own validation rules) or in shell scripts.
|
|
28
|
+
const VALID_NAME = /^[a-z0-9][a-z0-9_-]{0,62}$/;
|
|
29
|
+
export function isValidFunctionName(name) {
|
|
30
|
+
return VALID_NAME.test(name);
|
|
31
|
+
}
|
|
32
|
+
// File contents for the scaffolded project. Each renderer takes the
|
|
33
|
+
// function name and returns the raw file body. Kept as named exports
|
|
34
|
+
// so the unit test can assert content without having to spin up the
|
|
35
|
+
// oclif command lifecycle.
|
|
36
|
+
export function renderHandler() {
|
|
37
|
+
return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
|
|
38
|
+
import { createPrimitiveClient } from "@primitivedotdev/sdk/api";
|
|
39
|
+
|
|
40
|
+
export default {
|
|
41
|
+
async fetch(
|
|
42
|
+
req: Request,
|
|
43
|
+
env: { PRIMITIVE_API_KEY: string },
|
|
44
|
+
): Promise<Response> {
|
|
45
|
+
const event = (await req.json()) as {
|
|
46
|
+
email: { headers: { from?: string; subject?: string } };
|
|
47
|
+
};
|
|
48
|
+
const client = createPrimitiveClient({ apiKey: env.PRIMITIVE_API_KEY });
|
|
49
|
+
|
|
50
|
+
const reply = await client.send({
|
|
51
|
+
from: "you@your-domain.primitive.email",
|
|
52
|
+
to: event.email.headers.from ?? "you@your-domain.primitive.email",
|
|
53
|
+
subject: \`Re: \${event.email.headers.subject ?? ""}\`,
|
|
54
|
+
bodyText: "Got your message.",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return Response.json({ ok: true, reply });
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
export function renderPackageJson(name) {
|
|
63
|
+
const pkg = {
|
|
64
|
+
name,
|
|
65
|
+
version: "0.1.0",
|
|
66
|
+
private: true,
|
|
67
|
+
type: "module",
|
|
68
|
+
scripts: {
|
|
69
|
+
build: "node build.mjs",
|
|
70
|
+
deploy: `npm run build && primitive functions:deploy --name ${name} --file ./dist/handler.js`,
|
|
71
|
+
redeploy: "npm run build && primitive functions:redeploy --id $PRIMITIVE_FUNCTION_ID --file ./dist/handler.js",
|
|
72
|
+
},
|
|
73
|
+
dependencies: {
|
|
74
|
+
"@primitivedotdev/sdk": SDK_VERSION_RANGE,
|
|
75
|
+
},
|
|
76
|
+
devDependencies: {
|
|
77
|
+
esbuild: ESBUILD_VERSION_RANGE,
|
|
78
|
+
typescript: "^5.7.2",
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
return `${JSON.stringify(pkg, null, 2)}\n`;
|
|
82
|
+
}
|
|
83
|
+
export function renderBuildMjs() {
|
|
84
|
+
return `import { build } from "esbuild";
|
|
85
|
+
|
|
86
|
+
// Bundle handler.ts into a single ESM file suitable for the Primitive
|
|
87
|
+
// Functions runtime. The runtime is a Workers-style environment, so
|
|
88
|
+
// we pick the "worker" / "browser" export conditions on @primitivedotdev/sdk
|
|
89
|
+
// (which routes us to the /api subpath safely without dragging in
|
|
90
|
+
// node:crypto-dependent webhook helpers).
|
|
91
|
+
|
|
92
|
+
await build({
|
|
93
|
+
entryPoints: ["handler.ts"],
|
|
94
|
+
bundle: true,
|
|
95
|
+
format: "esm",
|
|
96
|
+
platform: "browser",
|
|
97
|
+
target: "es2022",
|
|
98
|
+
conditions: ["worker", "browser"],
|
|
99
|
+
outfile: "dist/handler.js",
|
|
100
|
+
});
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
export function renderTsconfig() {
|
|
104
|
+
const tsconfig = {
|
|
105
|
+
compilerOptions: {
|
|
106
|
+
target: "ES2022",
|
|
107
|
+
module: "ESNext",
|
|
108
|
+
moduleResolution: "Bundler",
|
|
109
|
+
strict: true,
|
|
110
|
+
lib: ["ES2022", "WebWorker"],
|
|
111
|
+
types: [],
|
|
112
|
+
esModuleInterop: true,
|
|
113
|
+
skipLibCheck: true,
|
|
114
|
+
},
|
|
115
|
+
include: ["handler.ts"],
|
|
116
|
+
};
|
|
117
|
+
return `${JSON.stringify(tsconfig, null, 2)}\n`;
|
|
118
|
+
}
|
|
119
|
+
export function renderGitignore() {
|
|
120
|
+
return "node_modules\ndist\n";
|
|
121
|
+
}
|
|
122
|
+
export function renderReadme(name) {
|
|
123
|
+
return `# ${name}
|
|
124
|
+
|
|
125
|
+
## What this is
|
|
126
|
+
|
|
127
|
+
A Primitive Function: a JavaScript handler that runs on inbound mail.
|
|
128
|
+
It receives the \`email.received\` event, demonstrates a basic reply
|
|
129
|
+
via the Primitive SDK, and returns a JSON envelope.
|
|
130
|
+
|
|
131
|
+
## Develop
|
|
132
|
+
|
|
133
|
+
\`\`\`
|
|
134
|
+
npm install
|
|
135
|
+
npm run build
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
## Deploy
|
|
139
|
+
|
|
140
|
+
\`\`\`
|
|
141
|
+
npm run deploy
|
|
142
|
+
\`\`\`
|
|
143
|
+
|
|
144
|
+
The deploy step calls \`primitive functions:deploy\` and requires
|
|
145
|
+
\`PRIMITIVE_API_KEY\` to be set in your shell (or pass \`--api-key\`).
|
|
146
|
+
Run \`primitive login\` once to save a key in your CLI config if you
|
|
147
|
+
prefer that to an env var.
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
// Files written by the scaffolder, in the order they're created.
|
|
151
|
+
// Exported as a pure function so the unit test can verify the
|
|
152
|
+
// exact content of every file without invoking the command and
|
|
153
|
+
// touching disk.
|
|
154
|
+
export function scaffoldFiles(name) {
|
|
155
|
+
return [
|
|
156
|
+
{ contents: renderHandler(), relativePath: "handler.ts" },
|
|
157
|
+
{ contents: renderPackageJson(name), relativePath: "package.json" },
|
|
158
|
+
{ contents: renderBuildMjs(), relativePath: "build.mjs" },
|
|
159
|
+
{ contents: renderTsconfig(), relativePath: "tsconfig.json" },
|
|
160
|
+
{ contents: renderGitignore(), relativePath: ".gitignore" },
|
|
161
|
+
{ contents: renderReadme(name), relativePath: "README.md" },
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
// Write the scaffold to disk. Refuses to overwrite an existing
|
|
165
|
+
// directory: if `outDir` exists the function throws and leaves the
|
|
166
|
+
// filesystem untouched. On any write error after creating the
|
|
167
|
+
// directory, the partially-written tree is cleaned up so re-runs
|
|
168
|
+
// see a clean slate. Exported for unit testing.
|
|
169
|
+
export function writeScaffold(params) {
|
|
170
|
+
if (!isValidFunctionName(params.name)) {
|
|
171
|
+
throw new Errors.CLIError(`Invalid function name "${params.name}". Use lowercase letters, digits, hyphens, or underscores (1-63 chars, must start with a letter or digit).`, { exit: 1 });
|
|
172
|
+
}
|
|
173
|
+
const files = scaffoldFiles(params.name);
|
|
174
|
+
const written = [];
|
|
175
|
+
// Create the target directory with recursive: false so the check
|
|
176
|
+
// and the create happen in one syscall. mkdirSync throws EEXIST
|
|
177
|
+
// atomically if the path already exists, which closes the TOCTOU
|
|
178
|
+
// window between a separate existsSync check and the mkdir call.
|
|
179
|
+
try {
|
|
180
|
+
mkdirSync(params.outDir, { recursive: false });
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
const code = error.code;
|
|
184
|
+
if (code === "EEXIST") {
|
|
185
|
+
throw new Errors.CLIError(`Target directory already exists: ${params.outDir}. Refusing to overwrite. Remove it or pick a different --out-dir.`, { exit: 1 });
|
|
186
|
+
}
|
|
187
|
+
if (code === "ENOENT") {
|
|
188
|
+
throw new Errors.CLIError(`Parent directory does not exist for ${params.outDir}. Create it first or pick a different --out-dir.`, { exit: 1 });
|
|
189
|
+
}
|
|
190
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
191
|
+
throw new Errors.CLIError(`Failed to create ${params.outDir}: ${detail}`, {
|
|
192
|
+
exit: 1,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
for (const file of files) {
|
|
197
|
+
const fullPath = resolve(params.outDir, file.relativePath);
|
|
198
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
199
|
+
writeFileSync(fullPath, file.contents, "utf8");
|
|
200
|
+
written.push(fullPath);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
// Roll back the partial scaffold so the user can retry without
|
|
205
|
+
// tripping the "directory already exists" guard above.
|
|
206
|
+
try {
|
|
207
|
+
rmSync(params.outDir, { force: true, recursive: true });
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Best-effort cleanup; surface the original error regardless.
|
|
211
|
+
}
|
|
212
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
213
|
+
throw new Errors.CLIError(`Failed to write scaffold to ${params.outDir}: ${detail}`, { exit: 1 });
|
|
214
|
+
}
|
|
215
|
+
return { written };
|
|
216
|
+
}
|
|
217
|
+
class FunctionsInitCommand extends Command {
|
|
218
|
+
static description = `Scaffold a new Primitive Function project in ./<name>/ with handler.ts, package.json, build.mjs, tsconfig.json, .gitignore, and README.md.
|
|
219
|
+
|
|
220
|
+
The scaffolded handler imports \`createPrimitiveClient\` from
|
|
221
|
+
\`@primitivedotdev/sdk/api\` and demonstrates the canonical pattern:
|
|
222
|
+
parse the email.received event, send a reply via the SDK, return a
|
|
223
|
+
JSON envelope. The build script uses esbuild's JS API and emits
|
|
224
|
+
./dist/handler.js, ready to hand to \`primitive functions:deploy --file\`.
|
|
225
|
+
|
|
226
|
+
Refuses to overwrite an existing directory. Use --out-dir to pick a
|
|
227
|
+
different target path than ./<name>/.`;
|
|
228
|
+
static summary = "Scaffold a new Primitive Function project ready for functions:deploy";
|
|
229
|
+
static examples = [
|
|
230
|
+
"<%= config.bin %> functions:init my-fn",
|
|
231
|
+
"<%= config.bin %> functions:init my-fn --out-dir ./functions/my-fn",
|
|
232
|
+
];
|
|
233
|
+
static args = {
|
|
234
|
+
name: Args.string({
|
|
235
|
+
description: "Function name. Lowercase letters, digits, hyphens, underscores. 1-63 chars. Used as the directory name (when --out-dir is not set) and as the package.json name.",
|
|
236
|
+
required: true,
|
|
237
|
+
}),
|
|
238
|
+
};
|
|
239
|
+
static flags = {
|
|
240
|
+
"out-dir": Flags.string({
|
|
241
|
+
description: "Directory to scaffold into. Defaults to ./<name>/. Must not already exist.",
|
|
242
|
+
}),
|
|
243
|
+
};
|
|
244
|
+
async run() {
|
|
245
|
+
const { args, flags } = await this.parse(FunctionsInitCommand);
|
|
246
|
+
const outDir = resolve(flags["out-dir"] ?? `./${args.name}`);
|
|
247
|
+
writeScaffold({ name: args.name, outDir });
|
|
248
|
+
this.log(`Scaffolded ${outDir}.`);
|
|
249
|
+
this.log("Next:");
|
|
250
|
+
this.log(` cd ${outDir}`);
|
|
251
|
+
this.log(" npm install");
|
|
252
|
+
this.log(" npm run build");
|
|
253
|
+
this.log(` primitive functions:deploy --name ${args.name} --file ./dist/handler.js`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
export default FunctionsInitCommand;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { Command, Flags } from "@oclif/core";
|
|
2
|
+
import { getFunction, setFunctionSecret, updateFunction, } from "../../api/generated/sdk.gen.js";
|
|
3
|
+
import { PrimitiveApiClient } from "../../api/index.js";
|
|
4
|
+
import { extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
|
|
5
|
+
import { resolveCliAuth } from "../auth.js";
|
|
6
|
+
// Pure-ish orchestration of the set-secret + optional redeploy
|
|
7
|
+
// flow. Pulled out as a named export so the unit test can drive
|
|
8
|
+
// both the happy path and each error stage with a fake API
|
|
9
|
+
// surface, without spinning up a real client or the oclif
|
|
10
|
+
// command lifecycle.
|
|
11
|
+
//
|
|
12
|
+
// The redeploy step uses the function's CURRENT code (fetched via
|
|
13
|
+
// getFunction) as the new bundle. This is the documented way to
|
|
14
|
+
// "refresh secret bindings without changing the handler": the
|
|
15
|
+
// server-side deploy reads the secrets table fresh on every call,
|
|
16
|
+
// so re-deploying the same code picks up the secret we just wrote.
|
|
17
|
+
export async function runSetSecret(api, params) {
|
|
18
|
+
const setResult = await api.setSecret({
|
|
19
|
+
id: params.id,
|
|
20
|
+
key: params.key,
|
|
21
|
+
value: params.value,
|
|
22
|
+
});
|
|
23
|
+
if (setResult.error) {
|
|
24
|
+
return {
|
|
25
|
+
kind: "error",
|
|
26
|
+
payload: extractErrorPayload(setResult.error),
|
|
27
|
+
stage: "set-secret",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const secret = setResult.data?.data;
|
|
31
|
+
if (!secret) {
|
|
32
|
+
// Server returned 2xx with no `data` body. Treat as an error
|
|
33
|
+
// so we don't fabricate a success payload; this should not
|
|
34
|
+
// happen in practice but the shape forces us to handle it.
|
|
35
|
+
return {
|
|
36
|
+
kind: "error",
|
|
37
|
+
payload: {
|
|
38
|
+
code: "client_error",
|
|
39
|
+
message: "Secret write returned no data",
|
|
40
|
+
},
|
|
41
|
+
stage: "set-secret",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (!params.redeploy) {
|
|
45
|
+
return { kind: "ok", result: { secret } };
|
|
46
|
+
}
|
|
47
|
+
const fnResult = await api.getFunction({ id: params.id });
|
|
48
|
+
if (fnResult.error) {
|
|
49
|
+
return {
|
|
50
|
+
kind: "error",
|
|
51
|
+
payload: extractErrorPayload(fnResult.error),
|
|
52
|
+
stage: "get-function",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const fn = fnResult.data?.data;
|
|
56
|
+
if (!fn) {
|
|
57
|
+
return {
|
|
58
|
+
kind: "error",
|
|
59
|
+
payload: {
|
|
60
|
+
code: "client_error",
|
|
61
|
+
message: "Could not read current function code for redeploy",
|
|
62
|
+
},
|
|
63
|
+
stage: "get-function",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const updateResult = await api.updateFunction({
|
|
67
|
+
code: fn.code,
|
|
68
|
+
id: params.id,
|
|
69
|
+
});
|
|
70
|
+
if (updateResult.error) {
|
|
71
|
+
return {
|
|
72
|
+
kind: "error",
|
|
73
|
+
payload: extractErrorPayload(updateResult.error),
|
|
74
|
+
stage: "redeploy",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const redeployed = updateResult.data?.data;
|
|
78
|
+
if (!redeployed) {
|
|
79
|
+
return {
|
|
80
|
+
kind: "error",
|
|
81
|
+
payload: {
|
|
82
|
+
code: "client_error",
|
|
83
|
+
message: "Redeploy returned no data",
|
|
84
|
+
},
|
|
85
|
+
stage: "redeploy",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { kind: "ok", result: { redeploy: redeployed, secret } };
|
|
89
|
+
}
|
|
90
|
+
class FunctionsSetSecretCommand extends Command {
|
|
91
|
+
static description = `Write a function secret and optionally redeploy so the new value lands in the running handler. Agent-grade shortcut for functions:set-function-secret + functions:redeploy.
|
|
92
|
+
|
|
93
|
+
Without --redeploy this is a plain secret upsert: the value is
|
|
94
|
+
encrypted at rest but is NOT visible to the running handler until
|
|
95
|
+
the next deploy. Pass --redeploy to re-run the deploy with the
|
|
96
|
+
function's current code in the same call, which refreshes the
|
|
97
|
+
binding set with the value you just wrote.
|
|
98
|
+
|
|
99
|
+
Keys must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters, digits,
|
|
100
|
+
underscores; first character is a letter or underscore). System-
|
|
101
|
+
managed keys are reserved and rejected.`;
|
|
102
|
+
static summary = "Write a function secret (optionally redeploying to push it live)";
|
|
103
|
+
static examples = [
|
|
104
|
+
"<%= config.bin %> functions:set-secret --id <fn-id> --key API_TOKEN --value abc123",
|
|
105
|
+
"<%= config.bin %> functions:set-secret --id <fn-id> --key API_TOKEN --value abc123 --redeploy",
|
|
106
|
+
];
|
|
107
|
+
static flags = {
|
|
108
|
+
"api-key": Flags.string({
|
|
109
|
+
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
110
|
+
env: "PRIMITIVE_API_KEY",
|
|
111
|
+
}),
|
|
112
|
+
"api-base-url-1": Flags.string({
|
|
113
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
114
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
115
|
+
hidden: true,
|
|
116
|
+
}),
|
|
117
|
+
"api-base-url-2": Flags.string({
|
|
118
|
+
description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
119
|
+
env: "PRIMITIVE_API_BASE_URL_2",
|
|
120
|
+
hidden: true,
|
|
121
|
+
}),
|
|
122
|
+
id: Flags.string({
|
|
123
|
+
description: "Function id (UUID). The function must already exist.",
|
|
124
|
+
required: true,
|
|
125
|
+
}),
|
|
126
|
+
key: Flags.string({
|
|
127
|
+
description: "Secret key. Uppercase letters, digits, underscores; must start with a letter or underscore. System-managed keys are reserved.",
|
|
128
|
+
required: true,
|
|
129
|
+
}),
|
|
130
|
+
value: Flags.string({
|
|
131
|
+
description: "Secret value (up to 4096 UTF-8 bytes). Encrypted at rest.",
|
|
132
|
+
required: true,
|
|
133
|
+
}),
|
|
134
|
+
redeploy: Flags.boolean({
|
|
135
|
+
description: "Also redeploy the function with its current code so the new value lands in the running handler. Without this, the secret is written but not visible to the handler until the next deploy. Note: source maps are stored only on the runtime side and getFunction does not return them, so this redeploy drops any previously-uploaded source map. If preserving stack-trace symbolication matters, use `functions:redeploy --file <bundle.js> --source-map-file <bundle.js.map>` instead.",
|
|
136
|
+
}),
|
|
137
|
+
time: Flags.boolean({
|
|
138
|
+
description: TIME_FLAG_DESCRIPTION,
|
|
139
|
+
}),
|
|
140
|
+
};
|
|
141
|
+
async run() {
|
|
142
|
+
const { flags } = await this.parse(FunctionsSetSecretCommand);
|
|
143
|
+
await runWithTiming(flags.time, async () => {
|
|
144
|
+
const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
|
|
145
|
+
flags["api-base-url-2"] !== undefined;
|
|
146
|
+
const auth = resolveCliAuth({
|
|
147
|
+
apiKey: flags["api-key"],
|
|
148
|
+
apiBaseUrl1: flags["api-base-url-1"],
|
|
149
|
+
apiBaseUrl2: flags["api-base-url-2"],
|
|
150
|
+
configDir: this.config.configDir,
|
|
151
|
+
});
|
|
152
|
+
const apiClient = new PrimitiveApiClient({
|
|
153
|
+
apiKey: auth.apiKey,
|
|
154
|
+
apiBaseUrl1: auth.apiBaseUrl1,
|
|
155
|
+
apiBaseUrl2: auth.apiBaseUrl2,
|
|
156
|
+
});
|
|
157
|
+
const authFailureContext = {
|
|
158
|
+
auth,
|
|
159
|
+
baseUrlOverridden,
|
|
160
|
+
configDir: this.config.configDir,
|
|
161
|
+
};
|
|
162
|
+
// Adapter: thin wrappers around the generated SDK calls,
|
|
163
|
+
// routed through host 1 (apiClient.client). The secrets and
|
|
164
|
+
// function-detail endpoints are not on host 2.
|
|
165
|
+
const apiSurface = {
|
|
166
|
+
getFunction: (p) => getFunction({
|
|
167
|
+
client: apiClient.client,
|
|
168
|
+
path: { id: p.id },
|
|
169
|
+
responseStyle: "fields",
|
|
170
|
+
}),
|
|
171
|
+
setSecret: (p) => setFunctionSecret({
|
|
172
|
+
body: { value: p.value },
|
|
173
|
+
client: apiClient.client,
|
|
174
|
+
path: { id: p.id, key: p.key },
|
|
175
|
+
responseStyle: "fields",
|
|
176
|
+
}),
|
|
177
|
+
updateFunction: (p) => updateFunction({
|
|
178
|
+
body: { code: p.code },
|
|
179
|
+
client: apiClient.client,
|
|
180
|
+
path: { id: p.id },
|
|
181
|
+
responseStyle: "fields",
|
|
182
|
+
}),
|
|
183
|
+
};
|
|
184
|
+
const outcome = await runSetSecret(apiSurface, {
|
|
185
|
+
id: flags.id,
|
|
186
|
+
key: flags.key,
|
|
187
|
+
redeploy: flags.redeploy === true,
|
|
188
|
+
value: flags.value,
|
|
189
|
+
});
|
|
190
|
+
if (outcome.kind === "error") {
|
|
191
|
+
// Stage-specific framing on stderr so callers can tell
|
|
192
|
+
// whether the secret landed before a failed redeploy. The
|
|
193
|
+
// JSON envelope still goes through writeErrorWithHints so
|
|
194
|
+
// any actionable hint (e.g. unauthorized) is surfaced.
|
|
195
|
+
if (outcome.stage === "get-function") {
|
|
196
|
+
process.stderr.write("Secret was written, but reading current function code for redeploy failed; the secret is NOT yet live. Re-run with --redeploy, or call `primitive functions:redeploy --id <id> --file <bundle>` once you have the bundle.\n");
|
|
197
|
+
}
|
|
198
|
+
else if (outcome.stage === "redeploy") {
|
|
199
|
+
process.stderr.write("Secret was written, but the redeploy step failed; the secret is NOT yet live. Inspect the function's deploy_error and re-run `primitive functions:redeploy --id <id> --file <bundle>` once the cause is fixed.\n");
|
|
200
|
+
}
|
|
201
|
+
writeErrorWithHints(outcome.payload);
|
|
202
|
+
removeStaleSavedCredentialOnUnauthorized({
|
|
203
|
+
...authFailureContext,
|
|
204
|
+
payload: outcome.payload,
|
|
205
|
+
});
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.log(JSON.stringify(outcome.result, null, 2));
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
export default FunctionsSetSecretCommand;
|
package/dist/oclif/index.js
CHANGED
|
@@ -5,7 +5,9 @@ import EmailsLatestCommand from "./commands/emails-latest.js";
|
|
|
5
5
|
import EmailsWaitCommand from "./commands/emails-wait.js";
|
|
6
6
|
import EmailsWatchCommand from "./commands/emails-watch.js";
|
|
7
7
|
import FunctionsDeployCommand from "./commands/functions-deploy.js";
|
|
8
|
+
import FunctionsInitCommand from "./commands/functions-init.js";
|
|
8
9
|
import FunctionsRedeployCommand from "./commands/functions-redeploy.js";
|
|
10
|
+
import FunctionsSetSecretCommand from "./commands/functions-set-secret.js";
|
|
9
11
|
import LoginCommand from "./commands/login.js";
|
|
10
12
|
import LogoutCommand from "./commands/logout.js";
|
|
11
13
|
import SendCommand from "./commands/send.js";
|
|
@@ -139,6 +141,13 @@ export const COMMANDS = {
|
|
|
139
141
|
// inbound mail. `watch` defaults to a human table; `wait` defaults to JSONL.
|
|
140
142
|
"emails:watch": EmailsWatchCommand,
|
|
141
143
|
"emails:wait": EmailsWaitCommand,
|
|
144
|
+
// `functions:init` scaffolds a deployable Function project so a
|
|
145
|
+
// new author can go zero-to-deployed without writing the handler,
|
|
146
|
+
// package.json, build script, and tsconfig from scratch. The
|
|
147
|
+
// scaffolded handler imports from @primitivedotdev/sdk/api (the
|
|
148
|
+
// runtime-client subpath) and demonstrates client.send() so the
|
|
149
|
+
// first thing the author sees is the SDK pattern, not raw fetch.
|
|
150
|
+
"functions:init": FunctionsInitCommand,
|
|
142
151
|
// `functions:deploy` and `functions:redeploy` are file-input
|
|
143
152
|
// shortcuts for create-function / update-function. The underlying
|
|
144
153
|
// ops take `code` as a body string, which is awkward at the CLI
|
|
@@ -147,5 +156,12 @@ export const COMMANDS = {
|
|
|
147
156
|
// available for callers that want the full surface.
|
|
148
157
|
"functions:deploy": FunctionsDeployCommand,
|
|
149
158
|
"functions:redeploy": FunctionsRedeployCommand,
|
|
159
|
+
// `functions:set-secret` is the one-call shortcut for "write a
|
|
160
|
+
// secret AND (optionally) push it live." The raw
|
|
161
|
+
// functions:set-function-secret / functions:create-function-secret
|
|
162
|
+
// operations only do the secret upsert; making the new value
|
|
163
|
+
// visible to the running handler requires a separate redeploy,
|
|
164
|
+
// which this shortcut folds in via --redeploy.
|
|
165
|
+
"functions:set-secret": FunctionsSetSecretCommand,
|
|
150
166
|
...generatedCommands,
|
|
151
167
|
};
|
package/oclif.manifest.json
CHANGED
|
@@ -709,6 +709,39 @@
|
|
|
709
709
|
"summary": "Wait for matching inbound emails",
|
|
710
710
|
"enableJsonFlag": false
|
|
711
711
|
},
|
|
712
|
+
"functions:init": {
|
|
713
|
+
"aliases": [],
|
|
714
|
+
"args": {
|
|
715
|
+
"name": {
|
|
716
|
+
"description": "Function name. Lowercase letters, digits, hyphens, underscores. 1-63 chars. Used as the directory name (when --out-dir is not set) and as the package.json name.",
|
|
717
|
+
"name": "name",
|
|
718
|
+
"required": true
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
"description": "Scaffold a new Primitive Function project in ./<name>/ with handler.ts, package.json, build.mjs, tsconfig.json, .gitignore, and README.md.\n\n The scaffolded handler imports `createPrimitiveClient` from\n `@primitivedotdev/sdk/api` and demonstrates the canonical pattern:\n parse the email.received event, send a reply via the SDK, return a\n JSON envelope. The build script uses esbuild's JS API and emits\n ./dist/handler.js, ready to hand to `primitive functions:deploy --file`.\n\n Refuses to overwrite an existing directory. Use --out-dir to pick a\n different target path than ./<name>/.",
|
|
722
|
+
"examples": [
|
|
723
|
+
"<%= config.bin %> functions:init my-fn",
|
|
724
|
+
"<%= config.bin %> functions:init my-fn --out-dir ./functions/my-fn"
|
|
725
|
+
],
|
|
726
|
+
"flags": {
|
|
727
|
+
"out-dir": {
|
|
728
|
+
"description": "Directory to scaffold into. Defaults to ./<name>/. Must not already exist.",
|
|
729
|
+
"name": "out-dir",
|
|
730
|
+
"hasDynamicHelp": false,
|
|
731
|
+
"multiple": false,
|
|
732
|
+
"type": "option"
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
"hasDynamicHelp": false,
|
|
736
|
+
"hiddenAliases": [],
|
|
737
|
+
"id": "functions:init",
|
|
738
|
+
"pluginAlias": "@primitivedotdev/sdk",
|
|
739
|
+
"pluginName": "@primitivedotdev/sdk",
|
|
740
|
+
"pluginType": "core",
|
|
741
|
+
"strict": true,
|
|
742
|
+
"summary": "Scaffold a new Primitive Function project ready for functions:deploy",
|
|
743
|
+
"enableJsonFlag": false
|
|
744
|
+
},
|
|
712
745
|
"functions:deploy": {
|
|
713
746
|
"aliases": [],
|
|
714
747
|
"args": {},
|
|
@@ -859,6 +892,88 @@
|
|
|
859
892
|
"summary": "Redeploy a function from a bundled handler file",
|
|
860
893
|
"enableJsonFlag": false
|
|
861
894
|
},
|
|
895
|
+
"functions:set-secret": {
|
|
896
|
+
"aliases": [],
|
|
897
|
+
"args": {},
|
|
898
|
+
"description": "Write a function secret and optionally redeploy so the new value lands in the running handler. Agent-grade shortcut for functions:set-function-secret + functions:redeploy.\n\n Without --redeploy this is a plain secret upsert: the value is\n encrypted at rest but is NOT visible to the running handler until\n the next deploy. Pass --redeploy to re-run the deploy with the\n function's current code in the same call, which refreshes the\n binding set with the value you just wrote.\n\n Keys must match `^[A-Z_][A-Z0-9_]*$` (uppercase letters, digits,\n underscores; first character is a letter or underscore). System-\n managed keys are reserved and rejected.",
|
|
899
|
+
"examples": [
|
|
900
|
+
"<%= config.bin %> functions:set-secret --id <fn-id> --key API_TOKEN --value abc123",
|
|
901
|
+
"<%= config.bin %> functions:set-secret --id <fn-id> --key API_TOKEN --value abc123 --redeploy"
|
|
902
|
+
],
|
|
903
|
+
"flags": {
|
|
904
|
+
"api-key": {
|
|
905
|
+
"description": "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
906
|
+
"env": "PRIMITIVE_API_KEY",
|
|
907
|
+
"name": "api-key",
|
|
908
|
+
"hasDynamicHelp": false,
|
|
909
|
+
"multiple": false,
|
|
910
|
+
"type": "option"
|
|
911
|
+
},
|
|
912
|
+
"api-base-url-1": {
|
|
913
|
+
"description": "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
914
|
+
"env": "PRIMITIVE_API_BASE_URL_1",
|
|
915
|
+
"hidden": true,
|
|
916
|
+
"name": "api-base-url-1",
|
|
917
|
+
"hasDynamicHelp": false,
|
|
918
|
+
"multiple": false,
|
|
919
|
+
"type": "option"
|
|
920
|
+
},
|
|
921
|
+
"api-base-url-2": {
|
|
922
|
+
"description": "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
923
|
+
"env": "PRIMITIVE_API_BASE_URL_2",
|
|
924
|
+
"hidden": true,
|
|
925
|
+
"name": "api-base-url-2",
|
|
926
|
+
"hasDynamicHelp": false,
|
|
927
|
+
"multiple": false,
|
|
928
|
+
"type": "option"
|
|
929
|
+
},
|
|
930
|
+
"id": {
|
|
931
|
+
"description": "Function id (UUID). The function must already exist.",
|
|
932
|
+
"name": "id",
|
|
933
|
+
"required": true,
|
|
934
|
+
"hasDynamicHelp": false,
|
|
935
|
+
"multiple": false,
|
|
936
|
+
"type": "option"
|
|
937
|
+
},
|
|
938
|
+
"key": {
|
|
939
|
+
"description": "Secret key. Uppercase letters, digits, underscores; must start with a letter or underscore. System-managed keys are reserved.",
|
|
940
|
+
"name": "key",
|
|
941
|
+
"required": true,
|
|
942
|
+
"hasDynamicHelp": false,
|
|
943
|
+
"multiple": false,
|
|
944
|
+
"type": "option"
|
|
945
|
+
},
|
|
946
|
+
"value": {
|
|
947
|
+
"description": "Secret value (up to 4096 UTF-8 bytes). Encrypted at rest.",
|
|
948
|
+
"name": "value",
|
|
949
|
+
"required": true,
|
|
950
|
+
"hasDynamicHelp": false,
|
|
951
|
+
"multiple": false,
|
|
952
|
+
"type": "option"
|
|
953
|
+
},
|
|
954
|
+
"redeploy": {
|
|
955
|
+
"description": "Also redeploy the function with its current code so the new value lands in the running handler. Without this, the secret is written but not visible to the handler until the next deploy. Note: source maps are stored only on the runtime side and getFunction does not return them, so this redeploy drops any previously-uploaded source map. If preserving stack-trace symbolication matters, use `functions:redeploy --file <bundle.js> --source-map-file <bundle.js.map>` instead.",
|
|
956
|
+
"name": "redeploy",
|
|
957
|
+
"allowNo": false,
|
|
958
|
+
"type": "boolean"
|
|
959
|
+
},
|
|
960
|
+
"time": {
|
|
961
|
+
"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.",
|
|
962
|
+
"name": "time",
|
|
963
|
+
"allowNo": false,
|
|
964
|
+
"type": "boolean"
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
"hasDynamicHelp": false,
|
|
968
|
+
"hiddenAliases": [],
|
|
969
|
+
"id": "functions:set-secret",
|
|
970
|
+
"pluginAlias": "@primitivedotdev/sdk",
|
|
971
|
+
"pluginName": "@primitivedotdev/sdk",
|
|
972
|
+
"pluginType": "core",
|
|
973
|
+
"strict": true,
|
|
974
|
+
"summary": "Write a function secret (optionally redeploying to push it live)",
|
|
975
|
+
"enableJsonFlag": false
|
|
976
|
+
},
|
|
862
977
|
"account:get-account": {
|
|
863
978
|
"aliases": [],
|
|
864
979
|
"args": {},
|
|
@@ -2918,7 +3033,7 @@
|
|
|
2918
3033
|
"functions:create-function": {
|
|
2919
3034
|
"aliases": [],
|
|
2920
3035
|
"args": {},
|
|
2921
|
-
"description": "Creates and deploys a new function. The handler must be a single\nESM module that exports a default async function receiving the\n`email.received` event (see the Webhook payload section for the\nfull schema). Code is bundled before being uploaded; ship a\nsingle self-contained file rather than relying on external\nimports.\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",
|
|
3036
|
+
"description": "Creates and deploys a new function. The handler must be a single\nESM module that exports a default async function receiving the\n`email.received` event (see the Webhook payload section for the\nfull schema). Code is bundled before being uploaded; ship a\nsingle self-contained file rather than relying on external\nimports.\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.",
|
|
2922
3037
|
"flags": {
|
|
2923
3038
|
"api-key": {
|
|
2924
3039
|
"description": "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
@@ -3001,7 +3116,7 @@
|
|
|
3001
3116
|
"functions:create-function-secret": {
|
|
3002
3117
|
"aliases": [],
|
|
3003
3118
|
"args": {},
|
|
3004
|
-
"description": "Idempotent insert-or-update keyed on `(function_id, key)`.\nReturns 201 the first time the key is set, 200 on subsequent\nupdates. Values are encrypted at rest and only become visible\nto the running handler on the next deploy (`PUT /functions/{id}`\nwith the existing code is sufficient to refresh bindings).\n\nKeys must match `^[A-Z_][A-Z0-9_]*$` (uppercase letters,\ndigits, underscores; first character is a letter or\nunderscore). Values are at most 4096 UTF-8 bytes. System-\nmanaged keys are reserved and rejected.\n",
|
|
3119
|
+
"description": "Idempotent insert-or-update keyed on `(function_id, key)`.\nReturns 201 the first time the key is set, 200 on subsequent\nupdates. Values are encrypted at rest and only become visible\nto the running handler on the next deploy (`PUT /functions/{id}`\nwith the existing code is sufficient to refresh bindings).\n\nKeys must match `^[A-Z_][A-Z0-9_]*$` (uppercase letters,\ndigits, underscores; first character is a letter or\nunderscore). Values are at most 4096 UTF-8 bytes. System-\nmanaged keys are reserved and rejected.\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.",
|
|
3005
3120
|
"flags": {
|
|
3006
3121
|
"api-key": {
|
|
3007
3122
|
"description": "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
@@ -3365,7 +3480,7 @@
|
|
|
3365
3480
|
"functions:set-function-secret": {
|
|
3366
3481
|
"aliases": [],
|
|
3367
3482
|
"args": {},
|
|
3368
|
-
"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",
|
|
3483
|
+
"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.",
|
|
3369
3484
|
"flags": {
|
|
3370
3485
|
"api-key": {
|
|
3371
3486
|
"description": "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
@@ -3506,7 +3621,7 @@
|
|
|
3506
3621
|
"functions:update-function": {
|
|
3507
3622
|
"aliases": [],
|
|
3508
3623
|
"args": {},
|
|
3509
|
-
"description": "Replaces the function's source code with the body's `code` and\ntriggers a redeploy. Same size limits as `POST /functions`.\nUse this verb to push secret writes into the running handler:\npassing the same `code` re-runs the deploy and refreshes the\nbinding set with the latest values from the secrets table.\n\nOn a 502 deploy failure, the previously-deployed code stays\nlive; the runtime never serves a half-built bundle. The\n`deploy_error` field on the returned record carries the error\nthat came back from the runtime so you can surface it to users\nwithout polling.\n",
|
|
3624
|
+
"description": "Replaces the function's source code with the body's `code` and\ntriggers a redeploy. Same size limits as `POST /functions`.\nUse this verb to push secret writes into the running handler:\npassing the same `code` re-runs the deploy and refreshes the\nbinding set with the latest values from the secrets table.\n\nOn a 502 deploy failure, the previously-deployed code stays\nlive; the runtime never serves a half-built bundle. The\n`deploy_error` field on the returned record carries the error\nthat came back from the runtime so you can surface it to users\nwithout polling.\n\n\nTip: prefer `primitive functions:redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
|
|
3510
3625
|
"flags": {
|
|
3511
3626
|
"api-key": {
|
|
3512
3627
|
"description": "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
@@ -4168,5 +4283,5 @@
|
|
|
4168
4283
|
"enableJsonFlag": false
|
|
4169
4284
|
}
|
|
4170
4285
|
},
|
|
4171
|
-
"version": "0.
|
|
4286
|
+
"version": "0.23.0"
|
|
4172
4287
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitivedotdev/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Official Primitive Node.js SDK: webhook, api, openapi, contract, and parser modules",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"description": "View and replay webhook delivery attempts"
|
|
85
85
|
},
|
|
86
86
|
"functions": {
|
|
87
|
-
"description": "Deploy JavaScript handlers that run on inbound mail. Use `primitive functions:deploy --name <name> --file <bundle.js>`
|
|
87
|
+
"description": "Deploy JavaScript handlers that run on inbound mail. Use `primitive functions:init <name>` to scaffold a deployable project (handler, package.json, build script). Use `primitive functions:deploy --name <name> --file <bundle.js>` to create, `primitive functions:redeploy --id <id> --file <bundle.js>` to push a new bundle, and `primitive functions:set-secret --id <id> --key <KEY> --value <value> [--redeploy]` to write a secret (with optional one-call redeploy so the value lands in the running handler). The auto-generated functions:create-function / functions:update-function / functions:create-function-secret / functions:set-function-secret operations stay available for the full body-string surface."
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
90
|
"topicSeparator": " "
|