@primitivedotdev/cli 0.30.0 → 0.30.1

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.
package/bin/run.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { execute } from "@oclif/core";
4
- import { applyProxyAutoDetect } from "../dist/oclif/proxy-auto-detect.js";
3
+ import { restartWithProxyEnvIfNeeded } from "../dist/oclif/proxy-auto-detect.js";
5
4
 
6
- // Auto-set NODE_USE_ENV_PROXY=1 when HTTP(S)_PROXY is in the env.
7
- // Must run before any network init (e.g. before oclif loads commands
8
- // that touch fetch). See proxy-auto-detect.ts for the full rationale.
9
- applyProxyAutoDetect();
5
+ // Auto-restart with NODE_USE_ENV_PROXY=1 when HTTP(S)_PROXY is in the env.
6
+ // Node reads NODE_USE_ENV_PROXY during process startup, so mutating
7
+ // process.env inside this process is too late for built-in fetch.
8
+ restartWithProxyEnvIfNeeded();
10
9
 
10
+ const { execute } = await import("@oclif/core");
11
11
  await execute({ dir: import.meta.url });
@@ -371,7 +371,7 @@ const mergeConfigs = (a, b) => {
371
371
  ...b
372
372
  };
373
373
  if (config.baseUrl?.endsWith("/")) config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
374
- config.headers = mergeHeaders(a.headers, b.headers);
374
+ config.headers = mergeHeaders$1(a.headers, b.headers);
375
375
  return config;
376
376
  };
377
377
  const headersEntries = (headers) => {
@@ -381,7 +381,7 @@ const headersEntries = (headers) => {
381
381
  });
382
382
  return entries;
383
383
  };
