@primitivedotdev/sdk 0.20.0 → 0.22.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.
@@ -36,9 +36,15 @@ class FunctionsDeployCommand extends Command {
36
36
  description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
37
37
  env: "PRIMITIVE_API_KEY",
38
38
  }),
39
- "base-url": Flags.string({
40
- description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
41
- env: "PRIMITIVE_API_URL",
39
+ "api-base-url-1": Flags.string({
40
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
41
+ env: "PRIMITIVE_API_BASE_URL_1",
42
+ hidden: true,
43
+ }),
44
+ "api-base-url-2": Flags.string({
45
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
46
+ env: "PRIMITIVE_API_BASE_URL_2",
47
+ hidden: true,
42
48
  }),
43
49
  name: Flags.string({
44
50
  description: "Slug-style name. Lowercase letters, digits, hyphens, underscores. 1-64 chars. Must be unique within the org.",
@@ -66,15 +72,18 @@ class FunctionsDeployCommand extends Command {
66
72
  const sourceMap = flags["source-map-file"]
67
73
  ? readTextFileFlag(flags["source-map-file"], "--source-map-file")
68
74
  : undefined;
69
- const baseUrlOverridden = flags["base-url"] !== undefined;
75
+ const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
76
+ flags["api-base-url-2"] !== undefined;
70
77
  const auth = resolveCliAuth({
71
78
  apiKey: flags["api-key"],
72
- baseUrl: flags["base-url"],
79
+ apiBaseUrl1: flags["api-base-url-1"],
80
+ apiBaseUrl2: flags["api-base-url-2"],
73
81
  configDir: this.config.configDir,
74
82
  });
75
83
  const apiClient = new PrimitiveApiClient({
76
84
  apiKey: auth.apiKey,
77
- baseUrl: auth.baseUrl,
85
+ apiBaseUrl1: auth.apiBaseUrl1,
86
+ apiBaseUrl2: auth.apiBaseUrl2,
78
87
  });
79
88
  const authFailureContext = {
80
89
  auth,
@@ -27,9 +27,15 @@ class FunctionsRedeployCommand extends Command {
27
27
  description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
28
28
  env: "PRIMITIVE_API_KEY",
29
29
  }),
30
- "base-url": Flags.string({
31
- description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
32
- env: "PRIMITIVE_API_URL",
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,
33
39
  }),
34
40
  id: Flags.string({
35
41
  description: "Function id (UUID). The function must already exist.",
@@ -55,15 +61,18 @@ class FunctionsRedeployCommand extends Command {
55
61
  const sourceMap = flags["source-map-file"]
56
62
  ? readTextFileFlag(flags["source-map-file"], "--source-map-file")
57
63
  : undefined;
58
- const baseUrlOverridden = flags["base-url"] !== undefined;
64
+ const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
65
+ flags["api-base-url-2"] !== undefined;
59
66
  const auth = resolveCliAuth({
60
67
  apiKey: flags["api-key"],
61
- baseUrl: flags["base-url"],
68
+ apiBaseUrl1: flags["api-base-url-1"],
69
+ apiBaseUrl2: flags["api-base-url-2"],
62
70
  configDir: this.config.configDir,
63
71
  });
64
72
  const apiClient = new PrimitiveApiClient({
65
73
  apiKey: auth.apiKey,
66
- baseUrl: auth.baseUrl,
74
+ apiBaseUrl1: auth.apiBaseUrl1,
75
+ apiBaseUrl2: auth.apiBaseUrl2,
67
76
  });
68
77
  const authFailureContext = {
69
78
  auth,
@@ -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;
@@ -4,7 +4,7 @@ import { Command, Errors, Flags } from "@oclif/core";
4
4
  import { getAccount, pollCliLogin, startCliLogin, } from "../../api/generated/sdk.gen.js";
5
5
  import { PrimitiveApiClient } from "../../api/index.js";
6
6
  import { API_ERROR_CODES, extractErrorCode, extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, writeErrorWithHints, } from "../api-command.js";
7
- import { acquireCliCredentialsLock, credentialsPath, loadCliCredentials, normalizeBaseUrl, saveCliCredentials, } from "../auth.js";
7
+ import { acquireCliCredentialsLock, credentialsPath, loadCliCredentials, normalizeApiBaseUrl1, normalizeApiBaseUrl2, saveCliCredentials, } from "../auth.js";
8
8
  const MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS = 60;
9
9
  function cliError(message) {
10
10
  return new Errors.CLIError(message, { exit: 1 });
@@ -36,13 +36,13 @@ function retryAfterSeconds(result) {
36
36
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
37
37
  }
38
38
  export async function checkExistingLogin(params) {
39
- const baseUrlOverridden = params.baseUrl !== undefined;
40
- const probeBaseUrl = baseUrlOverridden
41
- ? normalizeBaseUrl(params.baseUrl)
42
- : params.credentials.base_url;
39
+ const baseUrlOverridden = params.apiBaseUrl1 !== undefined;
40
+ const probeApiBaseUrl1 = baseUrlOverridden
41
+ ? normalizeApiBaseUrl1(params.apiBaseUrl1)
42
+ : params.credentials.api_base_url_1;
43
43
  const apiClient = new PrimitiveApiClient({
44
44
  apiKey: params.credentials.api_key,
45
- baseUrl: probeBaseUrl,
45
+ apiBaseUrl1: probeApiBaseUrl1,
46
46
  });
47
47
  const result = await (params.checkAccount ??
48
48
  ((client) => getAccount({
@@ -54,7 +54,10 @@ export async function checkExistingLogin(params) {
54
54
  const payload = extractErrorPayload(result.error);
55
55
  const auth = {
56
56
  apiKey: params.credentials.api_key,
57
- baseUrl: probeBaseUrl,
57
+ apiBaseUrl1: probeApiBaseUrl1,
58
+ // Host-2 isn't relevant to checkExistingLogin (login is on host-1
59
+ // only), but the auth shape requires it. Use the default.
60
+ apiBaseUrl2: normalizeApiBaseUrl2(undefined),
58
61
  credentials: params.credentials,
59
62
  source: "stored",
60
63
  };
@@ -84,9 +87,10 @@ class LoginCommand extends Command {
84
87
  "<%= config.bin %> login --force",
85
88
  ];
86
89
  static flags = {
87
- "base-url": Flags.string({
88
- description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
89
- env: "PRIMITIVE_API_URL",
90
+ "api-base-url-1": Flags.string({
91
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
92
+ env: "PRIMITIVE_API_BASE_URL_1",
93
+ hidden: true,
90
94
  }),
91
95
  "device-name": Flags.string({
92
96
  description: "Device name shown in the browser approval screen",
@@ -117,7 +121,7 @@ class LoginCommand extends Command {
117
121
  }
118
122
  }
119
123
  async runWithCredentialLock(flags) {
120
- const baseUrl = normalizeBaseUrl(flags["base-url"]);
124
+ const apiBaseUrl1 = normalizeApiBaseUrl1(flags["api-base-url-1"]);
121
125
  let existing;
122
126
  try {
123
127
  existing = loadCliCredentials(this.config.configDir);
@@ -134,7 +138,7 @@ class LoginCommand extends Command {
134
138
  }
135
139
  else if (existing) {
136
140
  const existingStatus = await checkExistingLogin({
137
- baseUrl: flags["base-url"],
141
+ apiBaseUrl1: flags["api-base-url-1"],
138
142
  configDir: this.config.configDir,
139
143
  credentials: existing,
140
144
  });
@@ -150,7 +154,7 @@ class LoginCommand extends Command {
150
154
  throw cliError(`Already logged in${org}. Run \`primitive logout\` before logging in again.`);
151
155
  }
152
156
  }
153
- const apiClient = new PrimitiveApiClient({ baseUrl });
157
+ const apiClient = new PrimitiveApiClient({ apiBaseUrl1 });
154
158
  const deviceName = flags["device-name"] ?? hostname();
155
159
  const started = await startCliLogin({
156
160
  body: {
@@ -192,7 +196,7 @@ class LoginCommand extends Command {
192
196
  }
193
197
  saveCliCredentials(this.config.configDir, {
194
198
  api_key: login.api_key,
195
- base_url: baseUrl,
199
+ api_base_url_1: apiBaseUrl1,
196
200
  created_at: new Date().toISOString(),
197
201
  key_id: login.key_id,
198
202
  key_prefix: login.key_prefix,
@@ -2,7 +2,7 @@ import { Command, Errors, Flags } from "@oclif/core";
2
2
  import { cliLogout } from "../../api/generated/sdk.gen.js";
3
3
  import { PrimitiveApiClient } from "../../api/index.js";
4
4
  import { API_ERROR_CODES, extractErrorCode, extractErrorPayload, writeErrorWithHints, } from "../api-command.js";
5
- import { acquireCliCredentialsLock, deleteCliCredentials, loadCliCredentials, normalizeBaseUrl, } from "../auth.js";
5
+ import { acquireCliCredentialsLock, deleteCliCredentials, loadCliCredentials, normalizeApiBaseUrl1, } from "../auth.js";
6
6
  function cliError(message) {
7
7
  return new Errors.CLIError(message, { exit: 1 });
8
8
  }
@@ -15,9 +15,10 @@ class LogoutCommand extends Command {
15
15
  static summary = "Log out and revoke the saved CLI key";
16
16
  static examples = ["<%= config.bin %> logout"];
17
17
  static flags = {
18
- "base-url": Flags.string({
19
- description: "Override the API base URL used for key revocation",
20
- env: "PRIMITIVE_API_URL",
18
+ "api-base-url-1": Flags.string({
19
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
20
+ env: "PRIMITIVE_API_BASE_URL_1",
21
+ hidden: true,
21
22
  }),
22
23
  };
23
24
  async run() {
@@ -52,12 +53,12 @@ class LogoutCommand extends Command {
52
53
  if (!credentials) {
53
54
  throw cliError("Not logged in. Run `primitive login` to create saved CLI credentials.");
54
55
  }
55
- const baseUrl = flags["base-url"]
56
- ? normalizeBaseUrl(flags["base-url"])
57
- : credentials.base_url;
56
+ const apiBaseUrl1 = flags["api-base-url-1"]
57
+ ? normalizeApiBaseUrl1(flags["api-base-url-1"])
58
+ : credentials.api_base_url_1;
58
59
  const apiClient = new PrimitiveApiClient({
59
60
  apiKey: credentials.api_key,
60
- baseUrl,
61
+ apiBaseUrl1,
61
62
  });
62
63
  const result = await cliLogout({
63
64
  body: { key_id: credentials.key_id },
@@ -115,9 +115,15 @@ class SendCommand extends Command {
115
115
  description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
116
116
  env: "PRIMITIVE_API_KEY",
117
117
  }),
118
- "base-url": Flags.string({
119
- description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
120
- env: "PRIMITIVE_API_URL",
118
+ "api-base-url-1": Flags.string({
119
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
120
+ env: "PRIMITIVE_API_BASE_URL_1",
121
+ hidden: true,
122
+ }),
123
+ "api-base-url-2": Flags.string({
124
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
125
+ env: "PRIMITIVE_API_BASE_URL_2",
126
+ hidden: true,
121
127
  }),
122
128
  to: Flags.string({
123
129
  description: "Recipient address (e.g. alice@example.com).",
@@ -154,15 +160,18 @@ class SendCommand extends Command {
154
160
  throw new Errors.CLIError("Either --body or --html (or both) is required.");
155
161
  }
156
162
  await runWithTiming(flags.time, async () => {
157
- const baseUrlOverridden = flags["base-url"] !== undefined;
163
+ const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
164
+ flags["api-base-url-2"] !== undefined;
158
165
  const auth = resolveCliAuth({
159
166
  apiKey: flags["api-key"],
160
- baseUrl: flags["base-url"],
167
+ apiBaseUrl1: flags["api-base-url-1"],
168
+ apiBaseUrl2: flags["api-base-url-2"],
161
169
  configDir: this.config.configDir,
162
170
  });
163
171
  const apiClient = new PrimitiveApiClient({
164
172
  apiKey: auth.apiKey,
165
- baseUrl: auth.baseUrl,
173
+ apiBaseUrl1: auth.apiBaseUrl1,
174
+ apiBaseUrl2: auth.apiBaseUrl2,
166
175
  });
167
176
  const authFailureContext = {
168
177
  auth,
@@ -187,7 +196,12 @@ class SendCommand extends Command {
187
196
  ? { wait_timeout_ms: flags["wait-timeout-ms"] }
188
197
  : {}),
189
198
  },
190
- client: apiClient.client,
199
+ // /send-mail goes to the attachments-supporting host. The
200
+ // wrapper exposes the host-2 client as _sendClient for this
201
+ // and any other host-2 operation that lands here. Customer
202
+ // SDK callers should use PrimitiveClient.send() instead so
203
+ // the routing stays internal.
204
+ client: apiClient._sendClient,
191
205
  responseStyle: "fields",
192
206
  });
193
207
  if (result.error) {
@@ -25,9 +25,15 @@ class WhoamiCommand extends Command {
25
25
  description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
26
26
  env: "PRIMITIVE_API_KEY",
27
27
  }),
28
- "base-url": Flags.string({
29
- description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
30
- env: "PRIMITIVE_API_URL",
28
+ "api-base-url-1": Flags.string({
29
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
30
+ env: "PRIMITIVE_API_BASE_URL_1",
31
+ hidden: true,
32
+ }),
33
+ "api-base-url-2": Flags.string({
34
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
35
+ env: "PRIMITIVE_API_BASE_URL_2",
36
+ hidden: true,
31
37
  }),
32
38
  time: Flags.boolean({
33
39
  description: TIME_FLAG_DESCRIPTION,
@@ -36,15 +42,18 @@ class WhoamiCommand extends Command {
36
42
  async run() {
37
43
  const { flags } = await this.parse(WhoamiCommand);
38
44
  await runWithTiming(flags.time, async () => {
39
- const baseUrlOverridden = flags["base-url"] !== undefined;
45
+ const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
46
+ flags["api-base-url-2"] !== undefined;
40
47
  const auth = resolveCliAuth({
41
48
  apiKey: flags["api-key"],
42
- baseUrl: flags["base-url"],
49
+ apiBaseUrl1: flags["api-base-url-1"],
50
+ apiBaseUrl2: flags["api-base-url-2"],
43
51
  configDir: this.config.configDir,
44
52
  });
45
53
  const apiClient = new PrimitiveApiClient({
46
54
  apiKey: auth.apiKey,
47
- baseUrl: auth.baseUrl,
55
+ apiBaseUrl1: auth.apiBaseUrl1,
56
+ apiBaseUrl2: auth.apiBaseUrl2,
48
57
  });
49
58
  const result = await getAccount({
50
59
  client: apiClient.client,
@@ -73,7 +73,7 @@ export function renderFishCompletion(binName) {
73
73
  ]) {
74
74
  lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l '${fishEscape(parameter.name.replace(/_/g, "-"))}' -r -d '${fishEscape(parameter.description ?? parameter.name)}'`);
75
75
  }
76
- lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'api-key' -r -d 'Primitive API key (defaults to PRIMITIVE_API_KEY or saved primitive login credentials)'`, `complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'base-url' -r -d 'API base URL (defaults to PRIMITIVE_API_URL or production)'`);
76
+ lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'api-key' -r -d 'Primitive API key (defaults to PRIMITIVE_API_KEY or saved primitive login credentials)'`);
77
77
  if (operation.hasJsonBody) {
78
78
  lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'body' -r -d 'JSON request body'`, `complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'body-file' -r -d 'Path to a JSON file used as the request body'`);
79
79
  }
@@ -2,8 +2,11 @@ import { Args, Command, Errors } from "@oclif/core";
2
2
  import { operationManifest, } from "../openapi/index.js";
3
3
  import { createOperationCommand } from "./api-command.js";
4
4
  import EmailsLatestCommand from "./commands/emails-latest.js";
5
+ import EmailsWaitCommand from "./commands/emails-wait.js";
6
+ import EmailsWatchCommand from "./commands/emails-watch.js";
5
7
  import FunctionsDeployCommand from "./commands/functions-deploy.js";
6
8
  import FunctionsRedeployCommand from "./commands/functions-redeploy.js";
9
+ import FunctionsSetSecretCommand from "./commands/functions-set-secret.js";
7
10
  import LoginCommand from "./commands/login.js";
8
11
  import LogoutCommand from "./commands/logout.js";
9
12
  import SendCommand from "./commands/send.js";
@@ -133,6 +136,10 @@ export const COMMANDS = {
133
136
  // inbound emails as a compact text table. emails:list-emails stays
134
137
  // available for the full JSON envelope + cursor pagination.
135
138
  "emails:latest": EmailsLatestCommand,
139
+ // `emails:watch` and `emails:wait` poll the search API for new matching
140
+ // inbound mail. `watch` defaults to a human table; `wait` defaults to JSONL.
141
+ "emails:watch": EmailsWatchCommand,
142
+ "emails:wait": EmailsWaitCommand,
136
143
  // `functions:deploy` and `functions:redeploy` are file-input
137
144
  // shortcuts for create-function / update-function. The underlying
138
145
  // ops take `code` as a body string, which is awkward at the CLI
@@ -141,5 +148,12 @@ export const COMMANDS = {
141
148
  // available for callers that want the full surface.
142
149
  "functions:deploy": FunctionsDeployCommand,
143
150
  "functions:redeploy": FunctionsRedeployCommand,
151
+ // `functions:set-secret` is the one-call shortcut for "write a
152
+ // secret AND (optionally) push it live." The raw
153
+ // functions:set-function-secret / functions:create-function-secret
154
+ // operations only do the secret upsert; making the new value
155
+ // visible to the running handler requires a separate redeploy,
156
+ // which this shortcut folds in via --redeploy.
157
+ "functions:set-secret": FunctionsSetSecretCommand,
144
158
  ...generatedCommands,
145
159
  };