@primitivedotdev/cli 0.24.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.
@@ -0,0 +1,262 @@
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 minor or major version bump of the SDK; keep in lockstep with
18
+ // the CLI's own @primitivedotdev/sdk dep range in cli-node/package.json
19
+ // so scaffolded projects use the same SDK version the CLI was built
20
+ // and tested against.
21
+ const SDK_VERSION_RANGE = "^0.23.0";
22
+ // esbuild version range. Pinned to the latest stable major used
23
+ // elsewhere in the Primitive codebase for bundling Workers-style
24
+ // handlers. Caret range so patch fixes flow in automatically.
25
+ const ESBUILD_VERSION_RANGE = "^0.27.0";
26
+ // Validate a directory name passed as the positional argument.
27
+ // Matches a conservative slug shape: lowercase letters, digits,
28
+ // hyphens, underscores. Rejecting weirder names up front prevents
29
+ // surprises when the same string lands in package.json's `name`
30
+ // field (which has its own validation rules) or in shell scripts.
31
+ const VALID_NAME = /^[a-z0-9][a-z0-9_-]{0,62}$/;
32
+ export function isValidFunctionName(name) {
33
+ return VALID_NAME.test(name);
34
+ }
35
+ // File contents for the scaffolded project. Each renderer takes the
36
+ // function name and returns the raw file body. Kept as named exports
37
+ // so the unit test can assert content without having to spin up the
38
+ // oclif command lifecycle.
39
+ export function renderHandler() {
40
+ return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
41
+ import { createPrimitiveClient } from "@primitivedotdev/sdk/api";
42
+
43
+ export default {
44
+ async fetch(
45
+ req: Request,
46
+ env: { PRIMITIVE_API_KEY: string },
47
+ ): Promise<Response> {
48
+ const event = (await req.json()) as {
49
+ email: { headers: { from?: string; subject?: string } };
50
+ };
51
+ const client = createPrimitiveClient({ apiKey: env.PRIMITIVE_API_KEY });
52
+
53
+ const reply = await client.send({
54
+ from: "you@your-domain.primitive.email",
55
+ to: event.email.headers.from ?? "you@your-domain.primitive.email",
56
+ subject: \`Re: \${event.email.headers.subject ?? ""}\`,
57
+ bodyText: "Got your message.",
58
+ });
59
+
60
+ return Response.json({ ok: true, reply });
61
+ },
62
+ };
63
+ `;
64
+ }
65
+ export function renderPackageJson(name) {
66
+ const pkg = {
67
+ name,
68
+ version: "0.1.0",
69
+ private: true,
70
+ type: "module",
71
+ scripts: {
72
+ build: "node build.mjs",
73
+ deploy: `npm run build && primitive functions:deploy --name ${name} --file ./dist/handler.js`,
74
+ redeploy: "npm run build && primitive functions:redeploy --id $PRIMITIVE_FUNCTION_ID --file ./dist/handler.js",
75
+ },
76
+ dependencies: {
77
+ "@primitivedotdev/sdk": SDK_VERSION_RANGE,
78
+ },
79
+ devDependencies: {
80
+ esbuild: ESBUILD_VERSION_RANGE,
81
+ typescript: "^5.7.2",
82
+ },
83
+ };
84
+ return `${JSON.stringify(pkg, null, 2)}\n`;
85
+ }
86
+ export function renderBuildMjs() {
87
+ return `import { build } from "esbuild";
88
+
89
+ // Bundle handler.ts into a single ESM file suitable for the Primitive
90
+ // Functions runtime. The runtime is a Workers-style environment, so
91
+ // we pick the "worker" / "browser" export conditions on @primitivedotdev/sdk
92
+ // (which routes us to the /api subpath safely without dragging in
93
+ // node:crypto-dependent webhook helpers).
94
+
95
+ await build({
96
+ entryPoints: ["handler.ts"],
97
+ bundle: true,
98
+ format: "esm",
99
+ platform: "browser",
100
+ target: "es2022",
101
+ conditions: ["worker", "browser"],
102
+ outfile: "dist/handler.js",
103
+ });
104
+ `;
105
+ }
106
+ export function renderTsconfig() {
107
+ const tsconfig = {
108
+ compilerOptions: {
109
+ target: "ES2022",
110
+ module: "ESNext",
111
+ moduleResolution: "Bundler",
112
+ strict: true,
113
+ lib: ["ES2022", "WebWorker"],
114
+ types: [],
115
+ esModuleInterop: true,
116
+ skipLibCheck: true,
117
+ },
118
+ include: ["handler.ts"],
119
+ };
120
+ return `${JSON.stringify(tsconfig, null, 2)}\n`;
121
+ }
122
+ export function renderGitignore() {
123
+ return "node_modules\ndist\n";
124
+ }
125
+ export function renderReadme(name) {
126
+ return `# ${name}
127
+
128
+ ## What this is
129
+
130
+ A Primitive Function: a JavaScript handler that runs on inbound mail.
131
+ It receives the \`email.received\` event, demonstrates a basic reply
132
+ via the Primitive SDK, and returns a JSON envelope.
133
+
134
+ ## Develop
135
+
136
+ \`\`\`
137
+ npm install
138
+ npm run build
139
+ \`\`\`
140
+
141
+ ## Deploy
142
+
143
+ \`\`\`
144
+ npm run deploy
145
+ \`\`\`
146
+
147
+ The deploy step calls \`primitive functions:deploy\` (provided by the
148
+ \`@primitivedotdev/cli\` package; install with
149
+ \`npm install -g @primitivedotdev/cli\` or run via
150
+ \`npx @primitivedotdev/cli@latest <command>\`). It requires
151
+ \`PRIMITIVE_API_KEY\` to be set in your shell (or pass \`--api-key\`).
152
+ Run \`primitive login\` once to save a key in your CLI config if you
153
+ prefer that to an env var.
154
+ `;
155
+ }
156
+ // Files written by the scaffolder, in the order they're created.
157
+ // Exported as a pure function so the unit test can verify the
158
+ // exact content of every file without invoking the command and
159
+ // touching disk.
160
+ export function scaffoldFiles(name) {
161
+ return [
162
+ { contents: renderHandler(), relativePath: "handler.ts" },
163
+ { contents: renderPackageJson(name), relativePath: "package.json" },
164
+ { contents: renderBuildMjs(), relativePath: "build.mjs" },
165
+ { contents: renderTsconfig(), relativePath: "tsconfig.json" },
166
+ { contents: renderGitignore(), relativePath: ".gitignore" },
167
+ { contents: renderReadme(name), relativePath: "README.md" },
168
+ ];
169
+ }
170
+ // Write the scaffold to disk. Refuses to overwrite an existing
171
+ // directory: if `outDir` exists the function throws and leaves the
172
+ // filesystem untouched. On any write error after creating the
173
+ // directory, the partially-written tree is cleaned up so re-runs
174
+ // see a clean slate. Exported for unit testing.
175
+ export function writeScaffold(params) {
176
+ if (!isValidFunctionName(params.name)) {
177
+ 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 });
178
+ }
179
+ const files = scaffoldFiles(params.name);
180
+ const written = [];
181
+ // Create the target directory with recursive: false so the check
182
+ // and the create happen in one syscall. mkdirSync throws EEXIST
183
+ // atomically if the path already exists, which closes the TOCTOU
184
+ // window between a separate existsSync check and the mkdir call.
185
+ try {
186
+ mkdirSync(params.outDir, { recursive: false });
187
+ }
188
+ catch (error) {
189
+ const code = error.code;
190
+ if (code === "EEXIST") {
191
+ throw new Errors.CLIError(`Target directory already exists: ${params.outDir}. Refusing to overwrite. Remove it or pick a different --out-dir.`, { exit: 1 });
192
+ }
193
+ if (code === "ENOENT") {
194
+ throw new Errors.CLIError(`Parent directory does not exist for ${params.outDir}. Create it first or pick a different --out-dir.`, { exit: 1 });
195
+ }
196
+ const detail = error instanceof Error ? error.message : String(error);
197
+ throw new Errors.CLIError(`Failed to create ${params.outDir}: ${detail}`, {
198
+ exit: 1,
199
+ });
200
+ }
201
+ try {
202
+ for (const file of files) {
203
+ const fullPath = resolve(params.outDir, file.relativePath);
204
+ mkdirSync(dirname(fullPath), { recursive: true });
205
+ writeFileSync(fullPath, file.contents, "utf8");
206
+ written.push(fullPath);
207
+ }
208
+ }
209
+ catch (error) {
210
+ // Roll back the partial scaffold so the user can retry without
211
+ // tripping the "directory already exists" guard above.
212
+ try {
213
+ rmSync(params.outDir, { force: true, recursive: true });
214
+ }
215
+ catch {
216
+ // Best-effort cleanup; surface the original error regardless.
217
+ }
218
+ const detail = error instanceof Error ? error.message : String(error);
219
+ throw new Errors.CLIError(`Failed to write scaffold to ${params.outDir}: ${detail}`, { exit: 1 });
220
+ }
221
+ return { written };
222
+ }
223
+ class FunctionsInitCommand extends Command {
224
+ static description = `Scaffold a new Primitive Function project in ./<name>/ with handler.ts, package.json, build.mjs, tsconfig.json, .gitignore, and README.md.
225
+
226
+ The scaffolded handler imports \`createPrimitiveClient\` from
227
+ \`@primitivedotdev/sdk/api\` and demonstrates the canonical pattern:
228
+ parse the email.received event, send a reply via the SDK, return a
229
+ JSON envelope. The build script uses esbuild's JS API and emits
230
+ ./dist/handler.js, ready to hand to \`primitive functions:deploy --file\`.
231
+
232
+ Refuses to overwrite an existing directory. Use --out-dir to pick a
233
+ different target path than ./<name>/.`;
234
+ static summary = "Scaffold a new Primitive Function project ready for functions:deploy";
235
+ static examples = [
236
+ "<%= config.bin %> functions:init my-fn",
237
+ "<%= config.bin %> functions:init my-fn --out-dir ./functions/my-fn",
238
+ ];
239
+ static args = {
240
+ name: Args.string({
241
+ 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.",
242
+ required: true,
243
+ }),
244
+ };
245
+ static flags = {
246
+ "out-dir": Flags.string({
247
+ description: "Directory to scaffold into. Defaults to ./<name>/. Must not already exist.",
248
+ }),
249
+ };
250
+ async run() {
251
+ const { args, flags } = await this.parse(FunctionsInitCommand);
252
+ const outDir = resolve(flags["out-dir"] ?? `./${args.name}`);
253
+ writeScaffold({ name: args.name, outDir });
254
+ this.log(`Scaffolded ${outDir}.`);
255
+ this.log("Next:");
256
+ this.log(` cd ${outDir}`);
257
+ this.log(" npm install");
258
+ this.log(" npm run build");
259
+ this.log(` primitive functions:deploy --name ${args.name} --file ./dist/handler.js`);
260
+ }
261
+ }
262
+ export default FunctionsInitCommand;
@@ -0,0 +1,112 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { PrimitiveApiClient, updateFunction } from "@primitivedotdev/sdk/api";
3
+ import { extractErrorPayload, readTextFileFlag, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
4
+ import { resolveCliAuth } from "../auth.js";
5
+ import { emitRawSendMailFetchWarning } from "../lint/raw-send-mail-fetch.js";
6
+ // `primitive functions:redeploy` is the agent-grade shortcut for
7
+ // `functions:update-function`. Same file-reading ergonomic as
8
+ // functions:deploy but for an existing function. Use this to push a
9
+ // new bundle, OR to refresh secret bindings: passing the
10
+ // previously-deployed bundle (or any equivalent file) re-runs the
11
+ // deploy and refreshes env from the secrets table, which is how
12
+ // secret writes go live.
13
+ class FunctionsRedeployCommand extends Command {
14
+ static description = `Update or redeploy a function from a bundled handler file. Agent-grade shortcut for functions:update-function.
15
+
16
+ Use to push a new bundle OR to refresh secret bindings into the
17
+ running handler. The same file is fine for both: the deploy reads
18
+ the bindings table fresh on every call, so passing the existing
19
+ bundle picks up any secret writes since the last deploy.`;
20
+ static summary = "Redeploy a function from a bundled handler file";
21
+ static examples = [
22
+ "<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js",
23
+ "<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js --source-map-file ./bundle.js.map",
24
+ ];
25
+ static flags = {
26
+ "api-key": Flags.string({
27
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
28
+ env: "PRIMITIVE_API_KEY",
29
+ }),
30
+ "api-base-url-1": Flags.string({
31
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
32
+ env: "PRIMITIVE_API_BASE_URL_1",
33
+ hidden: true,
34
+ }),
35
+ "api-base-url-2": Flags.string({
36
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
37
+ env: "PRIMITIVE_API_BASE_URL_2",
38
+ hidden: true,
39
+ }),
40
+ id: Flags.string({
41
+ description: "Function id (UUID). The function must already exist.",
42
+ required: true,
43
+ }),
44
+ file: Flags.string({
45
+ description: "Path to the bundled ESM handler file. Loaded as the `code` body field.",
46
+ required: true,
47
+ }),
48
+ "source-map-file": Flags.string({
49
+ description: "Optional path to a source map for the bundle. Used to symbolicate stack traces in the function's logs.",
50
+ }),
51
+ time: Flags.boolean({
52
+ description: TIME_FLAG_DESCRIPTION,
53
+ }),
54
+ };
55
+ async run() {
56
+ const { flags } = await this.parse(FunctionsRedeployCommand);
57
+ await runWithTiming(flags.time, async () => {
58
+ // Reads inside the timed block: --time captures disk I/O too,
59
+ // which is the latency the flag is meant to surface.
60
+ const code = readTextFileFlag(flags.file, "--file");
61
+ const sourceMap = flags["source-map-file"]
62
+ ? readTextFileFlag(flags["source-map-file"], "--source-map-file")
63
+ : undefined;
64
+ // Non-blocking deploy-time lint: if the bundle has a raw
65
+ // fetch(...) call against /send-mail, nudge the author toward
66
+ // `createPrimitiveClient` from `@primitivedotdev/sdk/api`.
67
+ // Same check as functions:deploy; warning goes to stderr and
68
+ // the deploy continues regardless.
69
+ emitRawSendMailFetchWarning(code, (chunk) => process.stderr.write(chunk));
70
+ const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
71
+ flags["api-base-url-2"] !== undefined;
72
+ const auth = resolveCliAuth({
73
+ apiKey: flags["api-key"],
74
+ apiBaseUrl1: flags["api-base-url-1"],
75
+ apiBaseUrl2: flags["api-base-url-2"],
76
+ configDir: this.config.configDir,
77
+ });
78
+ const apiClient = new PrimitiveApiClient({
79
+ apiKey: auth.apiKey,
80
+ apiBaseUrl1: auth.apiBaseUrl1,
81
+ apiBaseUrl2: auth.apiBaseUrl2,
82
+ });
83
+ const authFailureContext = {
84
+ auth,
85
+ baseUrlOverridden,
86
+ configDir: this.config.configDir,
87
+ };
88
+ const result = await updateFunction({
89
+ path: { id: flags.id },
90
+ body: {
91
+ code,
92
+ ...(sourceMap !== undefined ? { sourceMap } : {}),
93
+ },
94
+ client: apiClient.client,
95
+ responseStyle: "fields",
96
+ });
97
+ if (result.error) {
98
+ const errorPayload = extractErrorPayload(result.error);
99
+ writeErrorWithHints(errorPayload);
100
+ removeStaleSavedCredentialOnUnauthorized({
101
+ ...authFailureContext,
102
+ payload: errorPayload,
103
+ });
104
+ process.exitCode = 1;
105
+ return;
106
+ }
107
+ const envelope = result.data;
108
+ this.log(JSON.stringify(envelope?.data ?? null, null, 2));
109
+ });
110
+ }
111
+ }
112
+ export default FunctionsRedeployCommand;
@@ -0,0 +1,212 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { getFunction, PrimitiveApiClient, setFunctionSecret, updateFunction, } from "@primitivedotdev/sdk/api";
3
+ import { extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
4
+ import { resolveCliAuth } from "../auth.js";
5
+ // Pure-ish orchestration of the set-secret + optional redeploy
6
+ // flow. Pulled out as a named export so the unit test can drive
7
+ // both the happy path and each error stage with a fake API
8
+ // surface, without spinning up a real client or the oclif
9
+ // command lifecycle.
10
+ //
11
+ // The redeploy step uses the function's CURRENT code (fetched via
12
+ // getFunction) as the new bundle. This is the documented way to
13
+ // "refresh secret bindings without changing the handler": the
14
+ // server-side deploy reads the secrets table fresh on every call,
15
+ // so re-deploying the same code picks up the secret we just wrote.
16
+ export async function runSetSecret(api, params) {
17
+ const setResult = await api.setSecret({
18
+ id: params.id,
19
+ key: params.key,
20
+ value: params.value,
21
+ });
22
+ if (setResult.error) {
23
+ return {
24
+ kind: "error",
25
+ payload: extractErrorPayload(setResult.error),
26
+ stage: "set-secret",
27
+ };
28
+ }
29
+ const secret = setResult.data?.data;
30
+ if (!secret) {
31
+ // Server returned 2xx with no `data` body. Treat as an error
32
+ // so we don't fabricate a success payload; this should not
33
+ // happen in practice but the shape forces us to handle it.
34
+ return {
35
+ kind: "error",
36
+ payload: {
37
+ code: "client_error",
38
+ message: "Secret write returned no data",
39
+ },
40
+ stage: "set-secret",
41
+ };
42
+ }
43
+ if (!params.redeploy) {
44
+ return { kind: "ok", result: { secret } };
45
+ }
46
+ const fnResult = await api.getFunction({ id: params.id });
47
+ if (fnResult.error) {
48
+ return {
49
+ kind: "error",
50
+ payload: extractErrorPayload(fnResult.error),
51
+ stage: "get-function",
52
+ };
53
+ }
54
+ const fn = fnResult.data?.data;
55
+ if (!fn) {
56
+ return {
57
+ kind: "error",
58
+ payload: {
59
+ code: "client_error",
60
+ message: "Could not read current function code for redeploy",
61
+ },
62
+ stage: "get-function",
63
+ };
64
+ }
65
+ const updateResult = await api.updateFunction({
66
+ code: fn.code,
67
+ id: params.id,
68
+ });
69
+ if (updateResult.error) {
70
+ return {
71
+ kind: "error",
72
+ payload: extractErrorPayload(updateResult.error),
73
+ stage: "redeploy",
74
+ };
75
+ }
76
+ const redeployed = updateResult.data?.data;
77
+ if (!redeployed) {
78
+ return {
79
+ kind: "error",
80
+ payload: {
81
+ code: "client_error",
82
+ message: "Redeploy returned no data",
83
+ },
84
+ stage: "redeploy",
85
+ };
86
+ }
87
+ return { kind: "ok", result: { redeploy: redeployed, secret } };
88
+ }
89
+ class FunctionsSetSecretCommand extends Command {
90
+ 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.
91
+
92
+ Without --redeploy this is a plain secret upsert: the value is
93
+ encrypted at rest but is NOT visible to the running handler until
94
+ the next deploy. Pass --redeploy to re-run the deploy with the
95
+ function's current code in the same call, which refreshes the
96
+ binding set with the value you just wrote.
97
+
98
+ Keys must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters, digits,
99
+ underscores; first character is a letter or underscore). System-
100
+ managed keys are reserved and rejected.`;
101
+ static summary = "Write a function secret (optionally redeploying to push it live)";
102
+ static examples = [
103
+ "<%= config.bin %> functions:set-secret --id <fn-id> --key API_TOKEN --value abc123",
104
+ "<%= config.bin %> functions:set-secret --id <fn-id> --key API_TOKEN --value abc123 --redeploy",
105
+ ];
106
+ static flags = {
107
+ "api-key": Flags.string({
108
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
109
+ env: "PRIMITIVE_API_KEY",
110
+ }),
111
+ "api-base-url-1": Flags.string({
112
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
113
+ env: "PRIMITIVE_API_BASE_URL_1",
114
+ hidden: true,
115
+ }),
116
+ "api-base-url-2": Flags.string({
117
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
118
+ env: "PRIMITIVE_API_BASE_URL_2",
119
+ hidden: true,
120
+ }),
121
+ id: Flags.string({
122
+ description: "Function id (UUID). The function must already exist.",
123
+ required: true,
124
+ }),
125
+ key: Flags.string({
126
+ description: "Secret key. Uppercase letters, digits, underscores; must start with a letter or underscore. System-managed keys are reserved.",
127
+ required: true,
128
+ }),
129
+ value: Flags.string({
130
+ description: "Secret value (up to 4096 UTF-8 bytes). Encrypted at rest.",
131
+ required: true,
132
+ }),
133
+ redeploy: Flags.boolean({
134
+ 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.",
135
+ }),
136
+ time: Flags.boolean({
137
+ description: TIME_FLAG_DESCRIPTION,
138
+ }),
139
+ };
140
+ async run() {
141
+ const { flags } = await this.parse(FunctionsSetSecretCommand);
142
+ await runWithTiming(flags.time, async () => {
143
+ const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
144
+ flags["api-base-url-2"] !== undefined;
145
+ const auth = resolveCliAuth({
146
+ apiKey: flags["api-key"],
147
+ apiBaseUrl1: flags["api-base-url-1"],
148
+ apiBaseUrl2: flags["api-base-url-2"],
149
+ configDir: this.config.configDir,
150
+ });
151
+ const apiClient = new PrimitiveApiClient({
152
+ apiKey: auth.apiKey,
153
+ apiBaseUrl1: auth.apiBaseUrl1,
154
+ apiBaseUrl2: auth.apiBaseUrl2,
155
+ });
156
+ const authFailureContext = {
157
+ auth,
158
+ baseUrlOverridden,
159
+ configDir: this.config.configDir,
160
+ };
161
+ // Adapter: thin wrappers around the generated SDK calls,
162
+ // routed through host 1 (apiClient.client). The secrets and
163
+ // function-detail endpoints are not on host 2.
164
+ const apiSurface = {
165
+ getFunction: (p) => getFunction({
166
+ client: apiClient.client,
167
+ path: { id: p.id },
168
+ responseStyle: "fields",
169
+ }),
170
+ setSecret: (p) => setFunctionSecret({
171
+ body: { value: p.value },
172
+ client: apiClient.client,
173
+ path: { id: p.id, key: p.key },
174
+ responseStyle: "fields",
175
+ }),
176
+ updateFunction: (p) => updateFunction({
177
+ body: { code: p.code },
178
+ client: apiClient.client,
179
+ path: { id: p.id },
180
+ responseStyle: "fields",
181
+ }),
182
+ };
183
+ const outcome = await runSetSecret(apiSurface, {
184
+ id: flags.id,
185
+ key: flags.key,
186
+ redeploy: flags.redeploy === true,
187
+ value: flags.value,
188
+ });
189
+ if (outcome.kind === "error") {
190
+ // Stage-specific framing on stderr so callers can tell
191
+ // whether the secret landed before a failed redeploy. The
192
+ // JSON envelope still goes through writeErrorWithHints so
193
+ // any actionable hint (e.g. unauthorized) is surfaced.
194
+ if (outcome.stage === "get-function") {
195
+ 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");
196
+ }
197
+ else if (outcome.stage === "redeploy") {
198
+ 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");
199
+ }
200
+ writeErrorWithHints(outcome.payload);
201
+ removeStaleSavedCredentialOnUnauthorized({
202
+ ...authFailureContext,
203
+ payload: outcome.payload,
204
+ });
205
+ process.exitCode = 1;
206
+ return;
207
+ }
208
+ this.log(JSON.stringify(outcome.result, null, 2));
209
+ });
210
+ }
211
+ }
212
+ export default FunctionsSetSecretCommand;