384
- const mergeHeaders = (...headers) => {
384
+ const mergeHeaders$1 = (...headers) => {
385
385
  const mergedHeaders = new Headers();
386
386
  for (const header of headers) {
387
387
  if (!header) continue;
@@ -461,7 +461,7 @@ const createClient = (config = {}) => {
461
461
  ..._config,
462
462
  ...options,
463
463
  fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
464
- headers: mergeHeaders(_config.headers, options.headers),
464
+ headers: mergeHeaders$1(_config.headers, options.headers),
465
465
  serializedBody: void 0
466
466
  };
467
467
  if (opts.security) await setAuthParams({
@@ -1604,17 +1604,18 @@ const updateFunction = (options) => (options.client ?? client).put({
1604
1604
  * Sends a real test email from a Primitive-controlled sender to a
1605
1605
  * local-part on one of the org's verified inbound domains. By
1606
1606
  * default the recipient is a synthetic
1607
- * `__primitive_function_test+<random>@<domain>` address that
1608
- * every handler's catch-all routing receives identically; pass
1609
- * `local_part` to override and exercise routing logic that
1610
- * branches on a specific recipient (the common pattern when one
1611
- * function handles multiple inboxes like `summarize@` and
1612
- * `action@`). The function fires through the normal MX delivery
1613
- * path, so reply / send-mail calls from inside the handler
1614
- * against the inbound's `email.id` work the same as in
1615
- * production. Returns immediately after the send is queued; the
1616
- * invocation appears on the function's invocations list within a
1617
- * few seconds.
1607
+ * `__primitive_function_test+<random>@<domain>` address on a
1608
+ * domain selected to route to the function. Scoped functions use
1609
+ * their scoped domain; fallback functions use a domain that has
1610
+ * no enabled domain-scoped endpoint. Pass `local_part` to
1611
+ * override and exercise routing logic that branches on a specific
1612
+ * recipient (the common pattern when one function handles multiple
1613
+ * inboxes like `summarize@` and `action@`). The function fires
1614
+ * through the normal MX delivery path, so reply / send-mail calls
1615
+ * from inside the handler against the inbound's `email.id` work
1616
+ * the same as in production. Returns immediately after the send is
1617
+ * queued; the invocation appears on the function's invocations
1618
+ * list within a few seconds.
1618
1619
  *
1619
1620
  * Requires that the function is currently `deployed`. Returns 422
1620
1621
  * if the function is in `pending` or `failed` state, or if the
@@ -3274,7 +3275,7 @@ const openapiDocument = {
3274
3275
  "post": {
3275
3276
  "operationId": "testFunction",
3276
3277
  "summary": "Send a test invocation",
3277
- "description": "Sends a real test email from a Primitive-controlled sender to a\nlocal-part on one of the org's verified inbound domains. By\ndefault the recipient is a synthetic\n`__primitive_function_test+<random>@<domain>` address that\nevery handler's catch-all routing receives identically; pass\n`local_part` to override and exercise routing logic that\nbranches on a specific recipient (the common pattern when one\nfunction handles multiple inboxes like `summarize@` and\n`action@`). The function fires through the normal MX delivery\npath, so reply / send-mail calls from inside the handler\nagainst the inbound's `email.id` work the same as in\nproduction. Returns immediately after the send is queued; the\ninvocation appears on the function's invocations list within a\nfew seconds.\n\nRequires that the function is currently `deployed`. Returns 422\nif the function is in `pending` or `failed` state, or if the\norg has no verified inbound domain to receive the test mail.\nReturns 400 if `local_part` is set to a value that does not\nmatch the local-part character set.\n",
3278
+ "description": "Sends a real test email from a Primitive-controlled sender to a\nlocal-part on one of the org's verified inbound domains. By\ndefault the recipient is a synthetic\n`__primitive_function_test+<random>@<domain>` address on a\ndomain selected to route to the function. Scoped functions use\ntheir scoped domain; fallback functions use a domain that has\nno enabled domain-scoped endpoint. Pass `local_part` to\noverride and exercise routing logic that branches on a specific\nrecipient (the common pattern when one function handles multiple\ninboxes like `summarize@` and `action@`). The function fires\nthrough the normal MX delivery path, so reply / send-mail calls\nfrom inside the handler against the inbound's `email.id` work\nthe same as in production. Returns immediately after the send is\nqueued; the invocation appears on the function's invocations\nlist within a few seconds.\n\nRequires that the function is currently `deployed`. Returns 422\nif the function is in `pending` or `failed` state, or if the\norg has no verified inbound domain to receive the test mail.\nReturns 400 if `local_part` is set to a value that does not\nmatch the local-part character set.\n",
3278
3279
  "tags": ["Functions"],
3279
3280
  "requestBody": {
3280
3281
  "required": false,
@@ -9398,7 +9399,7 @@ const operationManifest = [
9398
9399
  "binaryResponse": false,
9399
9400
  "bodyRequired": false,
9400
9401
  "command": "test-function",
9401
- "description": "Sends a real test email from a Primitive-controlled sender to a\nlocal-part on one of the org's verified inbound domains. By\ndefault the recipient is a synthetic\n`__primitive_function_test+<random>@<domain>` address that\nevery handler's catch-all routing receives identically; pass\n`local_part` to override and exercise routing logic that\nbranches on a specific recipient (the common pattern when one\nfunction handles multiple inboxes like `summarize@` and\n`action@`). The function fires through the normal MX delivery\npath, so reply / send-mail calls from inside the handler\nagainst the inbound's `email.id` work the same as in\nproduction. Returns immediately after the send is queued; the\ninvocation appears on the function's invocations list within a\nfew seconds.\n\nRequires that the function is currently `deployed`. Returns 422\nif the function is in `pending` or `failed` state, or if the\norg has no verified inbound domain to receive the test mail.\nReturns 400 if `local_part` is set to a value that does not\nmatch the local-part character set.\n",
9402
+ "description": "Sends a real test email from a Primitive-controlled sender to a\nlocal-part on one of the org's verified inbound domains. By\ndefault the recipient is a synthetic\n`__primitive_function_test+<random>@<domain>` address on a\ndomain selected to route to the function. Scoped functions use\ntheir scoped domain; fallback functions use a domain that has\nno enabled domain-scoped endpoint. Pass `local_part` to\noverride and exercise routing logic that branches on a specific\nrecipient (the common pattern when one function handles multiple\ninboxes like `summarize@` and `action@`). The function fires\nthrough the normal MX delivery path, so reply / send-mail calls\nfrom inside the handler against the inbound's `email.id` work\nthe same as in production. Returns immediately after the send is\nqueued; the invocation appears on the function's invocations\nlist within a few seconds.\n\nRequires that the function is currently `deployed`. Returns 422\nif the function is in `pending` or `failed` state, or if the\norg has no verified inbound domain to receive the test mail.\nReturns 400 if `local_part` is set to a value that does not\nmatch the local-part character set.\n",
9402
9403
  "hasJsonBody": true,
9403
9404
  "method": "POST",
9404
9405
  "operationId": "testFunction",
@@ -10752,7 +10753,7 @@ const CREDENTIALS_FILE = "credentials.json";
10752
10753
  const CREDENTIALS_LOCK_DIR = "credentials.lock";
10753
10754
  const CREDENTIALS_LOCK_STALE_MS = 1800 * 1e3;
10754
10755
  const MALFORMED_CREDENTIALS_HINT = "Run `primitive logout` and then `primitive login`.";
10755
- function isRecord$1(value) {
10756
+ function isRecord$2(value) {
10756
10757
  return value !== null && typeof value === "object" && !Array.isArray(value);
10757
10758
  }
10758
10759
  function requireString(value, key) {
@@ -10775,7 +10776,7 @@ var StaleCredentialFormatError = class extends Error {
10775
10776
  }
10776
10777
  };
10777
10778
  function parseCredentials(raw) {
10778
- if (!isRecord$1(raw)) throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
10779
+ if (!isRecord$2(raw)) throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
10779
10780
  if (typeof raw.api_base_url_1 !== "string" && typeof raw.base_url === "string") throw new StaleCredentialFormatError();
10780
10781
  const orgName = raw.org_name;
10781
10782
  if (orgName !== null && typeof orgName !== "string") throw new Error(`Stored Primitive CLI credentials are malformed: org_name must be a string or null. ${MALFORMED_CREDENTIALS_HINT}`);
@@ -10920,6 +10921,269 @@ function resolveCliAuth(params) {
10920
10921
  };
10921
10922
  }
10922
10923
  //#endregion
10924
+ //#region src/oclif/cli-config.ts
10925
+ const CONFIG_FILE = "config.json";
10926
+ const CONFIG_VERSION = 1;
10927
+ const DEFAULT_ENVIRONMENT = "default";
10928
+ function cliConfigPath(configDir) {
10929
+ return join(configDir, CONFIG_FILE);
10930
+ }
10931
+ function cliConfigError(message) {
10932
+ return new Errors.CLIError(`${message} Run \`primitive config reset\` to clear the local CLI config.`, { exit: 1 });
10933
+ }
10934
+ function isRecord$1(value) {
10935
+ return value !== null && typeof value === "object" && !Array.isArray(value);
10936
+ }
10937
+ function normalizeCliEnvironmentName(name) {
10938
+ const trimmed = name?.trim();
10939
+ if (!trimmed) throw new Errors.CLIError("Environment name must be a non-empty string.", { exit: 1 });
10940
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/.test(trimmed)) throw new Errors.CLIError("Environment name must start with a letter or number and may only contain letters, numbers, '.', '_', or '-'.", { exit: 1 });
10941
+ return trimmed;
10942
+ }
10943
+ function validateCliHeaderName(name) {
10944
+ const trimmed = name.trim();
10945
+ if (!trimmed) throw new Errors.CLIError("Header name must be a non-empty string.", { exit: 1 });
10946
+ if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(trimmed)) throw new Errors.CLIError(`Invalid header name: ${name}`, { exit: 1 });
10947
+ if (trimmed.toLowerCase() === "authorization") throw new Errors.CLIError("The Authorization header is managed by PRIMITIVE_API_KEY or saved CLI credentials.", { exit: 1 });
10948
+ return trimmed;
10949
+ }
10950
+ function validateCliHeaderValue(value, name) {
10951
+ if (value.length === 0) throw new Errors.CLIError(`Header ${name} value must not be empty.`, { exit: 1 });
10952
+ if (/[\r\n\0]/.test(value)) throw new Errors.CLIError(`Header ${name} value must not contain CR, LF, or NUL characters.`, { exit: 1 });
10953
+ return value;
10954
+ }
10955
+ function parseHeaderAssignment(assignment) {
10956
+ const separator = assignment.indexOf("=");
10957
+ if (separator <= 0) throw new Errors.CLIError("Header values must use name=value syntax, for example `x-custom=secret`.", { exit: 1 });
10958
+ const name = validateCliHeaderName(assignment.slice(0, separator));
10959
+ return [name, validateCliHeaderValue(assignment.slice(separator + 1), name)];
10960
+ }
10961
+ function parseHeaders(raw, context) {
10962
+ if (raw === void 0) return {};
10963
+ if (!isRecord$1(raw)) throw cliConfigError(`${context} headers must be a JSON object.`);
10964
+ const headers = {};
10965
+ for (const [rawName, rawValue] of Object.entries(raw)) {
10966
+ const name = validateCliHeaderName(rawName);
10967
+ if (typeof rawValue !== "string") throw cliConfigError(`${context} header ${name} must be a string.`);
10968
+ headers[name] = validateCliHeaderValue(rawValue, name);
10969
+ }
10970
+ return headers;
10971
+ }
10972
+ function parseEnvironmentConfig(raw, context) {
10973
+ if (!isRecord$1(raw)) throw cliConfigError(`${context} must be a JSON object.`);
10974
+ const env = {};
10975
+ if (raw.api_base_url_1 !== void 0) {
10976
+ if (typeof raw.api_base_url_1 !== "string") throw cliConfigError(`${context}.api_base_url_1 must be a string.`);
10977
+ env.api_base_url_1 = normalizeApiBaseUrl1(raw.api_base_url_1);
10978
+ }
10979
+ if (raw.api_base_url_2 !== void 0) {
10980
+ if (typeof raw.api_base_url_2 !== "string") throw cliConfigError(`${context}.api_base_url_2 must be a string.`);
10981
+ env.api_base_url_2 = normalizeApiBaseUrl2(raw.api_base_url_2);
10982
+ }
10983
+ const headers = parseHeaders(raw.headers, context);
10984
+ if (Object.keys(headers).length > 0) env.headers = headers;
10985
+ return env;
10986
+ }
10987
+ function parseStoredCliConfig(raw) {
10988
+ if (!isRecord$1(raw)) throw cliConfigError("Primitive CLI config must be a JSON object.");
10989
+ if (raw.version !== CONFIG_VERSION) throw cliConfigError(`Primitive CLI config version must be ${CONFIG_VERSION}.`);
10990
+ const currentRaw = raw.current_environment;
10991
+ const current_environment = currentRaw === null || currentRaw === void 0 ? null : typeof currentRaw === "string" ? normalizeCliEnvironmentName(currentRaw) : (() => {
10992
+ throw cliConfigError("Primitive CLI config current_environment must be a string or null.");
10993
+ })();
10994
+ if (!isRecord$1(raw.environments)) throw cliConfigError("Primitive CLI config environments must be an object.");
10995
+ const environments = {};
10996
+ for (const [rawName, rawEnv] of Object.entries(raw.environments)) {
10997
+ const name = normalizeCliEnvironmentName(rawName);
10998
+ environments[name] = parseEnvironmentConfig(rawEnv, `Primitive CLI config environment ${name}`);
10999
+ }
11000
+ if (current_environment && !environments[current_environment]) throw cliConfigError(`Primitive CLI config current environment ${current_environment} does not exist.`);
11001
+ return {
11002
+ version: CONFIG_VERSION,
11003
+ current_environment,
11004
+ environments
11005
+ };
11006
+ }
11007
+ function emptyCliConfig() {
11008
+ return {
11009
+ version: CONFIG_VERSION,
11010
+ current_environment: null,
11011
+ environments: {}
11012
+ };
11013
+ }
11014
+ function loadCliConfig(configDir) {
11015
+ const path = cliConfigPath(configDir);
11016
+ let contents;
11017
+ try {
11018
+ contents = readFileSync(path, "utf8");
11019
+ } catch (error) {
11020
+ if (error && typeof error === "object" && error.code === "ENOENT") return null;
11021
+ throw cliConfigError(`Could not read Primitive CLI config: ${error instanceof Error ? error.message : String(error)}.`);
11022
+ }
11023
+ try {
11024
+ return parseStoredCliConfig(JSON.parse(contents));
11025
+ } catch (error) {
11026
+ if (error instanceof SyntaxError) throw cliConfigError("Primitive CLI config is not valid JSON.");
11027
+ throw error;
11028
+ }
11029
+ }
11030
+ function saveCliConfig(configDir, config) {
11031
+ mkdirSync(configDir, {
11032
+ mode: 448,
11033
+ recursive: true
11034
+ });
11035
+ const path = cliConfigPath(configDir);
11036
+ const tempPath = join(configDir, `${CONFIG_FILE}.${process.pid}.${randomUUID()}.tmp`);
11037
+ try {
11038
+ writeFileSync(tempPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
11039
+ renameSync(tempPath, path);
11040
+ } catch (error) {
11041
+ rmSync(tempPath, { force: true });
11042
+ throw error;
11043
+ }
11044
+ }
11045
+ function deleteCliConfig(configDir) {
11046
+ rmSync(cliConfigPath(configDir), { force: true });
11047
+ }
11048
+ function resolveConfigEnvironment(config) {
11049
+ if (!config) return null;
11050
+ const current = config.current_environment;
11051
+ if (current) {
11052
+ const environment = config.environments[current];
11053
+ return environment ? {
11054
+ name: current,
11055
+ config: environment
11056
+ } : null;
11057
+ }
11058
+ const defaultEnvironment = config.environments[DEFAULT_ENVIRONMENT];
11059
+ return defaultEnvironment ? {
11060
+ name: DEFAULT_ENVIRONMENT,
11061
+ config: defaultEnvironment
11062
+ } : null;
11063
+ }
11064
+ function upsertCliEnvironment(params) {
11065
+ const name = normalizeCliEnvironmentName(params.environmentName ?? DEFAULT_ENVIRONMENT);
11066
+ const existing = params.config.environments[name] ?? {};
11067
+ const nextHeaders = { ...existing.headers ?? {} };
11068
+ for (const assignment of params.headers ?? []) {
11069
+ const [headerName, value] = parseHeaderAssignment(assignment);
11070
+ nextHeaders[headerName] = value;
11071
+ }
11072
+ for (const rawName of params.unsetHeaders ?? []) delete nextHeaders[validateCliHeaderName(rawName)];
11073
+ const nextEnvironment = {
11074
+ ...existing,
11075
+ ...params.apiBaseUrl1 !== void 0 ? { api_base_url_1: normalizeApiBaseUrl1(params.apiBaseUrl1) } : {},
11076
+ ...params.apiBaseUrl2 !== void 0 ? { api_base_url_2: normalizeApiBaseUrl2(params.apiBaseUrl2) } : {},
11077
+ ...Object.keys(nextHeaders).length > 0 ? { headers: nextHeaders } : {}
11078
+ };
11079
+ if (Object.keys(nextHeaders).length === 0) delete nextEnvironment.headers;
11080
+ return {
11081
+ ...params.config,
11082
+ current_environment: params.use === false ? params.config.current_environment : name,
11083
+ environments: {
11084
+ ...params.config.environments,
11085
+ [name]: nextEnvironment
11086
+ }
11087
+ };
11088
+ }
11089
+ function removeCliEnvironment(config, environmentName) {
11090
+ const name = normalizeCliEnvironmentName(environmentName);
11091
+ const environments = { ...config.environments };
11092
+ delete environments[name];
11093
+ return {
11094
+ ...config,
11095
+ current_environment: config.current_environment === name ? null : config.current_environment,
11096
+ environments
11097
+ };
11098
+ }
11099
+ function redactCliEnvironment(environment) {
11100
+ const headers = environment.headers && Object.keys(environment.headers).length > 0 ? Object.fromEntries(Object.keys(environment.headers).map((name) => [name, "***"])) : void 0;
11101
+ return {
11102
+ ...environment,
11103
+ ...headers ? { headers } : {}
11104
+ };
11105
+ }
11106
+ //#endregion
11107
+ //#region src/oclif/api-client.ts
11108
+ const API_HEADERS_ENV = "PRIMITIVE_API_HEADERS";
11109
+ function mergeHeaders(...headers) {
11110
+ const merged = {};
11111
+ for (const headerSet of headers) {
11112
+ if (!headerSet) continue;
11113
+ for (const [name, value] of Object.entries(headerSet)) merged[name] = value;
11114
+ }
11115
+ return Object.keys(merged).length > 0 ? merged : void 0;
11116
+ }
11117
+ function parseHeadersJson(raw) {
11118
+ let parsed;
11119
+ try {
11120
+ parsed = JSON.parse(raw);
11121
+ } catch (error) {
11122
+ const detail = error instanceof Error ? error.message : String(error);
11123
+ throw new Errors.CLIError(`${API_HEADERS_ENV} must be valid JSON object syntax: ${detail}`, { exit: 1 });
11124
+ }
11125
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Errors.CLIError(`${API_HEADERS_ENV} must be a JSON object.`, { exit: 1 });
11126
+ const headers = {};
11127
+ for (const [rawName, rawValue] of Object.entries(parsed)) {
11128
+ const name = validateCliHeaderName(rawName);
11129
+ if (typeof rawValue !== "string") throw new Errors.CLIError(`${API_HEADERS_ENV}.${name} must be a string.`, { exit: 1 });
11130
+ headers[name] = validateCliHeaderValue(rawValue, name);
11131
+ }
11132
+ return headers;
11133
+ }
11134
+ function cliApiHeadersFromEnv(env = process.env) {
11135
+ const rawGenericHeaders = env[API_HEADERS_ENV]?.trim();
11136
+ return rawGenericHeaders ? parseHeadersJson(rawGenericHeaders) : void 0;
11137
+ }
11138
+ function resolveCliApiRequestConfig(params) {
11139
+ const currentEnvironment = resolveConfigEnvironment(loadCliConfig(params.configDir));
11140
+ const configuredApiBaseUrl1 = currentEnvironment?.config.api_base_url_1;
11141
+ const configuredApiBaseUrl2 = currentEnvironment?.config.api_base_url_2;
11142
+ const apiBaseUrl1 = params.apiBaseUrl1 !== void 0 ? normalizeApiBaseUrl1(params.apiBaseUrl1) : configuredApiBaseUrl1;
11143
+ const apiBaseUrl2 = params.apiBaseUrl2 !== void 0 ? normalizeApiBaseUrl2(params.apiBaseUrl2) : configuredApiBaseUrl2;
11144
+ return {
11145
+ apiBaseUrl1,
11146
+ apiBaseUrl2,
11147
+ baseUrlOverridden: apiBaseUrl1 !== void 0 || apiBaseUrl2 !== void 0,
11148
+ environmentName: currentEnvironment?.name ?? null,
11149
+ headers: mergeHeaders(currentEnvironment?.config.headers, cliApiHeadersFromEnv(params.env)),
11150
+ resolvedApiBaseUrl1: normalizeApiBaseUrl1(apiBaseUrl1),
11151
+ resolvedApiBaseUrl2: normalizeApiBaseUrl2(apiBaseUrl2)
11152
+ };
11153
+ }
11154
+ function createCliApiClient(params) {
11155
+ const requestConfig = resolveCliApiRequestConfig(params);
11156
+ return {
11157
+ apiClient: new PrimitiveApiClient({
11158
+ apiKey: params.apiKey,
11159
+ apiBaseUrl1: requestConfig.resolvedApiBaseUrl1,
11160
+ apiBaseUrl2: requestConfig.resolvedApiBaseUrl2,
11161
+ headers: requestConfig.headers
11162
+ }),
11163
+ requestConfig
11164
+ };
11165
+ }
11166
+ function createAuthenticatedCliApiClient(params) {
11167
+ const requestConfig = resolveCliApiRequestConfig(params);
11168
+ const auth = resolveCliAuth({
11169
+ apiKey: params.apiKey,
11170
+ apiBaseUrl1: requestConfig.apiBaseUrl1,
11171
+ apiBaseUrl2: requestConfig.apiBaseUrl2,
11172
+ configDir: params.configDir
11173
+ });
11174
+ return {
11175
+ apiClient: new PrimitiveApiClient({
11176
+ apiKey: auth.apiKey,
11177
+ apiBaseUrl1: auth.apiBaseUrl1,
11178
+ apiBaseUrl2: auth.apiBaseUrl2,
11179
+ headers: requestConfig.headers
11180
+ }),
11181
+ auth,
11182
+ baseUrlOverridden: requestConfig.baseUrlOverridden,
11183
+ requestConfig
11184
+ };
11185
+ }
11186
+ //#endregion
10923
11187
  //#region src/oclif/endpoints-test-redirect.ts
10924
11188
  async function detectFunctionEndpoint(endpointId, listEndpoints) {
10925
11189
  let response;
@@ -11243,7 +11507,7 @@ function writeErrorWithHints(payload) {
11243
11507
  function removeStaleSavedCredentialOnUnauthorized(params) {
11244
11508
  if (extractErrorCode(params.payload) !== API_ERROR_CODES.unauthorized || params.auth.source !== "stored") return false;
11245
11509
  if (params.baseUrlOverridden && params.auth.credentials !== null && params.auth.apiBaseUrl1 !== params.auth.credentials.api_base_url_1) {
11246
- process.stderr.write("Saved Primitive CLI credentials were rejected by the overridden API base URL. The local credential was not removed; unset PRIMITIVE_API_BASE_URL_1, or run `primitive logout` to remove the stored credential.\n");
11510
+ process.stderr.write("Saved Primitive CLI credentials were rejected by the overridden API base URL. The local credential was not removed; unset PRIMITIVE_API_BASE_URL_1, run `primitive config reset` to clear configured URL overrides, or run `primitive logout` to remove the stored credential.\n");
11247
11511
  return false;
11248
11512
  }
11249
11513
  deleteCliCredentials(params.configDir);
@@ -11268,9 +11532,6 @@ async function runWithTiming(enabled, fn) {
11268
11532
  const TIME_FLAG_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.";
11269
11533
  const API_BASE_URL_1_FLAG_DESCRIPTION = "Override the primary API base URL. Internal testing only; not documented to customers.";
11270
11534
  const API_BASE_URL_2_FLAG_DESCRIPTION = "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.";
11271
- function baseUrlOverriddenFromFlags(flags) {
11272
- return typeof flags["api-base-url-1"] === "string" || typeof flags["api-base-url-2"] === "string";
11273
- }
11274
11535
  const HOST_2_OPERATIONS = new Set(["sendEmail"]);
11275
11536
  const RESERVED_FLAG_NAMES = new Set([
11276
11537
  "api-key",
@@ -11379,18 +11640,12 @@ function createOperationCommand(operation) {
11379
11640
  const { flags } = await this.parse(OperationCommand);
11380
11641
  const parsedFlags = flags;
11381
11642
  await runWithTiming(parsedFlags.time === true, async () => {
11382
- const baseUrlOverridden = typeof parsedFlags["api-base-url-1"] === "string" || typeof parsedFlags["api-base-url-2"] === "string";
11383
- const auth = resolveCliAuth({
11643
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
11384
11644
  apiKey: typeof parsedFlags["api-key"] === "string" ? parsedFlags["api-key"] : void 0,
11385
11645
  apiBaseUrl1: typeof parsedFlags["api-base-url-1"] === "string" ? parsedFlags["api-base-url-1"] : void 0,
11386
11646
  apiBaseUrl2: typeof parsedFlags["api-base-url-2"] === "string" ? parsedFlags["api-base-url-2"] : void 0,
11387
11647
  configDir: this.config.configDir
11388
11648
  });
11389
- const apiClient = new PrimitiveApiClient({
11390
- apiKey: auth.apiKey,
11391
- apiBaseUrl1: auth.apiBaseUrl1,
11392
- apiBaseUrl2: auth.apiBaseUrl2
11393
- });
11394
11649
  let body;
11395
11650
  if (operation.hasJsonBody) {
11396
11651
  const explicit = readJsonBody(parsedFlags);
@@ -11482,6 +11737,128 @@ function canonicalizeCliReferences(description) {
11482
11737
  return description.replaceAll("`primitive emails:latest`", "`primitive emails latest`").replaceAll("`primitive describe emails:get-email | jq '.responseSchema.properties'`", "`primitive describe emails:get | jq '.responseSchema.properties'`");
11483
11738
  }
11484
11739
  //#endregion
11740
+ //#region src/oclif/commands/config.ts
11741
+ function loadOrCreateConfig(configDir) {
11742
+ return loadCliConfig(configDir) ?? emptyCliConfig();
11743
+ }
11744
+ function redactConfig(config) {
11745
+ return {
11746
+ ...config,
11747
+ environments: Object.fromEntries(Object.entries(config.environments).map(([name, environment]) => [name, redactCliEnvironment(environment)]))
11748
+ };
11749
+ }
11750
+ var ConfigSetCommand = class ConfigSetCommand extends Command {
11751
+ static hidden = true;
11752
+ static summary = "Set hidden Primitive CLI request config";
11753
+ static flags = {
11754
+ environment: Flags.string({
11755
+ char: "e",
11756
+ description: "Environment name to create or update"
11757
+ }),
11758
+ "api-base-url-1": Flags.string({ description: "Primary API base URL" }),
11759
+ "api-base-url-2": Flags.string({ description: "Attachments-supporting API base URL" }),
11760
+ header: Flags.string({
11761
+ description: "Request header in name=value form. Repeatable.",
11762
+ multiple: true
11763
+ }),
11764
+ "unset-header": Flags.string({
11765
+ description: "Request header name to remove. Repeatable.",
11766
+ multiple: true
11767
+ })
11768
+ };
11769
+ async run() {
11770
+ const { flags } = await this.parse(ConfigSetCommand);
11771
+ const headers = flags.header ?? [];
11772
+ if (flags["api-base-url-1"] === void 0 && flags["api-base-url-2"] === void 0 && headers.length === 0 && (flags["unset-header"] ?? []).length === 0) throw new Errors.CLIError("Nothing to set. Pass an API base URL, --header, or --unset-header.", { exit: 1 });
11773
+ const config = upsertCliEnvironment({
11774
+ apiBaseUrl1: flags["api-base-url-1"],
11775
+ apiBaseUrl2: flags["api-base-url-2"],
11776
+ config: loadOrCreateConfig(this.config.configDir),
11777
+ environmentName: flags.environment,
11778
+ headers,
11779
+ unsetHeaders: flags["unset-header"]
11780
+ });
11781
+ saveCliConfig(this.config.configDir, config);
11782
+ process.stderr.write(`Primitive CLI environment ${config.current_environment} is active.\n`);
11783
+ }
11784
+ };
11785
+ var ConfigUseCommand = class ConfigUseCommand extends Command {
11786
+ static hidden = true;
11787
+ static summary = "Switch active Primitive CLI request config";
11788
+ static args = { environment: Args.string({
11789
+ description: "Environment name to use",
11790
+ required: true
11791
+ }) };
11792
+ async run() {
11793
+ const { args } = await this.parse(ConfigUseCommand);
11794
+ const environment = normalizeCliEnvironmentName(args.environment);
11795
+ const config = loadOrCreateConfig(this.config.configDir);
11796
+ if (!config.environments[environment]) throw new Errors.CLIError(`Primitive CLI environment ${environment} is not configured.`, { exit: 1 });
11797
+ saveCliConfig(this.config.configDir, {
11798
+ ...config,
11799
+ current_environment: environment
11800
+ });
11801
+ process.stderr.write(`Primitive CLI environment ${environment} is active.\n`);
11802
+ }
11803
+ };
11804
+ var ConfigListCommand = class ConfigListCommand extends Command {
11805
+ static hidden = true;
11806
+ static summary = "List hidden Primitive CLI request configs";
11807
+ static flags = {
11808
+ json: Flags.boolean({ description: "Print JSON" }),
11809
+ "show-secrets": Flags.boolean({ description: "Show header values instead of redacting them" })
11810
+ };
11811
+ async run() {
11812
+ const { flags } = await this.parse(ConfigListCommand);
11813
+ const config = loadOrCreateConfig(this.config.configDir);
11814
+ const output = flags["show-secrets"] ? config : redactConfig(config);
11815
+ if (flags.json) {
11816
+ this.log(JSON.stringify(output, null, 2));
11817
+ return;
11818
+ }
11819
+ const entries = Object.entries(config.environments);
11820
+ if (entries.length === 0) {
11821
+ this.log("No Primitive CLI environments configured.");
11822
+ return;
11823
+ }
11824
+ const activeEnvironment = resolveConfigEnvironment(config)?.name ?? null;
11825
+ for (const [name, environment] of entries) {
11826
+ const active = activeEnvironment === name ? "*" : " ";
11827
+ const headerNames = Object.keys(environment.headers ?? {});
11828
+ this.log(`${active} ${name}`);
11829
+ if (environment.api_base_url_1) this.log(` api_base_url_1: ${environment.api_base_url_1}`);
11830
+ if (environment.api_base_url_2) this.log(` api_base_url_2: ${environment.api_base_url_2}`);
11831
+ this.log(` headers: ${headerNames.length > 0 ? headerNames.join(", ") : "(none)"}`);
11832
+ }
11833
+ }
11834
+ };
11835
+ var ConfigResetCommand = class ConfigResetCommand extends Command {
11836
+ static hidden = true;
11837
+ static summary = "Reset hidden Primitive CLI request config";
11838
+ static flags = { environment: Flags.string({
11839
+ char: "e",
11840
+ description: "Only remove one environment"
11841
+ }) };
11842
+ async run() {
11843
+ const { flags } = await this.parse(ConfigResetCommand);
11844
+ if (flags.environment === void 0) {
11845
+ deleteCliConfig(this.config.configDir);
11846
+ process.stderr.write("Primitive CLI request config reset.\n");
11847
+ return;
11848
+ }
11849
+ const environment = normalizeCliEnvironmentName(flags.environment);
11850
+ const config = loadCliConfig(this.config.configDir);
11851
+ if (!config?.environments[environment]) {
11852
+ process.stderr.write(`Primitive CLI environment ${environment} was not configured.\n`);
11853
+ return;
11854
+ }
11855
+ const nextConfig = removeCliEnvironment(config, environment);
11856
+ if (Object.keys(nextConfig.environments).length === 0) deleteCliConfig(this.config.configDir);
11857
+ else saveCliConfig(this.config.configDir, nextConfig);
11858
+ process.stderr.write(`Primitive CLI environment ${environment} removed.\n`);
11859
+ }
11860
+ };
11861
+ //#endregion
11485
11862
  //#region src/oclif/commands/doctor.ts
11486
11863
  const MIN_NODE_MAJOR = 22;
11487
11864
  function renderRow({ label, outcome }) {
@@ -11580,14 +11957,10 @@ function checkApiKey(opts) {
11580
11957
  hint: "Run `primitive login`, pass --api-key explicitly, or export PRIMITIVE_API_KEY=prim_..."
11581
11958
  };
11582
11959
  }
11583
- async function checkAccount(opts) {
11960
+ async function checkAccount(client) {
11584
11961
  try {
11585
11962
  const result = await getAccount({
11586
- client: new PrimitiveApiClient({
11587
- apiKey: opts.apiKey,
11588
- apiBaseUrl1: opts.apiBaseUrl1,
11589
- apiBaseUrl2: opts.apiBaseUrl2
11590
- }).client,
11963
+ client: client.client,
11591
11964
  responseStyle: "fields"
11592
11965
  });
11593
11966
  const apiError = result.error;
@@ -11628,14 +12001,10 @@ async function checkAccount(opts) {
11628
12001
  };
11629
12002
  }
11630
12003
  }
11631
- async function checkDomains(opts) {
12004
+ async function checkDomains(client) {
11632
12005
  try {
11633
12006
  const result = await listDomains({
11634
- client: new PrimitiveApiClient({
11635
- apiKey: opts.apiKey,
11636
- apiBaseUrl1: opts.apiBaseUrl1,
11637
- apiBaseUrl2: opts.apiBaseUrl2
11638
- }).client,
12007
+ client: client.client,
11639
12008
  responseStyle: "fields"
11640
12009
  });
11641
12010
  if (result.error) return {
@@ -11706,28 +12075,20 @@ var DoctorCommand = class DoctorCommand extends Command {
11706
12075
  outcome: apiKeyCheck
11707
12076
  });
11708
12077
  if (apiKeyCheck.status !== "fail") {
11709
- const auth = resolveCliAuth({
12078
+ const { apiClient, auth } = createAuthenticatedCliApiClient({
11710
12079
  apiKey: flags["api-key"],
11711
12080
  apiBaseUrl1: flags["api-base-url-1"],
11712
12081
  apiBaseUrl2: flags["api-base-url-2"],
11713
12082
  configDir: this.config.configDir
11714
12083
  });
11715
12084
  if (auth.apiKey !== void 0) {
11716
- const accountCheck = await checkAccount({
11717
- apiKey: auth.apiKey,
11718
- apiBaseUrl1: auth.apiBaseUrl1,
11719
- apiBaseUrl2: auth.apiBaseUrl2
11720
- });
12085
+ const accountCheck = await checkAccount(apiClient);
11721
12086
  rows.push({
11722
12087
  label: "API auth",
11723
12088
  outcome: accountCheck.outcome
11724
12089
  });
11725
12090
  if (accountCheck.outcome.status === "ok") {
11726
- const domainsOutcome = await checkDomains({
11727
- apiKey: auth.apiKey,
11728
- apiBaseUrl1: auth.apiBaseUrl1,
11729
- apiBaseUrl2: auth.apiBaseUrl2
11730
- });
12091
+ const domainsOutcome = await checkDomains(apiClient);
11731
12092
  rows.push({
11732
12093
  label: "Domains",
11733
12094
  outcome: domainsOutcome
@@ -11821,19 +12182,14 @@ var EmailsLatestCommand = class EmailsLatestCommand extends Command {
11821
12182
  async run() {
11822
12183
  const { flags } = await this.parse(EmailsLatestCommand);
11823
12184
  await runWithTiming(flags.time, async () => {
11824
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
11825
- const auth = resolveCliAuth({
12185
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
11826
12186
  apiKey: flags["api-key"],
11827
12187
  apiBaseUrl1: flags["api-base-url-1"],
11828
12188
  apiBaseUrl2: flags["api-base-url-2"],
11829
12189
  configDir: this.config.configDir
11830
12190
  });
11831
12191
  const result = await listEmails({
11832
- client: new PrimitiveApiClient({
11833
- apiKey: auth.apiKey,
11834
- apiBaseUrl1: auth.apiBaseUrl1,
11835
- apiBaseUrl2: auth.apiBaseUrl2
11836
- }).client,
12192
+ client: apiClient.client,
11837
12193
  query: { limit: flags.limit },
11838
12194
  responseStyle: "fields"
11839
12195
  });
@@ -12030,18 +12386,12 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
12030
12386
  };
12031
12387
  async run() {
12032
12388
  const { flags } = await this.parse(EmailsWaitCommand);
12033
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
12034
- const auth = resolveCliAuth({
12389
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
12035
12390
  apiKey: flags["api-key"],
12036
12391
  apiBaseUrl1: flags["api-base-url-1"],
12037
12392
  apiBaseUrl2: flags["api-base-url-2"],
12038
12393
  configDir: this.config.configDir
12039
12394
  });
12040
- const apiClient = new PrimitiveApiClient({
12041
- apiKey: auth.apiKey,
12042
- apiBaseUrl1: auth.apiBaseUrl1,
12043
- apiBaseUrl2: auth.apiBaseUrl2
12044
- });
12045
12395
  let since;
12046
12396
  try {
12047
12397
  since = sinceFromFlags(flags);
@@ -12158,18 +12508,12 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
12158
12508
  };
12159
12509
  async run() {
12160
12510
  const { flags } = await this.parse(EmailsWatchCommand);
12161
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
12162
- const auth = resolveCliAuth({
12511
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
12163
12512
  apiKey: flags["api-key"],
12164
12513
  apiBaseUrl1: flags["api-base-url-1"],
12165
12514
  apiBaseUrl2: flags["api-base-url-2"],
12166
12515
  configDir: this.config.configDir
12167
12516
  });
12168
- const apiClient = new PrimitiveApiClient({
12169
- apiKey: auth.apiKey,
12170
- apiBaseUrl1: auth.apiBaseUrl1,
12171
- apiBaseUrl2: auth.apiBaseUrl2
12172
- });
12173
12517
  let since;
12174
12518
  try {
12175
12519
  since = sinceFromFlags(flags);
@@ -12860,18 +13204,12 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
12860
13204
  const code = readTextFileFlag(flags.file, "--file");
12861
13205
  const sourceMap = flags["source-map-file"] ? readTextFileFlag(flags["source-map-file"], "--source-map-file") : void 0;
12862
13206
  emitRawSendMailFetchWarning(code, (chunk) => process.stderr.write(chunk));
12863
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
12864
- const auth = resolveCliAuth({
13207
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
12865
13208
  apiKey: flags["api-key"],
12866
13209
  apiBaseUrl1: flags["api-base-url-1"],
12867
13210
  apiBaseUrl2: flags["api-base-url-2"],
12868
13211
  configDir: this.config.configDir
12869
13212
  });
12870
- const apiClient = new PrimitiveApiClient({
12871
- apiKey: auth.apiKey,
12872
- apiBaseUrl1: auth.apiBaseUrl1,
12873
- apiBaseUrl2: auth.apiBaseUrl2
12874
- });
12875
13213
  const authFailureContext = {
12876
13214
  auth,
12877
13215
  baseUrlOverridden,
@@ -12978,8 +13316,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
12978
13316
  name: "Primitive Team",
12979
13317
  url: "https://primitive.dev"
12980
13318
  };
12981
- const SDK_VERSION_RANGE = "^0.30.0";
12982
- const CLI_VERSION_RANGE = "^0.30.0";
13319
+ const SDK_VERSION_RANGE = "^0.30.1";
13320
+ const CLI_VERSION_RANGE = "^0.30.1";
12983
13321
  const ESBUILD_VERSION_RANGE = "^0.27.0";
12984
13322
  function renderHandler() {
12985
13323
  return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
@@ -13001,14 +13339,14 @@ import {
13001
13339
  // the loop guard recognizes mail returning to it as self-traffic.
13002
13340
  const REPLY_FROM = "you@your-domain.primitive.email";
13003
13341
 
13004
- // Loop protection. A deployed Function receives catch-all inbound for
13005
- // the managed *.primitive.email subdomain, which includes bounces and
13006
- // auto-replies generated by its own outbound traffic. Without this
13007
- // guard the handler can respond to its own bounces and create a
13008
- // fan-out loop.
13342
+ // Loop protection. A newly deployed Function starts as a fallback
13343
+ // endpoint for managed *.primitive.email domains that do not have a
13344
+ // domain-scoped endpoint. That can include bounces and auto-replies
13345
+ // generated by the handler's own outbound traffic. Without this guard
13346
+ // the handler can respond to its own bounces and create a fan-out loop.
13009
13347
  //
13010
13348
  // The default check returns true when From is on any *.primitive.email
13011
- // address (covers the managed subdomain catch-all, the simple
13349
+ // address (covers managed-domain fallback mail, the simple
13012
13350
  // self-reply case, and bounces from mailer-daemon@*.primitive.email)
13013
13351
  // or when From contains REPLY_FROM as a case-insensitive substring.
13014
13352
  // Substring matching is deliberate so display-name forms like
@@ -13462,18 +13800,12 @@ var FunctionsLogsCommand = class FunctionsLogsCommand extends Command {
13462
13800
  if (flags["poll-interval"] <= 0) this.error("--poll-interval must be greater than 0.", { exit: 2 });
13463
13801
  if (flags.follow && flags.cursor) this.error("--cursor cannot be combined with --follow.", { exit: 2 });
13464
13802
  await runWithTiming(flags.time, async () => {
13465
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
13466
- const auth = resolveCliAuth({
13803
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
13467
13804
  apiKey: flags["api-key"],
13468
13805
  apiBaseUrl1: flags["api-base-url-1"],
13469
13806
  apiBaseUrl2: flags["api-base-url-2"],
13470
13807
  configDir: this.config.configDir
13471
13808
  });
13472
- const apiClient = new PrimitiveApiClient({
13473
- apiKey: auth.apiKey,
13474
- apiBaseUrl1: auth.apiBaseUrl1,
13475
- apiBaseUrl2: auth.apiBaseUrl2
13476
- });
13477
13809
  const seenIds = /* @__PURE__ */ new Set();
13478
13810
  let completedInitialFollowPoll = false;
13479
13811
  let hasObservedLogs = false;
@@ -13704,18 +14036,12 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
13704
14036
  const code = readTextFileFlag(flags.file, "--file");
13705
14037
  const sourceMap = flags["source-map-file"] ? readTextFileFlag(flags["source-map-file"], "--source-map-file") : void 0;
13706
14038
  emitRawSendMailFetchWarning(code, (chunk) => process.stderr.write(chunk));
13707
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
13708
- const auth = resolveCliAuth({
14039
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
13709
14040
  apiKey: flags["api-key"],
13710
14041
  apiBaseUrl1: flags["api-base-url-1"],
13711
14042
  apiBaseUrl2: flags["api-base-url-2"],
13712
14043
  configDir: this.config.configDir
13713
14044
  });
13714
- const apiClient = new PrimitiveApiClient({
13715
- apiKey: auth.apiKey,
13716
- apiBaseUrl1: auth.apiBaseUrl1,
13717
- apiBaseUrl2: auth.apiBaseUrl2
13718
- });
13719
14045
  const authFailureContext = {
13720
14046
  auth,
13721
14047
  baseUrlOverridden,
@@ -13925,18 +14251,12 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
13925
14251
  async run() {
13926
14252
  const { flags } = await this.parse(FunctionsSetSecretCommand);
13927
14253
  await runWithTiming(flags.time, async () => {
13928
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
13929
- const auth = resolveCliAuth({
14254
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
13930
14255
  apiKey: flags["api-key"],
13931
14256
  apiBaseUrl1: flags["api-base-url-1"],
13932
14257
  apiBaseUrl2: flags["api-base-url-2"],
13933
14258
  configDir: this.config.configDir
13934
14259
  });
13935
- const apiClient = new PrimitiveApiClient({
13936
- apiKey: auth.apiKey,
13937
- apiBaseUrl1: auth.apiBaseUrl1,
13938
- apiBaseUrl2: auth.apiBaseUrl2
13939
- });
13940
14260
  const authFailureContext = {
13941
14261
  auth,
13942
14262
  baseUrlOverridden,
@@ -14053,7 +14373,8 @@ function stringOrNull(value) {
14053
14373
  return typeof value === "string" && value.length > 0 ? value : null;
14054
14374
  }
14055
14375
  function findMatchingFunctionEndpoints(params) {
14056
- const matches = [];
14376
+ const domainMatches = [];
14377
+ const fallbackMatches = [];
14057
14378
  for (const endpoint of params.endpoints) {
14058
14379
  if (endpoint.kind !== "function") continue;
14059
14380
  if (endpoint.enabled === false) continue;
@@ -14063,20 +14384,22 @@ function findMatchingFunctionEndpoints(params) {
14063
14384
  const domainId = stringOrNull(endpoint.domain_id);
14064
14385
  if (domainId !== null && (params.inboundDomainId === null || domainId !== params.inboundDomainId)) continue;
14065
14386
  const functionId = stringOrNull(endpoint.function_id);
14066
- matches.push({
14387
+ const match = {
14067
14388
  function_id: functionId,
14068
14389
  id,
14069
14390
  is_current_function: functionId === params.currentFunctionId,
14070
- scope: domainId === null ? "catch-all" : "domain"
14071
- });
14391
+ scope: domainId === null ? "fallback" : "domain"
14392
+ };
14393
+ if (domainId === null) fallbackMatches.push(match);
14394
+ else domainMatches.push(match);
14072
14395
  }
14073
- return matches;
14396
+ return domainMatches.length > 0 ? domainMatches : fallbackMatches;
14074
14397
  }
14075
14398
  function formatFunctionEndpointNoiseWarning(params) {
14076
14399
  if (params.endpoints.filter((endpoint) => !endpoint.is_current_function).length === 0) return null;
14077
14400
  const lines = [`Warning: ${params.endpoints.length} function endpoints may receive mail for ${params.toAddress}:`];
14078
14401
  for (const endpoint of params.endpoints) {
14079
- const scope = endpoint.scope === "catch-all" ? "catch-all" : `scoped to ${params.inboundDomain}`;
14402
+ const scope = endpoint.scope === "fallback" ? "fallback" : `scoped to ${params.inboundDomain}`;
14080
14403
  const current = endpoint.is_current_function ? " (this function)" : "";
14081
14404
  const target = endpoint.function_id ? ` -> function ${endpoint.function_id}` : "";
14082
14405
  lines.push(`- endpoint ${endpoint.id}${target}, ${scope}${current}`);
@@ -14155,18 +14478,12 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
14155
14478
  const { flags } = await this.parse(FunctionsTestFunctionCommand);
14156
14479
  const shouldWait = flags.wait || flags["show-sends"];
14157
14480
  const shouldShowSends = flags["show-sends"];
14158
- const baseUrlOverridden = baseUrlOverriddenFromFlags(flags);
14159
- const auth = resolveCliAuth({
14481
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
14160
14482
  apiKey: flags["api-key"],
14161
14483
  apiBaseUrl1: flags["api-base-url-1"],
14162
14484
  apiBaseUrl2: flags["api-base-url-2"],
14163
14485
  configDir: this.config.configDir
14164
14486
  });
14165
- const apiClient = new PrimitiveApiClient({
14166
- apiKey: auth.apiKey,
14167
- apiBaseUrl1: auth.apiBaseUrl1,
14168
- apiBaseUrl2: auth.apiBaseUrl2
14169
- });
14170
14487
  await runWithTiming(flags.time, async () => {
14171
14488
  const triggerResult = await testFunction({
14172
14489
  client: apiClient.client,
@@ -14306,11 +14623,16 @@ function retryAfterSeconds$1(result) {
14306
14623
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
14307
14624
  }
14308
14625
  async function checkExistingLogin(params) {
14309
- const baseUrlOverridden = params.apiBaseUrl1 !== void 0;
14310
- const probeApiBaseUrl1 = baseUrlOverridden ? normalizeApiBaseUrl1(params.apiBaseUrl1) : params.credentials.api_base_url_1;
14626
+ const requestConfig = resolveCliApiRequestConfig({
14627
+ apiBaseUrl1: params.apiBaseUrl1,
14628
+ configDir: params.configDir
14629
+ });
14630
+ const probeApiBaseUrl1 = requestConfig.apiBaseUrl1 ?? params.credentials.api_base_url_1;
14311
14631
  const apiClient = new PrimitiveApiClient({
14312
14632
  apiKey: params.credentials.api_key,
14313
- apiBaseUrl1: probeApiBaseUrl1
14633
+ apiBaseUrl1: probeApiBaseUrl1,
14634
+ apiBaseUrl2: requestConfig.resolvedApiBaseUrl2,
14635
+ headers: requestConfig.headers
14314
14636
  });
14315
14637
  const result = await (params.checkAccount ?? ((client) => getAccount({
14316
14638
  client: client.client,
@@ -14326,7 +14648,7 @@ async function checkExistingLogin(params) {
14326
14648
  credentials: params.credentials,
14327
14649
  source: "stored"
14328
14650
  },
14329
- baseUrlOverridden,
14651
+ baseUrlOverridden: requestConfig.baseUrlOverridden,
14330
14652
  configDir: params.configDir,
14331
14653
  payload
14332
14654
  })) return { status: "removed_stale" };
@@ -14372,7 +14694,11 @@ var LoginCommand = class LoginCommand extends Command {
14372
14694
  }
14373
14695
  }
14374
14696
  async runWithCredentialLock(flags) {
14375
- const apiBaseUrl1 = normalizeApiBaseUrl1(flags["api-base-url-1"]);
14697
+ const { apiClient, requestConfig } = createCliApiClient({
14698
+ apiBaseUrl1: flags["api-base-url-1"],
14699
+ configDir: this.config.configDir
14700
+ });
14701
+ const apiBaseUrl1 = requestConfig.resolvedApiBaseUrl1;
14376
14702
  let existing;
14377
14703
  try {
14378
14704
  existing = loadCliCredentials(this.config.configDir);
@@ -14395,7 +14721,6 @@ var LoginCommand = class LoginCommand extends Command {
14395
14721
  throw cliError$2(existingStatus.message);
14396
14722
  } else throw cliError$2(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before logging in again.`);
14397
14723
  }
14398
- const apiClient = new PrimitiveApiClient({ apiBaseUrl1 });
14399
14724
  const started = await startCliLogin({
14400
14725
  body: { device_name: flags["device-name"] ?? hostname() },
14401
14726
  client: apiClient.client,
@@ -14505,10 +14830,15 @@ var LogoutCommand = class LogoutCommand extends Command {
14505
14830
  return;
14506
14831
  }
14507
14832
  if (!credentials) throw cliError$1("Not logged in. Run `primitive login` to create saved CLI credentials.");
14508
- const apiBaseUrl1 = flags["api-base-url-1"] ? normalizeApiBaseUrl1(flags["api-base-url-1"]) : credentials.api_base_url_1;
14833
+ const requestConfig = resolveCliApiRequestConfig({
14834
+ apiBaseUrl1: flags["api-base-url-1"],
14835
+ configDir: this.config.configDir
14836
+ });
14837
+ const apiBaseUrl1 = requestConfig.apiBaseUrl1 ? normalizeApiBaseUrl1(requestConfig.apiBaseUrl1) : credentials.api_base_url_1;
14509
14838
  const apiClient = new PrimitiveApiClient({
14510
14839
  apiKey: credentials.api_key,
14511
- apiBaseUrl1
14840
+ apiBaseUrl1,
14841
+ headers: requestConfig.headers
14512
14842
  });
14513
14843
  const result = await cliLogout({
14514
14844
  body: { key_id: credentials.key_id },
@@ -14689,18 +15019,12 @@ var ReplyCommand = class ReplyCommand extends Command {
14689
15019
  });
14690
15020
  if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
14691
15021
  await runWithTiming(flags.time, async () => {
14692
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
14693
- const auth = resolveCliAuth({
15022
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
14694
15023
  apiKey: flags["api-key"],
14695
15024
  apiBaseUrl1: flags["api-base-url-1"],
14696
15025
  apiBaseUrl2: flags["api-base-url-2"],
14697
15026
  configDir: this.config.configDir
14698
15027
  });
14699
- const apiClient = new PrimitiveApiClient({
14700
- apiKey: auth.apiKey,
14701
- apiBaseUrl1: auth.apiBaseUrl1,
14702
- apiBaseUrl2: auth.apiBaseUrl2
14703
- });
14704
15028
  const result = await replyToEmail({
14705
15029
  body: {
14706
15030
  ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
@@ -14829,18 +15153,12 @@ var SendCommand = class SendCommand extends Command {
14829
15153
  });
14830
15154
  if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
14831
15155
  await runWithTiming(flags.time, async () => {
14832
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
14833
- const auth = resolveCliAuth({
15156
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
14834
15157
  apiKey: flags["api-key"],
14835
15158
  apiBaseUrl1: flags["api-base-url-1"],
14836
15159
  apiBaseUrl2: flags["api-base-url-2"],
14837
15160
  configDir: this.config.configDir
14838
15161
  });
14839
- const apiClient = new PrimitiveApiClient({
14840
- apiKey: auth.apiKey,
14841
- apiBaseUrl1: auth.apiBaseUrl1,
14842
- apiBaseUrl2: auth.apiBaseUrl2
14843
- });
14844
15162
  const authFailureContext = {
14845
15163
  auth,
14846
15164
  baseUrlOverridden,
@@ -15113,7 +15431,11 @@ async function runSignupWithCredentialLock(params) {
15113
15431
  const startFn = deps.startCliSignup ?? startCliSignup;
15114
15432
  const verifyFn = deps.verifyCliSignup ?? verifyCliSignup;
15115
15433
  const checkExistingLoginFn = deps.checkExistingLogin ?? checkExistingLogin;
15116
- const apiBaseUrl1 = normalizeApiBaseUrl1(flags["api-base-url-1"]);
15434
+ const { apiClient, requestConfig } = createCliApiClient({
15435
+ apiBaseUrl1: flags["api-base-url-1"],
15436
+ configDir
15437
+ });
15438
+ const apiBaseUrl1 = requestConfig.resolvedApiBaseUrl1;
15117
15439
  let existing;
15118
15440
  try {
15119
15441
  existing = loadCliCredentials(configDir);
@@ -15137,7 +15459,6 @@ async function runSignupWithCredentialLock(params) {
15137
15459
  } else throw cliError(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before creating a new account.`);
15138
15460
  }
15139
15461
  if (flags.force) deletePendingCliSignup(configDir);
15140
- const apiClient = new PrimitiveApiClient({ apiBaseUrl1 });
15141
15462
  let start = flags.force ? null : loadPendingCliSignup(configDir, apiBaseUrl1);
15142
15463
  const resumed = Boolean(start);
15143
15464
  if (start) process$1.stderr.write(`Continuing pending Primitive CLI signup for ${start.email}.\n`);
@@ -15294,19 +15615,14 @@ var WhoamiCommand = class WhoamiCommand extends Command {
15294
15615
  async run() {
15295
15616
  const { flags } = await this.parse(WhoamiCommand);
15296
15617
  await runWithTiming(flags.time, async () => {
15297
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
15298
- const auth = resolveCliAuth({
15618
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
15299
15619
  apiKey: flags["api-key"],
15300
15620
  apiBaseUrl1: flags["api-base-url-1"],
15301
15621
  apiBaseUrl2: flags["api-base-url-2"],
15302
15622
  configDir: this.config.configDir
15303
15623
  });
15304
15624
  const result = await getAccount({
15305
- client: new PrimitiveApiClient({
15306
- apiKey: auth.apiKey,
15307
- apiBaseUrl1: auth.apiBaseUrl1,
15308
- apiBaseUrl2: auth.apiBaseUrl2
15309
- }).client,
15625
+ client: apiClient.client,
15310
15626
  responseStyle: "fields"
15311
15627
  });
15312
15628
  if (result.error) {
@@ -15535,6 +15851,10 @@ const generatedCommands = Object.fromEntries(operationManifest.filter((operation
15535
15851
  const COMMANDS = {
15536
15852
  completion: CompletionCommand,
15537
15853
  "list-operations": ListOperationsCommand,
15854
+ "config:list": ConfigListCommand,
15855
+ "config:reset": ConfigResetCommand,
15856
+ "config:set": ConfigSetCommand,
15857
+ "config:use": ConfigUseCommand,
15538
15858
  describe: DescribeCommand,
15539
15859
  send: SendCommand,
15540
15860
  reply: ReplyCommand,
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  //#region src/oclif/proxy-auto-detect.ts
2
3
  const PROXY_ENV_VARS = [
3
4
  "HTTP_PROXY",
@@ -15,7 +16,7 @@ function detectProxyVars(env) {
15
16
  return typeof value === "string" && value.length > 0;
16
17
  });
17
18
  }
18
- function applyProxyAutoDetect(options = {}) {
19
+ function restartWithProxyEnvIfNeeded(options = {}) {
19
20
  const env = options.env ?? process.env;
20
21
  const stderr = options.stderr ?? process.stderr;
21
22
  const detectedVars = detectProxyVars(env);
@@ -29,17 +30,42 @@ function applyProxyAutoDetect(options = {}) {
29
30
  detectedVars,
30
31
  reason: "node_use_env_proxy_already_set"
31
32
  };
32
- env.NODE_USE_ENV_PROXY = "1";
33
+ const argv = options.argv ?? process.argv;
34
+ const entrypoint = argv[1];
35
+ if (!entrypoint) return {
36
+ applied: false,
37
+ detectedVars,
38
+ reason: "missing_entrypoint"
39
+ };
40
+ const execPath = options.execPath ?? process.execPath;
41
+ const execArgv = options.execArgv ?? process.execArgv;
42
+ const spawn = options.spawn ?? spawnSync;
43
+ const exit = options.exit ?? ((code) => {
44
+ process.exit(code);
45
+ throw new Error("process.exit returned unexpectedly");
46
+ });
33
47
  if (!hintPrinted) {
34
48
  hintPrinted = true;
35
49
  const names = detectedVars.join("/");
36
- stderr.write(`primitive: proxy detected via ${names}, NODE_USE_ENV_PROXY=1 set automatically\n`);
50
+ stderr.write(`primitive: proxy detected via ${names}, restarting with NODE_USE_ENV_PROXY=1\n`);
37
51
  }
38
- return {
39
- applied: true,
40
- detectedVars,
41
- reason: "applied"
42
- };
52
+ const child = spawn(execPath, [
53
+ ...execArgv,
54
+ entrypoint,
55
+ ...argv.slice(2)
56
+ ], {
57
+ env: {
58
+ ...env,
59
+ NODE_USE_ENV_PROXY: "1"
60
+ },
61
+ stdio: "inherit"
62
+ });
63
+ if (child.error) throw child.error;
64
+ if (child.signal) {
65
+ (options.kill ?? process.kill)(options.pid ?? process.pid, child.signal);
66
+ return exit(1);
67
+ }
68
+ return exit(child.status ?? 1);
43
69
  }
44
70
  //#endregion
45
- export { _resetHintLatchForTest, applyProxyAutoDetect };
71
+ export { _resetHintLatchForTest, restartWithProxyEnvIfNeeded };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/cli",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.",
5
5
  "type": "module",
6
6
  "sideEffects": false,