@primitivedotdev/cli 0.29.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.
@@ -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,
@@ -7375,8 +7376,11 @@ const operationManifest = [
7375
7376
  "type": "string"
7376
7377
  },
7377
7378
  {
7379
+ "default": 50,
7378
7380
  "description": "Number of results per page",
7379
7381
  "enum": null,
7382
+ "maximum": 100,
7383
+ "minimum": 1,
7380
7384
  "name": "limit",
7381
7385
  "required": false,
7382
7386
  "type": "integer"
@@ -7645,13 +7649,17 @@ const operationManifest = [
7645
7649
  "type": "string"
7646
7650
  },
7647
7651
  {
7652
+ "default": 50,
7648
7653
  "description": "Number of results per page",
7649
7654
  "enum": null,
7655
+ "maximum": 100,
7656
+ "minimum": 1,
7650
7657
  "name": "limit",
7651
7658
  "required": false,
7652
7659
  "type": "integer"
7653
7660
  },
7654
7661
  {
7662
+ "default": "true",
7655
7663
  "description": "Include subject/body highlight snippets when text search is active.",
7656
7664
  "enum": ["true", "false"],
7657
7665
  "name": "snippet",
@@ -7659,6 +7667,7 @@ const operationManifest = [
7659
7667
  "type": "string"
7660
7668
  },
7661
7669
  {
7670
+ "default": "true",
7662
7671
  "description": "Include facet counts for sender, domain, status, and attachment presence.",
7663
7672
  "enum": ["true", "false"],
7664
7673
  "name": "include_facets",
@@ -9114,8 +9123,11 @@ const operationManifest = [
9114
9123
  "type": "string"
9115
9124
  }],
9116
9125
  "queryParams": [{
9126
+ "default": 50,
9117
9127
  "description": "Maximum number of rows to return. Clamped to 1..200; default\n50.\n",
9118
9128
  "enum": null,
9129
+ "maximum": 200,
9130
+ "minimum": 1,
9119
9131
  "name": "limit",
9120
9132
  "required": false,
9121
9133
  "type": "integer"
@@ -9387,7 +9399,7 @@ const operationManifest = [
9387
9399
  "binaryResponse": false,
9388
9400
  "bodyRequired": false,
9389
9401
  "command": "test-function",
9390
- "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",
9391
9403
  "hasJsonBody": true,
9392
9404
  "method": "POST",
9393
9405
  "operationId": "testFunction",
@@ -9950,8 +9962,11 @@ const operationManifest = [
9950
9962
  "type": "string"
9951
9963
  },
9952
9964
  {
9965
+ "default": 50,
9953
9966
  "description": "Number of results per page",
9954
9967
  "enum": null,
9968
+ "maximum": 100,
9969
+ "minimum": 1,
9955
9970
  "name": "limit",
9956
9971
  "required": false,
9957
9972
  "type": "integer"
@@ -10523,8 +10538,11 @@ const operationManifest = [
10523
10538
  "type": "string"
10524
10539
  },
10525
10540
  {
10541
+ "default": 50,
10526
10542
  "description": "Number of results per page",
10527
10543
  "enum": null,
10544
+ "maximum": 100,
10545
+ "minimum": 1,
10528
10546
  "name": "limit",
10529
10547
  "required": false,
10530
10548
  "type": "integer"
@@ -10735,7 +10753,7 @@ const CREDENTIALS_FILE = "credentials.json";
10735
10753
  const CREDENTIALS_LOCK_DIR = "credentials.lock";
10736
10754
  const CREDENTIALS_LOCK_STALE_MS = 1800 * 1e3;
10737
10755
  const MALFORMED_CREDENTIALS_HINT = "Run `primitive logout` and then `primitive login`.";
10738
- function isRecord$1(value) {
10756
+ function isRecord$2(value) {
10739
10757
  return value !== null && typeof value === "object" && !Array.isArray(value);
10740
10758
  }
10741
10759
  function requireString(value, key) {
@@ -10758,7 +10776,7 @@ var StaleCredentialFormatError = class extends Error {
10758
10776
  }
10759
10777
  };
10760
10778
  function parseCredentials(raw) {
10761
- 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}`);
10762
10780
  if (typeof raw.api_base_url_1 !== "string" && typeof raw.base_url === "string") throw new StaleCredentialFormatError();
10763
10781
  const orgName = raw.org_name;
10764
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}`);
@@ -10903,6 +10921,269 @@ function resolveCliAuth(params) {
10903
10921
  };
10904
10922
  }
10905
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
10906
11187
  //#region src/oclif/endpoints-test-redirect.ts
10907
11188
  async function detectFunctionEndpoint(endpointId, listEndpoints) {
10908
11189
  let response;
@@ -10988,6 +11269,22 @@ function flagName(parameterName) {
10988
11269
  function flagDescription(parameter) {
10989
11270
  return parameter.description ?? parameter.name;
10990
11271
  }
11272
+ const numberFlag = Flags.custom({ async parse(input, _context, options) {
11273
+ const trimmed = input.trim();
11274
+ if (trimmed === "") throw new Errors.CLIError(`Expected a number but received: ${input}`);
11275
+ const value = Number(trimmed);
11276
+ if (!Number.isFinite(value)) throw new Errors.CLIError(`Expected a number but received: ${input}`);
11277
+ if (options.min !== void 0 && value < options.min) throw new Errors.CLIError(`Expected a number greater than or equal to ${options.min} but received: ${input}`);
11278
+ if (options.max !== void 0 && value > options.max) throw new Errors.CLIError(`Expected a number less than or equal to ${options.max} but received: ${input}`);
11279
+ return value;
11280
+ } });
11281
+ function numericFlagOptions(parameter) {
11282
+ return {
11283
+ ...typeof parameter.default === "number" ? { default: parameter.default } : {},
11284
+ ...typeof parameter.maximum === "number" ? { max: parameter.maximum } : {},
11285
+ ...typeof parameter.minimum === "number" ? { min: parameter.minimum } : {}
11286
+ };
11287
+ }
10991
11288
  function extractBodyFields(schema) {
10992
11289
  if (!schema || typeof schema !== "object") return [];
10993
11290
  const properties = schema.properties;
@@ -11003,7 +11300,8 @@ function extractBodyFields(schema) {
11003
11300
  if (typeof t === "string") {
11004
11301
  displayType = t;
11005
11302
  if (t === "string") kind = "string";
11006
- else if (t === "integer" || t === "number") kind = "integer";
11303
+ else if (t === "integer") kind = "integer";
11304
+ else if (t === "number") kind = "number";
11007
11305
  else if (t === "boolean") kind = "boolean";
11008
11306
  else if (t === "array") {
11009
11307
  const items = propSchema.items;
@@ -11019,7 +11317,8 @@ function extractBodyFields(schema) {
11019
11317
  const single = nonNull[0];
11020
11318
  displayType = `${single}?`;
11021
11319
  if (single === "string") kind = "string";
11022
- else if (single === "integer" || single === "number") kind = "integer";
11320
+ else if (single === "integer") kind = "integer";
11321
+ else if (single === "number") kind = "number";
11023
11322
  else if (single === "boolean") kind = "boolean";
11024
11323
  else kind = "complex";
11025
11324
  } else {
@@ -11036,7 +11335,9 @@ function extractBodyFields(schema) {
11036
11335
  required: required.has(name),
11037
11336
  displayType,
11038
11337
  kind,
11039
- ...enumValues && enumValues.length > 0 ? { enumValues } : {}
11338
+ ...enumValues && enumValues.length > 0 ? { enumValues } : {},
11339
+ ...typeof propSchema.maximum === "number" ? { maximum: propSchema.maximum } : {},
11340
+ ...typeof propSchema.minimum === "number" ? { minimum: propSchema.minimum } : {}
11040
11341
  });
11041
11342
  }
11042
11343
  return fields.sort((a, b) => {
@@ -11084,7 +11385,14 @@ function flagForParameter(parameter) {
11084
11385
  required: parameter.required
11085
11386
  };
11086
11387
  if (parameter.type === "boolean") return Flags.boolean(common);
11087
- if (parameter.type === "integer") return Flags.integer(common);
11388
+ if (parameter.type === "integer") return Flags.integer({
11389
+ ...common,
11390
+ ...numericFlagOptions(parameter)
11391
+ });
11392
+ if (parameter.type === "number") return numberFlag({
11393
+ ...common,
11394
+ ...numericFlagOptions(parameter)
11395
+ });
11088
11396
  if (parameter.enum && parameter.enum.length > 0) return Flags.string({
11089
11397
  ...common,
11090
11398
  options: parameter.enum
@@ -11199,7 +11507,7 @@ function writeErrorWithHints(payload) {
11199
11507
  function removeStaleSavedCredentialOnUnauthorized(params) {
11200
11508
  if (extractErrorCode(params.payload) !== API_ERROR_CODES.unauthorized || params.auth.source !== "stored") return false;
11201
11509
  if (params.baseUrlOverridden && params.auth.credentials !== null && params.auth.apiBaseUrl1 !== params.auth.credentials.api_base_url_1) {
11202
- 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");
11203
11511
  return false;
11204
11512
  }
11205
11513
  deleteCliCredentials(params.configDir);
@@ -11224,9 +11532,6 @@ async function runWithTiming(enabled, fn) {
11224
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.";
11225
11533
  const API_BASE_URL_1_FLAG_DESCRIPTION = "Override the primary API base URL. Internal testing only; not documented to customers.";
11226
11534
  const API_BASE_URL_2_FLAG_DESCRIPTION = "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.";
11227
- function baseUrlOverriddenFromFlags(flags) {
11228
- return typeof flags["api-base-url-1"] === "string" || typeof flags["api-base-url-2"] === "string";
11229
- }
11230
11535
  const HOST_2_OPERATIONS = new Set(["sendEmail"]);
11231
11536
  const RESERVED_FLAG_NAMES = new Set([
11232
11537
  "api-key",
@@ -11234,12 +11539,20 @@ const RESERVED_FLAG_NAMES = new Set([
11234
11539
  "api-base-url-2",
11235
11540
  "raw-body",
11236
11541
  "body-file",
11542
+ "envelope",
11237
11543
  "output"
11238
11544
  ]);
11239
11545
  function bodyFieldFlag(field) {
11240
11546
  const common = { description: field.description || field.name };
11241
11547
  if (field.kind === "boolean") return Flags.boolean(common);
11242
- if (field.kind === "integer") return Flags.integer(common);
11548
+ if (field.kind === "integer") return Flags.integer({
11549
+ ...common,
11550
+ ...numericFlagOptions(field)
11551
+ });
11552
+ if (field.kind === "number") return numberFlag({
11553
+ ...common,
11554
+ ...numericFlagOptions(field)
11555
+ });
11243
11556
  if (field.enumValues) return Flags.string({
11244
11557
  ...common,
11245
11558
  options: field.enumValues
@@ -11264,6 +11577,7 @@ function buildFlags(operation) {
11264
11577
  }),
11265
11578
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
11266
11579
  };
11580
+ if (!operation.binaryResponse) flags.envelope = Flags.boolean({ description: "Print the full response envelope, including pagination metadata such as meta.cursor. Defaults to printing only the data payload for backward compatibility." });
11267
11581
  for (const parameter of [...operation.pathParams, ...operation.queryParams]) flags[flagName(parameter.name)] = flagForParameter(parameter);
11268
11582
  const bodyFieldFlagToProperty = /* @__PURE__ */ new Map();
11269
11583
  if (operation.hasJsonBody) {
@@ -11302,6 +11616,9 @@ function collectValues(parameters, flags) {
11302
11616
  }
11303
11617
  return values;
11304
11618
  }
11619
+ function operationOutputPayload(envelope, includeEnvelope) {
11620
+ return includeEnvelope ? envelope ?? null : envelope?.data ?? null;
11621
+ }
11305
11622
  const OPERATION_HINTS = {
11306
11623
  createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
11307
11624
  updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
@@ -11323,18 +11640,12 @@ function createOperationCommand(operation) {
11323
11640
  const { flags } = await this.parse(OperationCommand);
11324
11641
  const parsedFlags = flags;
11325
11642
  await runWithTiming(parsedFlags.time === true, async () => {
11326
- const baseUrlOverridden = typeof parsedFlags["api-base-url-1"] === "string" || typeof parsedFlags["api-base-url-2"] === "string";
11327
- const auth = resolveCliAuth({
11643
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
11328
11644
  apiKey: typeof parsedFlags["api-key"] === "string" ? parsedFlags["api-key"] : void 0,
11329
11645
  apiBaseUrl1: typeof parsedFlags["api-base-url-1"] === "string" ? parsedFlags["api-base-url-1"] : void 0,
11330
11646
  apiBaseUrl2: typeof parsedFlags["api-base-url-2"] === "string" ? parsedFlags["api-base-url-2"] : void 0,
11331
11647
  configDir: this.config.configDir
11332
11648
  });
11333
- const apiClient = new PrimitiveApiClient({
11334
- apiKey: auth.apiKey,
11335
- apiBaseUrl1: auth.apiBaseUrl1,
11336
- apiBaseUrl2: auth.apiBaseUrl2
11337
- });
11338
11649
  let body;
11339
11650
  if (operation.hasJsonBody) {
11340
11651
  const explicit = readJsonBody(parsedFlags);
@@ -11409,7 +11720,7 @@ function createOperationCommand(operation) {
11409
11720
  writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
11410
11721
  process.stderr.write(chunk);
11411
11722
  } });
11412
- this.log(JSON.stringify(envelope?.data ?? null, null, 2));
11723
+ this.log(JSON.stringify(operationOutputPayload(envelope, parsedFlags.envelope === true), null, 2));
11413
11724
  });
11414
11725
  }
11415
11726
  }
@@ -11426,6 +11737,128 @@ function canonicalizeCliReferences(description) {
11426
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'`");
11427
11738
  }
11428
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
11429
11862
  //#region src/oclif/commands/doctor.ts
11430
11863
  const MIN_NODE_MAJOR = 22;
11431
11864
  function renderRow({ label, outcome }) {
@@ -11524,14 +11957,10 @@ function checkApiKey(opts) {
11524
11957
  hint: "Run `primitive login`, pass --api-key explicitly, or export PRIMITIVE_API_KEY=prim_..."
11525
11958
  };
11526
11959
  }
11527
- async function checkAccount(opts) {
11960
+ async function checkAccount(client) {
11528
11961
  try {
11529
11962
  const result = await getAccount({
11530
- client: new PrimitiveApiClient({
11531
- apiKey: opts.apiKey,
11532
- apiBaseUrl1: opts.apiBaseUrl1,
11533
- apiBaseUrl2: opts.apiBaseUrl2
11534
- }).client,
11963
+ client: client.client,
11535
11964
  responseStyle: "fields"
11536
11965
  });
11537
11966
  const apiError = result.error;
@@ -11572,14 +12001,10 @@ async function checkAccount(opts) {
11572
12001
  };
11573
12002
  }
11574
12003
  }
11575
- async function checkDomains(opts) {
12004
+ async function checkDomains(client) {
11576
12005
  try {
11577
12006
  const result = await listDomains({
11578
- client: new PrimitiveApiClient({
11579
- apiKey: opts.apiKey,
11580
- apiBaseUrl1: opts.apiBaseUrl1,
11581
- apiBaseUrl2: opts.apiBaseUrl2
11582
- }).client,
12007
+ client: client.client,
11583
12008
  responseStyle: "fields"
11584
12009
  });
11585
12010
  if (result.error) return {
@@ -11650,28 +12075,20 @@ var DoctorCommand = class DoctorCommand extends Command {
11650
12075
  outcome: apiKeyCheck
11651
12076
  });
11652
12077
  if (apiKeyCheck.status !== "fail") {
11653
- const auth = resolveCliAuth({
12078
+ const { apiClient, auth } = createAuthenticatedCliApiClient({
11654
12079
  apiKey: flags["api-key"],
11655
12080
  apiBaseUrl1: flags["api-base-url-1"],
11656
12081
  apiBaseUrl2: flags["api-base-url-2"],
11657
12082
  configDir: this.config.configDir
11658
12083
  });
11659
12084
  if (auth.apiKey !== void 0) {
11660
- const accountCheck = await checkAccount({
11661
- apiKey: auth.apiKey,
11662
- apiBaseUrl1: auth.apiBaseUrl1,
11663
- apiBaseUrl2: auth.apiBaseUrl2
11664
- });
12085
+ const accountCheck = await checkAccount(apiClient);
11665
12086
  rows.push({
11666
12087
  label: "API auth",
11667
12088
  outcome: accountCheck.outcome
11668
12089
  });
11669
12090
  if (accountCheck.outcome.status === "ok") {
11670
- const domainsOutcome = await checkDomains({
11671
- apiKey: auth.apiKey,
11672
- apiBaseUrl1: auth.apiBaseUrl1,
11673
- apiBaseUrl2: auth.apiBaseUrl2
11674
- });
12091
+ const domainsOutcome = await checkDomains(apiClient);
11675
12092
  rows.push({
11676
12093
  label: "Domains",
11677
12094
  outcome: domainsOutcome
@@ -11765,19 +12182,14 @@ var EmailsLatestCommand = class EmailsLatestCommand extends Command {
11765
12182
  async run() {
11766
12183
  const { flags } = await this.parse(EmailsLatestCommand);
11767
12184
  await runWithTiming(flags.time, async () => {
11768
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
11769
- const auth = resolveCliAuth({
12185
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
11770
12186
  apiKey: flags["api-key"],
11771
12187
  apiBaseUrl1: flags["api-base-url-1"],
11772
12188
  apiBaseUrl2: flags["api-base-url-2"],
11773
12189
  configDir: this.config.configDir
11774
12190
  });
11775
12191
  const result = await listEmails({
11776
- client: new PrimitiveApiClient({
11777
- apiKey: auth.apiKey,
11778
- apiBaseUrl1: auth.apiBaseUrl1,
11779
- apiBaseUrl2: auth.apiBaseUrl2
11780
- }).client,
12192
+ client: apiClient.client,
11781
12193
  query: { limit: flags.limit },
11782
12194
  responseStyle: "fields"
11783
12195
  });
@@ -11974,18 +12386,12 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
11974
12386
  };
11975
12387
  async run() {
11976
12388
  const { flags } = await this.parse(EmailsWaitCommand);
11977
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
11978
- const auth = resolveCliAuth({
12389
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
11979
12390
  apiKey: flags["api-key"],
11980
12391
  apiBaseUrl1: flags["api-base-url-1"],
11981
12392
  apiBaseUrl2: flags["api-base-url-2"],
11982
12393
  configDir: this.config.configDir
11983
12394
  });
11984
- const apiClient = new PrimitiveApiClient({
11985
- apiKey: auth.apiKey,
11986
- apiBaseUrl1: auth.apiBaseUrl1,
11987
- apiBaseUrl2: auth.apiBaseUrl2
11988
- });
11989
12395
  let since;
11990
12396
  try {
11991
12397
  since = sinceFromFlags(flags);
@@ -12102,18 +12508,12 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
12102
12508
  };
12103
12509
  async run() {
12104
12510
  const { flags } = await this.parse(EmailsWatchCommand);
12105
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
12106
- const auth = resolveCliAuth({
12511
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
12107
12512
  apiKey: flags["api-key"],
12108
12513
  apiBaseUrl1: flags["api-base-url-1"],
12109
12514
  apiBaseUrl2: flags["api-base-url-2"],
12110
12515
  configDir: this.config.configDir
12111
12516
  });
12112
- const apiClient = new PrimitiveApiClient({
12113
- apiKey: auth.apiKey,
12114
- apiBaseUrl1: auth.apiBaseUrl1,
12115
- apiBaseUrl2: auth.apiBaseUrl2
12116
- });
12117
12517
  let since;
12118
12518
  try {
12119
12519
  since = sinceFromFlags(flags);
@@ -12167,6 +12567,92 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
12167
12567
  }
12168
12568
  };
12169
12569
  //#endregion
12570
+ //#region src/oclif/function-deploy-wait.ts
12571
+ function validateDeployWaitFlags(params) {
12572
+ if (params.timeoutSeconds < 0) return "--timeout must be greater than or equal to 0.";
12573
+ if (params.pollIntervalSeconds <= 0) return "--poll-interval must be greater than 0.";
12574
+ return null;
12575
+ }
12576
+ function isTerminal(status) {
12577
+ return status === "deployed" || status === "failed";
12578
+ }
12579
+ function resultForTerminal(snapshot) {
12580
+ if (snapshot.deploy_status === "failed") return {
12581
+ function: snapshot,
12582
+ kind: "failed"
12583
+ };
12584
+ return {
12585
+ function: snapshot,
12586
+ kind: "ok"
12587
+ };
12588
+ }
12589
+ function toDeployWaitSnapshot(value) {
12590
+ return {
12591
+ ...value.created_at !== void 0 ? { created_at: value.created_at } : {},
12592
+ ...value.deploy_error !== void 0 ? { deploy_error: value.deploy_error } : {},
12593
+ deploy_status: value.deploy_status,
12594
+ ...value.deployed_at !== void 0 ? { deployed_at: value.deployed_at } : {},
12595
+ gateway_url: value.gateway_url,
12596
+ id: value.id,
12597
+ name: value.name,
12598
+ ...value.updated_at !== void 0 ? { updated_at: value.updated_at } : {}
12599
+ };
12600
+ }
12601
+ function elapsedSeconds(startedAt, now) {
12602
+ return Math.max(0, Math.round((now() - startedAt) / 1e3));
12603
+ }
12604
+ async function defaultSleep(ms) {
12605
+ await new Promise((resolve) => setTimeout(resolve, ms));
12606
+ }
12607
+ async function waitForFunctionDeploy(params) {
12608
+ const now = params.now ?? Date.now;
12609
+ const sleep = params.sleep ?? defaultSleep;
12610
+ const writeStderr = params.writeStderr ?? ((chunk) => {
12611
+ process.stderr.write(chunk);
12612
+ });
12613
+ const startedAt = now();
12614
+ const timeoutMs = params.timeoutSeconds * 1e3;
12615
+ const pollIntervalMs = params.pollIntervalSeconds * 1e3;
12616
+ const hasTimeout = params.timeoutSeconds > 0;
12617
+ let last = params.initial ? toDeployWaitSnapshot(params.initial) : null;
12618
+ let lastStatus = last?.deploy_status ?? "unknown";
12619
+ if (last && isTerminal(last.deploy_status)) return resultForTerminal(last);
12620
+ writeStderr(`Waiting for function ${params.id} deploy to finish (current status: ${lastStatus})...\n`);
12621
+ while (true) {
12622
+ const elapsedMs = now() - startedAt;
12623
+ if (hasTimeout && elapsedMs >= timeoutMs) return {
12624
+ elapsedSeconds: elapsedSeconds(startedAt, now),
12625
+ kind: "timeout",
12626
+ lastFunction: last
12627
+ };
12628
+ await sleep(hasTimeout ? Math.min(pollIntervalMs, Math.max(0, timeoutMs - elapsedMs)) : pollIntervalMs);
12629
+ if (hasTimeout && now() - startedAt >= timeoutMs) return {
12630
+ elapsedSeconds: elapsedSeconds(startedAt, now),
12631
+ kind: "timeout",
12632
+ lastFunction: last
12633
+ };
12634
+ const result = await params.getFunction({ id: params.id });
12635
+ if (result.error) return {
12636
+ kind: "error",
12637
+ payload: extractErrorPayload(result.error)
12638
+ };
12639
+ const fetched = result.data?.data;
12640
+ if (!fetched) return {
12641
+ kind: "error",
12642
+ payload: {
12643
+ code: "client_error",
12644
+ message: "Get function returned no data while waiting for deploy"
12645
+ }
12646
+ };
12647
+ last = toDeployWaitSnapshot(fetched);
12648
+ if (last.deploy_status !== lastStatus) {
12649
+ lastStatus = last.deploy_status;
12650
+ writeStderr(`Function ${params.id} deploy status: ${last.deploy_status}\n`);
12651
+ }
12652
+ if (isTerminal(last.deploy_status)) return resultForTerminal(last);
12653
+ }
12654
+ }
12655
+ //#endregion
12170
12656
  //#region src/oclif/lint/raw-send-mail-fetch.ts
12171
12657
  const RAW_SEND_MAIL_FETCH_REGEX = /fetch\s*\(\s*[`'"][^`'"]*primitive\.dev[^`'"]*\/send-mail(?![A-Za-z0-9_-])/g;
12172
12658
  const SNIPPET_PADDING = 60;
@@ -12208,8 +12694,8 @@ function resolveSecretFlags(input) {
12208
12694
  const secrets = [];
12209
12695
  const seenKeys = /* @__PURE__ */ new Set();
12210
12696
  const env = input.env ?? process.env;
12211
- const readFile = input.readFile ?? defaultReadFile;
12212
- const readStdin = input.readStdin ?? defaultReadStdin;
12697
+ const readFile = input.readFile ?? defaultReadFile$1;
12698
+ const readStdin = input.readStdin ?? defaultReadStdin$1;
12213
12699
  const envFileCache = /* @__PURE__ */ new Map();
12214
12700
  const reserveSecretKey = (key, sourceLabel) => {
12215
12701
  const keyError = validateKey(key, sourceLabel);
@@ -12291,6 +12777,8 @@ function resolveSecretFlags(input) {
12291
12777
  };
12292
12778
  }
12293
12779
  function resolveSingleSecretValue(input) {
12780
+ const keyError = validateKey(input.key, "--key");
12781
+ if (keyError) return keyError;
12294
12782
  if ([
12295
12783
  input.value !== void 0 ? "--value" : null,
12296
12784
  input.valueFromEnv !== void 0 ? "--value-from-env" : null,
@@ -12302,8 +12790,8 @@ function resolveSingleSecretValue(input) {
12302
12790
  message: "Pass exactly one of --value, --value-from-env, --value-file, --value-from-env-file, or --stdin."
12303
12791
  };
12304
12792
  const env = input.env ?? process.env;
12305
- const readFile = input.readFile ?? defaultReadFile;
12306
- const readStdin = input.readStdin ?? defaultReadStdin;
12793
+ const readFile = input.readFile ?? defaultReadFile$1;
12794
+ const readStdin = input.readStdin ?? defaultReadStdin$1;
12307
12795
  if (input.value !== void 0) return {
12308
12796
  kind: "ok",
12309
12797
  value: input.value
@@ -12341,10 +12829,10 @@ function resolveSingleSecretValue(input) {
12341
12829
  message: "Pass exactly one of --value, --value-from-env, --value-file, --value-from-env-file, or --stdin."
12342
12830
  };
12343
12831
  }
12344
- function defaultReadFile(path) {
12832
+ function defaultReadFile$1(path) {
12345
12833
  return readFileSync(path, "utf8");
12346
12834
  }
12347
- function defaultReadStdin() {
12835
+ function defaultReadStdin$1() {
12348
12836
  if (process.stdin.isTTY) throw new Error("stdin is a TTY; pipe a value into this command or pass a file/env source instead.");
12349
12837
  return readFileSync(0, "utf8");
12350
12838
  }
@@ -12631,6 +13119,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
12631
13119
  static summary = "Deploy a new function from a bundled handler file";
12632
13120
  static examples = [
12633
13121
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js",
13122
+ "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --wait",
12634
13123
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
12635
13124
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
12636
13125
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret-from-env OPENAI_KEY --secret-from-env-file .env.local:OWNER_EMAIL",
@@ -12677,11 +13166,29 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
12677
13166
  multiple: true
12678
13167
  }),
12679
13168
  "secret-from-stdin": Flags.string({ description: "Secret KEY to read from stdin and seed on the deployed function. A single trailing line ending is stripped. Stdin is consumed once, so this flag is not repeatable." }),
13169
+ wait: Flags.boolean({ description: "Wait until the function deploy reaches deployed or failed. Progress is written to stderr; stdout remains the final JSON payload." }),
13170
+ timeout: Flags.integer({
13171
+ default: 120,
13172
+ description: "Seconds to wait when --wait is set before exiting non-zero. Use 0 to wait forever."
13173
+ }),
13174
+ "poll-interval": Flags.integer({
13175
+ default: 2,
13176
+ description: "Seconds between deploy-status polls when --wait is set."
13177
+ }),
12680
13178
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
12681
13179
  };
12682
13180
  async run() {
12683
13181
  const { flags } = await this.parse(FunctionsDeployCommand);
12684
13182
  await runWithTiming(flags.time, async () => {
13183
+ const waitFlagError = validateDeployWaitFlags({
13184
+ pollIntervalSeconds: flags["poll-interval"],
13185
+ timeoutSeconds: flags.timeout
13186
+ });
13187
+ if (waitFlagError) {
13188
+ process.stderr.write(`${waitFlagError}\n`);
13189
+ process.exitCode = 1;
13190
+ return;
13191
+ }
12685
13192
  const parsedSecrets = resolveSecretFlags({
12686
13193
  fromEnv: flags["secret-from-env"] ?? [],
12687
13194
  fromEnvFile: flags["secret-from-env-file"] ?? [],
@@ -12697,18 +13204,12 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
12697
13204
  const code = readTextFileFlag(flags.file, "--file");
12698
13205
  const sourceMap = flags["source-map-file"] ? readTextFileFlag(flags["source-map-file"], "--source-map-file") : void 0;
12699
13206
  emitRawSendMailFetchWarning(code, (chunk) => process.stderr.write(chunk));
12700
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
12701
- const auth = resolveCliAuth({
13207
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
12702
13208
  apiKey: flags["api-key"],
12703
13209
  apiBaseUrl1: flags["api-base-url-1"],
12704
13210
  apiBaseUrl2: flags["api-base-url-2"],
12705
13211
  configDir: this.config.configDir
12706
13212
  });
12707
- const apiClient = new PrimitiveApiClient({
12708
- apiKey: auth.apiKey,
12709
- apiBaseUrl1: auth.apiBaseUrl1,
12710
- apiBaseUrl2: auth.apiBaseUrl2
12711
- });
12712
13213
  const authFailureContext = {
12713
13214
  auth,
12714
13215
  baseUrlOverridden,
@@ -12767,6 +13268,42 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
12767
13268
  return;
12768
13269
  }
12769
13270
  const payload = outcome.result.redeploy ?? outcome.result.created;
13271
+ if (flags.wait) {
13272
+ const waitResult = await waitForFunctionDeploy({
13273
+ getFunction: (p) => getFunction({
13274
+ client: apiClient.client,
13275
+ path: { id: p.id },
13276
+ responseStyle: "fields"
13277
+ }),
13278
+ id: payload.id,
13279
+ initial: payload,
13280
+ pollIntervalSeconds: flags["poll-interval"],
13281
+ timeoutSeconds: flags.timeout,
13282
+ writeStderr: (chunk) => process.stderr.write(chunk)
13283
+ });
13284
+ if (waitResult.kind === "error") {
13285
+ writeErrorWithHints(waitResult.payload);
13286
+ removeStaleSavedCredentialOnUnauthorized({
13287
+ ...authFailureContext,
13288
+ payload: waitResult.payload
13289
+ });
13290
+ process.exitCode = 1;
13291
+ return;
13292
+ }
13293
+ if (waitResult.kind === "timeout") {
13294
+ const status = waitResult.lastFunction?.deploy_status ?? "unknown";
13295
+ process.stderr.write(`Timed out after ${flags.timeout}s waiting for function ${payload.id} deploy to finish (last status: ${status}).\n`);
13296
+ process.exitCode = 2;
13297
+ return;
13298
+ }
13299
+ this.log(JSON.stringify(waitResult.function, null, 2));
13300
+ if (waitResult.kind === "failed") {
13301
+ const detail = waitResult.function.deploy_error ? `: ${waitResult.function.deploy_error}` : ".";
13302
+ process.stderr.write(`Function ${payload.id} deploy failed${detail}\n`);
13303
+ process.exitCode = 1;
13304
+ }
13305
+ return;
13306
+ }
12770
13307
  this.log(JSON.stringify(payload, null, 2));
12771
13308
  });
12772
13309
  }
@@ -12779,8 +13316,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
12779
13316
  name: "Primitive Team",
12780
13317
  url: "https://primitive.dev"
12781
13318
  };
12782
- const SDK_VERSION_RANGE = "^0.29.0";
12783
- const CLI_VERSION_RANGE = "^0.29.0";
13319
+ const SDK_VERSION_RANGE = "^0.30.1";
13320
+ const CLI_VERSION_RANGE = "^0.30.1";
12784
13321
  const ESBUILD_VERSION_RANGE = "^0.27.0";
12785
13322
  function renderHandler() {
12786
13323
  return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
@@ -12802,14 +13339,14 @@ import {
12802
13339
  // the loop guard recognizes mail returning to it as self-traffic.
12803
13340
  const REPLY_FROM = "you@your-domain.primitive.email";
12804
13341
 
12805
- // Loop protection. A deployed Function receives catch-all inbound for
12806
- // the managed *.primitive.email subdomain, which includes bounces and
12807
- // auto-replies generated by its own outbound traffic. Without this
12808
- // guard the handler can respond to its own bounces and create a
12809
- // 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.
12810
13347
  //
12811
13348
  // The default check returns true when From is on any *.primitive.email
12812
- // address (covers the managed subdomain catch-all, the simple
13349
+ // address (covers managed-domain fallback mail, the simple
12813
13350
  // self-reply case, and bounces from mailer-daemon@*.primitive.email)
12814
13351
  // or when From contains REPLY_FROM as a case-insensitive substring.
12815
13352
  // Substring matching is deliberate so display-name forms like
@@ -13179,6 +13716,160 @@ var FunctionsInitCommand = class FunctionsInitCommand extends Command {
13179
13716
  }
13180
13717
  };
13181
13718
  //#endregion
13719
+ //#region src/oclif/commands/functions-logs.ts
13720
+ const DEFAULT_LOG_LIMIT = 50;
13721
+ const DEFAULT_LOG_POLL_INTERVAL_SECONDS = 2;
13722
+ function levelLabel(level) {
13723
+ return level.toUpperCase().padEnd(5);
13724
+ }
13725
+ function orderFunctionLogsForDisplay(rows) {
13726
+ return [...rows].reverse();
13727
+ }
13728
+ function formatFunctionLogLine(row) {
13729
+ const metadata = row.metadata && Object.keys(row.metadata).length > 0 ? ` ${JSON.stringify(row.metadata)}` : "";
13730
+ return `${row.ts} ${levelLabel(row.level)} ${row.message}${metadata}`;
13731
+ }
13732
+ function collectFreshFunctionLogsFromPage(rows, seenIds) {
13733
+ const freshNewestFirst = [];
13734
+ let reachedSeen = false;
13735
+ for (const row of rows) {
13736
+ if (seenIds.has(row.id)) {
13737
+ reachedSeen = true;
13738
+ continue;
13739
+ }
13740
+ freshNewestFirst.push(row);
13741
+ seenIds.add(row.id);
13742
+ }
13743
+ return {
13744
+ freshNewestFirst,
13745
+ reachedSeen
13746
+ };
13747
+ }
13748
+ function emitLogRows(rows, jsonl) {
13749
+ for (const row of rows) {
13750
+ const line = jsonl ? JSON.stringify(row) : formatFunctionLogLine(row);
13751
+ process.stdout.write(`${line}\n`);
13752
+ }
13753
+ }
13754
+ var FunctionsLogsCommand = class FunctionsLogsCommand extends Command {
13755
+ static description = "List or follow function execution logs. Defaults to compact text output; use --jsonl for one JSON object per log row.";
13756
+ static summary = "List or follow a function's execution logs";
13757
+ static examples = [
13758
+ "<%= config.bin %> functions logs --id <fn-id>",
13759
+ "<%= config.bin %> functions logs --id <fn-id> --jsonl",
13760
+ "<%= config.bin %> functions logs --id <fn-id> --follow"
13761
+ ];
13762
+ static flags = {
13763
+ "api-key": Flags.string({
13764
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
13765
+ env: "PRIMITIVE_API_KEY"
13766
+ }),
13767
+ "api-base-url-1": Flags.string({
13768
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
13769
+ env: "PRIMITIVE_API_BASE_URL_1",
13770
+ hidden: true
13771
+ }),
13772
+ "api-base-url-2": Flags.string({
13773
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
13774
+ env: "PRIMITIVE_API_BASE_URL_2",
13775
+ hidden: true
13776
+ }),
13777
+ id: Flags.string({
13778
+ description: "Function id (UUID).",
13779
+ required: true
13780
+ }),
13781
+ limit: Flags.integer({
13782
+ default: DEFAULT_LOG_LIMIT,
13783
+ description: "Maximum rows to fetch per poll. Server clamps to 1..200."
13784
+ }),
13785
+ cursor: Flags.string({ description: "Opaque pagination cursor from a previous logs response. Not supported with --follow." }),
13786
+ follow: Flags.boolean({
13787
+ char: "f",
13788
+ description: "Keep polling the newest logs and print rows not seen yet."
13789
+ }),
13790
+ jsonl: Flags.boolean({ description: "Print one compact JSON object per log row." }),
13791
+ "poll-interval": Flags.integer({
13792
+ default: DEFAULT_LOG_POLL_INTERVAL_SECONDS,
13793
+ description: "Seconds between polls when --follow is set."
13794
+ }),
13795
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
13796
+ };
13797
+ async run() {
13798
+ const { flags } = await this.parse(FunctionsLogsCommand);
13799
+ if (flags.limit <= 0) this.error("--limit must be greater than 0.", { exit: 2 });
13800
+ if (flags["poll-interval"] <= 0) this.error("--poll-interval must be greater than 0.", { exit: 2 });
13801
+ if (flags.follow && flags.cursor) this.error("--cursor cannot be combined with --follow.", { exit: 2 });
13802
+ await runWithTiming(flags.time, async () => {
13803
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
13804
+ apiKey: flags["api-key"],
13805
+ apiBaseUrl1: flags["api-base-url-1"],
13806
+ apiBaseUrl2: flags["api-base-url-2"],
13807
+ configDir: this.config.configDir
13808
+ });
13809
+ const seenIds = /* @__PURE__ */ new Set();
13810
+ let completedInitialFollowPoll = false;
13811
+ let hasObservedLogs = false;
13812
+ let wroteEmptyHint = false;
13813
+ while (true) {
13814
+ let cursor = flags.cursor;
13815
+ let nextCursor = null;
13816
+ let rows = [];
13817
+ while (true) {
13818
+ const result = await listFunctionLogs({
13819
+ client: apiClient.client,
13820
+ path: { id: flags.id },
13821
+ query: {
13822
+ ...cursor ? { cursor } : {},
13823
+ limit: flags.limit
13824
+ },
13825
+ responseStyle: "fields"
13826
+ });
13827
+ if (result.error) {
13828
+ const errorPayload = extractErrorPayload(result.error);
13829
+ writeErrorWithHints(errorPayload);
13830
+ removeStaleSavedCredentialOnUnauthorized({
13831
+ auth,
13832
+ baseUrlOverridden,
13833
+ configDir: this.config.configDir,
13834
+ payload: errorPayload
13835
+ });
13836
+ process.exitCode = 1;
13837
+ return;
13838
+ }
13839
+ const page = result.data?.data ?? {
13840
+ items: [],
13841
+ next_cursor: null
13842
+ };
13843
+ nextCursor = page.next_cursor;
13844
+ if (!flags.follow) {
13845
+ rows = orderFunctionLogsForDisplay(page.items);
13846
+ break;
13847
+ }
13848
+ if (page.items.length > 0) hasObservedLogs = true;
13849
+ const collected = collectFreshFunctionLogsFromPage(page.items, seenIds);
13850
+ rows.push(...collected.freshNewestFirst);
13851
+ if (!completedInitialFollowPoll || collected.reachedSeen || !page.next_cursor) {
13852
+ rows = orderFunctionLogsForDisplay(rows);
13853
+ break;
13854
+ }
13855
+ cursor = page.next_cursor;
13856
+ }
13857
+ if (rows.length === 0 && !wroteEmptyHint) {
13858
+ process.stderr.write(flags.follow ? hasObservedLogs ? "Waiting for new function logs...\n" : "No function logs yet. Waiting for new rows...\n" : "No function logs yet. Trigger the function, then run this command again.\n");
13859
+ wroteEmptyHint = true;
13860
+ }
13861
+ emitLogRows(rows, flags.jsonl);
13862
+ if (!flags.follow) {
13863
+ if (nextCursor) process.stderr.write(`next cursor: ${nextCursor}\n`);
13864
+ return;
13865
+ }
13866
+ completedInitialFollowPoll = true;
13867
+ await sleep$1(flags["poll-interval"] * 1e3);
13868
+ }
13869
+ });
13870
+ }
13871
+ };
13872
+ //#endregion
13182
13873
  //#region src/oclif/commands/functions-redeploy.ts
13183
13874
  async function runRedeployWithSecrets(api, params) {
13184
13875
  const writtenSecrets = [];
@@ -13260,6 +13951,7 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
13260
13951
  static summary = "Redeploy a function from a bundled handler file";
13261
13952
  static examples = [
13262
13953
  "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js",
13954
+ "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --wait",
13263
13955
  "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --source-map-file ./bundle.js.map",
13264
13956
  "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
13265
13957
  "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --secret-from-env OPENAI_KEY --secret-from-file PRIVATE_KEY=./private-key.pem",
@@ -13306,11 +13998,29 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
13306
13998
  multiple: true
13307
13999
  }),
13308
14000
  "secret-from-stdin": Flags.string({ description: "Secret KEY to read from stdin and write before the redeploy. A single trailing line ending is stripped. Stdin is consumed once, so this flag is not repeatable." }),
14001
+ wait: Flags.boolean({ description: "Wait until the function deploy reaches deployed or failed. Progress is written to stderr; stdout remains the final JSON payload." }),
14002
+ timeout: Flags.integer({
14003
+ default: 120,
14004
+ description: "Seconds to wait when --wait is set before exiting non-zero. Use 0 to wait forever."
14005
+ }),
14006
+ "poll-interval": Flags.integer({
14007
+ default: 2,
14008
+ description: "Seconds between deploy-status polls when --wait is set."
14009
+ }),
13309
14010
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
13310
14011
  };
13311
14012
  async run() {
13312
14013
  const { flags } = await this.parse(FunctionsRedeployCommand);
13313
14014
  await runWithTiming(flags.time, async () => {
14015
+ const waitFlagError = validateDeployWaitFlags({
14016
+ pollIntervalSeconds: flags["poll-interval"],
14017
+ timeoutSeconds: flags.timeout
14018
+ });
14019
+ if (waitFlagError) {
14020
+ process.stderr.write(`${waitFlagError}\n`);
14021
+ process.exitCode = 1;
14022
+ return;
14023
+ }
13314
14024
  const parsedSecrets = resolveSecretFlags({
13315
14025
  fromEnv: flags["secret-from-env"] ?? [],
13316
14026
  fromEnvFile: flags["secret-from-env-file"] ?? [],
@@ -13326,18 +14036,12 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
13326
14036
  const code = readTextFileFlag(flags.file, "--file");
13327
14037
  const sourceMap = flags["source-map-file"] ? readTextFileFlag(flags["source-map-file"], "--source-map-file") : void 0;
13328
14038
  emitRawSendMailFetchWarning(code, (chunk) => process.stderr.write(chunk));
13329
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
13330
- const auth = resolveCliAuth({
14039
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
13331
14040
  apiKey: flags["api-key"],
13332
14041
  apiBaseUrl1: flags["api-base-url-1"],
13333
14042
  apiBaseUrl2: flags["api-base-url-2"],
13334
14043
  configDir: this.config.configDir
13335
14044
  });
13336
- const apiClient = new PrimitiveApiClient({
13337
- apiKey: auth.apiKey,
13338
- apiBaseUrl1: auth.apiBaseUrl1,
13339
- apiBaseUrl2: auth.apiBaseUrl2
13340
- });
13341
14045
  const authFailureContext = {
13342
14046
  auth,
13343
14047
  baseUrlOverridden,
@@ -13386,6 +14090,42 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
13386
14090
  process.exitCode = 1;
13387
14091
  return;
13388
14092
  }
14093
+ if (flags.wait) {
14094
+ const waitResult = await waitForFunctionDeploy({
14095
+ getFunction: (p) => getFunction({
14096
+ client: apiClient.client,
14097
+ path: { id: p.id },
14098
+ responseStyle: "fields"
14099
+ }),
14100
+ id: outcome.result.redeploy.id,
14101
+ initial: outcome.result.redeploy,
14102
+ pollIntervalSeconds: flags["poll-interval"],
14103
+ timeoutSeconds: flags.timeout,
14104
+ writeStderr: (chunk) => process.stderr.write(chunk)
14105
+ });
14106
+ if (waitResult.kind === "error") {
14107
+ writeErrorWithHints(waitResult.payload);
14108
+ removeStaleSavedCredentialOnUnauthorized({
14109
+ ...authFailureContext,
14110
+ payload: waitResult.payload
14111
+ });
14112
+ process.exitCode = 1;
14113
+ return;
14114
+ }
14115
+ if (waitResult.kind === "timeout") {
14116
+ const status = waitResult.lastFunction?.deploy_status ?? "unknown";
14117
+ process.stderr.write(`Timed out after ${flags.timeout}s waiting for function ${outcome.result.redeploy.id} deploy to finish (last status: ${status}).\n`);
14118
+ process.exitCode = 2;
14119
+ return;
14120
+ }
14121
+ this.log(JSON.stringify(waitResult.function, null, 2));
14122
+ if (waitResult.kind === "failed") {
14123
+ const detail = waitResult.function.deploy_error ? `: ${waitResult.function.deploy_error}` : ".";
14124
+ process.stderr.write(`Function ${outcome.result.redeploy.id} deploy failed${detail}\n`);
14125
+ process.exitCode = 1;
14126
+ }
14127
+ return;
14128
+ }
13389
14129
  this.log(JSON.stringify(outcome.result.redeploy, null, 2));
13390
14130
  });
13391
14131
  }
@@ -13511,18 +14251,12 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
13511
14251
  async run() {
13512
14252
  const { flags } = await this.parse(FunctionsSetSecretCommand);
13513
14253
  await runWithTiming(flags.time, async () => {
13514
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
13515
- const auth = resolveCliAuth({
14254
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
13516
14255
  apiKey: flags["api-key"],
13517
14256
  apiBaseUrl1: flags["api-base-url-1"],
13518
14257
  apiBaseUrl2: flags["api-base-url-2"],
13519
14258
  configDir: this.config.configDir
13520
14259
  });
13521
- const apiClient = new PrimitiveApiClient({
13522
- apiKey: auth.apiKey,
13523
- apiBaseUrl1: auth.apiBaseUrl1,
13524
- apiBaseUrl2: auth.apiBaseUrl2
13525
- });
13526
14260
  const authFailureContext = {
13527
14261
  auth,
13528
14262
  baseUrlOverridden,
@@ -13630,11 +14364,17 @@ function buildFunctionTestOutcome(params) {
13630
14364
  if (params.showSends) outcome.sent_emails = params.detail.replies;
13631
14365
  return outcome;
13632
14366
  }
14367
+ function writeFunctionTestProgress(message, writeStderr = (chunk) => {
14368
+ process.stderr.write(chunk);
14369
+ }) {
14370
+ writeStderr(`${message}\n`);
14371
+ }
13633
14372
  function stringOrNull(value) {
13634
14373
  return typeof value === "string" && value.length > 0 ? value : null;
13635
14374
  }
13636
14375
  function findMatchingFunctionEndpoints(params) {
13637
- const matches = [];
14376
+ const domainMatches = [];
14377
+ const fallbackMatches = [];
13638
14378
  for (const endpoint of params.endpoints) {
13639
14379
  if (endpoint.kind !== "function") continue;
13640
14380
  if (endpoint.enabled === false) continue;
@@ -13644,20 +14384,22 @@ function findMatchingFunctionEndpoints(params) {
13644
14384
  const domainId = stringOrNull(endpoint.domain_id);
13645
14385
  if (domainId !== null && (params.inboundDomainId === null || domainId !== params.inboundDomainId)) continue;
13646
14386
  const functionId = stringOrNull(endpoint.function_id);
13647
- matches.push({
14387
+ const match = {
13648
14388
  function_id: functionId,
13649
14389
  id,
13650
14390
  is_current_function: functionId === params.currentFunctionId,
13651
- scope: domainId === null ? "catch-all" : "domain"
13652
- });
14391
+ scope: domainId === null ? "fallback" : "domain"
14392
+ };
14393
+ if (domainId === null) fallbackMatches.push(match);
14394
+ else domainMatches.push(match);
13653
14395
  }
13654
- return matches;
14396
+ return domainMatches.length > 0 ? domainMatches : fallbackMatches;
13655
14397
  }
13656
14398
  function formatFunctionEndpointNoiseWarning(params) {
13657
14399
  if (params.endpoints.filter((endpoint) => !endpoint.is_current_function).length === 0) return null;
13658
14400
  const lines = [`Warning: ${params.endpoints.length} function endpoints may receive mail for ${params.toAddress}:`];
13659
14401
  for (const endpoint of params.endpoints) {
13660
- const scope = endpoint.scope === "catch-all" ? "catch-all" : `scoped to ${params.inboundDomain}`;
14402
+ const scope = endpoint.scope === "fallback" ? "fallback" : `scoped to ${params.inboundDomain}`;
13661
14403
  const current = endpoint.is_current_function ? " (this function)" : "";
13662
14404
  const target = endpoint.function_id ? ` -> function ${endpoint.function_id}` : "";
13663
14405
  lines.push(`- endpoint ${endpoint.id}${target}, ${scope}${current}`);
@@ -13736,18 +14478,12 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
13736
14478
  const { flags } = await this.parse(FunctionsTestFunctionCommand);
13737
14479
  const shouldWait = flags.wait || flags["show-sends"];
13738
14480
  const shouldShowSends = flags["show-sends"];
13739
- const baseUrlOverridden = baseUrlOverriddenFromFlags(flags);
13740
- const auth = resolveCliAuth({
14481
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
13741
14482
  apiKey: flags["api-key"],
13742
14483
  apiBaseUrl1: flags["api-base-url-1"],
13743
14484
  apiBaseUrl2: flags["api-base-url-2"],
13744
14485
  configDir: this.config.configDir
13745
14486
  });
13746
- const apiClient = new PrimitiveApiClient({
13747
- apiKey: auth.apiKey,
13748
- apiBaseUrl1: auth.apiBaseUrl1,
13749
- apiBaseUrl2: auth.apiBaseUrl2
13750
- });
13751
14487
  await runWithTiming(flags.time, async () => {
13752
14488
  const triggerResult = await testFunction({
13753
14489
  client: apiClient.client,
@@ -13784,7 +14520,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
13784
14520
  const timeoutMs = flags.timeout * 1e3;
13785
14521
  const pollIntervalMs = flags["poll-interval"] * 1e3;
13786
14522
  const isExpired = () => flags.timeout > 0 && Date.now() - startedAt > timeoutMs;
13787
- this.log(`Waiting for test inbound to arrive at ${invocation.to}...`);
14523
+ writeFunctionTestProgress(`Waiting for test inbound to arrive at ${invocation.to}...`);
13788
14524
  let inboundId;
13789
14525
  while (!isExpired()) {
13790
14526
  const page = await fetchEmailSearchPage({
@@ -13813,7 +14549,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
13813
14549
  await sleep$1(pollIntervalMs);
13814
14550
  }
13815
14551
  if (!inboundId) this.error(`Timed out after ${flags.timeout}s waiting for test inbound ${invocation.to} to land. Browse ${invocation.watch_url} for the live view.`, { exit: 2 });
13816
- this.log(`Inbound landed (${inboundId}). Waiting for function to run...`);
14552
+ writeFunctionTestProgress(`Inbound landed (${inboundId}). Waiting for function to run...`);
13817
14553
  let detail;
13818
14554
  while (!isExpired()) {
13819
14555
  const result = await getEmail({
@@ -13887,11 +14623,16 @@ function retryAfterSeconds$1(result) {
13887
14623
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
13888
14624
  }
13889
14625
  async function checkExistingLogin(params) {
13890
- const baseUrlOverridden = params.apiBaseUrl1 !== void 0;
13891
- 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;
13892
14631
  const apiClient = new PrimitiveApiClient({
13893
14632
  apiKey: params.credentials.api_key,
13894
- apiBaseUrl1: probeApiBaseUrl1
14633
+ apiBaseUrl1: probeApiBaseUrl1,
14634
+ apiBaseUrl2: requestConfig.resolvedApiBaseUrl2,
14635
+ headers: requestConfig.headers
13895
14636
  });
13896
14637
  const result = await (params.checkAccount ?? ((client) => getAccount({
13897
14638
  client: client.client,
@@ -13907,7 +14648,7 @@ async function checkExistingLogin(params) {
13907
14648
  credentials: params.credentials,
13908
14649
  source: "stored"
13909
14650
  },
13910
- baseUrlOverridden,
14651
+ baseUrlOverridden: requestConfig.baseUrlOverridden,
13911
14652
  configDir: params.configDir,
13912
14653
  payload
13913
14654
  })) return { status: "removed_stale" };
@@ -13953,7 +14694,11 @@ var LoginCommand = class LoginCommand extends Command {
13953
14694
  }
13954
14695
  }
13955
14696
  async runWithCredentialLock(flags) {
13956
- 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;
13957
14702
  let existing;
13958
14703
  try {
13959
14704
  existing = loadCliCredentials(this.config.configDir);
@@ -13976,7 +14721,6 @@ var LoginCommand = class LoginCommand extends Command {
13976
14721
  throw cliError$2(existingStatus.message);
13977
14722
  } else throw cliError$2(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before logging in again.`);
13978
14723
  }
13979
- const apiClient = new PrimitiveApiClient({ apiBaseUrl1 });
13980
14724
  const started = await startCliLogin({
13981
14725
  body: { device_name: flags["device-name"] ?? hostname() },
13982
14726
  client: apiClient.client,
@@ -14086,10 +14830,15 @@ var LogoutCommand = class LogoutCommand extends Command {
14086
14830
  return;
14087
14831
  }
14088
14832
  if (!credentials) throw cliError$1("Not logged in. Run `primitive login` to create saved CLI credentials.");
14089
- 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;
14090
14838
  const apiClient = new PrimitiveApiClient({
14091
14839
  apiKey: credentials.api_key,
14092
- apiBaseUrl1
14840
+ apiBaseUrl1,
14841
+ headers: requestConfig.headers
14093
14842
  });
14094
14843
  const result = await cliLogout({
14095
14844
  body: { key_id: credentials.key_id },
@@ -14116,6 +14865,106 @@ var LogoutCommand = class LogoutCommand extends Command {
14116
14865
  }
14117
14866
  };
14118
14867
  //#endregion
14868
+ //#region src/oclif/message-body-sources.ts
14869
+ function defaultReadFile(path) {
14870
+ return readFileSync(path, "utf8");
14871
+ }
14872
+ function defaultReadStdin() {
14873
+ if (process.stdin.isTTY) throw new Error("stdin is a TTY; pipe a value into this command or pass a file/string source instead.");
14874
+ return readFileSync(0, "utf8");
14875
+ }
14876
+ function selectedSources(sources) {
14877
+ return sources.filter(([, selected]) => selected).map(([label]) => label);
14878
+ }
14879
+ function readTextFile(path, label, readFile) {
14880
+ try {
14881
+ return {
14882
+ content: readFile(path),
14883
+ kind: "ok"
14884
+ };
14885
+ } catch (error) {
14886
+ return {
14887
+ kind: "error",
14888
+ message: `Could not read ${label} ${path}: ${error instanceof Error ? error.message : String(error)}`
14889
+ };
14890
+ }
14891
+ }
14892
+ function readTextStdin(label, readStdin) {
14893
+ try {
14894
+ return {
14895
+ content: readStdin(),
14896
+ kind: "ok"
14897
+ };
14898
+ } catch (error) {
14899
+ return {
14900
+ kind: "error",
14901
+ message: `Could not read ${label}: ${error instanceof Error ? error.message : String(error)}`
14902
+ };
14903
+ }
14904
+ }
14905
+ function resolveMessageBodies(input) {
14906
+ const bodySources = selectedSources([
14907
+ ["--body", input.body !== void 0],
14908
+ ["--body-file", input.bodyFile !== void 0],
14909
+ ["--body-stdin", input.bodyStdin === true]
14910
+ ]);
14911
+ if (bodySources.length > 1) return {
14912
+ kind: "error",
14913
+ message: `Pass only one plain-text body source (got ${bodySources.join(", ")}).`
14914
+ };
14915
+ const htmlSources = selectedSources([
14916
+ ["--html", input.html !== void 0],
14917
+ ["--html-file", input.htmlFile !== void 0],
14918
+ ["--html-stdin", input.htmlStdin === true]
14919
+ ]);
14920
+ if (htmlSources.length > 1) return {
14921
+ kind: "error",
14922
+ message: `Pass only one HTML body source (got ${htmlSources.join(", ")}).`
14923
+ };
14924
+ const stdinSources = selectedSources([["--body-stdin", input.bodyStdin === true], ["--html-stdin", input.htmlStdin === true]]);
14925
+ if (stdinSources.length > 1) return {
14926
+ kind: "error",
14927
+ message: `Stdin can only be consumed once (got ${stdinSources.join(", ")}).`
14928
+ };
14929
+ if (bodySources.length === 0 && htmlSources.length === 0) return {
14930
+ kind: "error",
14931
+ message: "Either a plain-text body source or an HTML body source is required."
14932
+ };
14933
+ const readFile = input.readFile ?? defaultReadFile;
14934
+ const readStdin = input.readStdin ?? defaultReadStdin;
14935
+ let body = input.body;
14936
+ let html = input.html;
14937
+ if (input.bodyFile !== void 0) {
14938
+ const result = readTextFile(input.bodyFile, "--body-file", readFile);
14939
+ if (result.kind === "error") return result;
14940
+ body = result.content;
14941
+ }
14942
+ if (input.bodyStdin === true) {
14943
+ const result = readTextStdin("--body-stdin", readStdin);
14944
+ if (result.kind === "error") return result;
14945
+ body = result.content;
14946
+ }
14947
+ if (input.htmlFile !== void 0) {
14948
+ const result = readTextFile(input.htmlFile, "--html-file", readFile);
14949
+ if (result.kind === "error") return result;
14950
+ html = result.content;
14951
+ }
14952
+ if (input.htmlStdin === true) {
14953
+ const result = readTextStdin("--html-stdin", readStdin);
14954
+ if (result.kind === "error") return result;
14955
+ html = result.content;
14956
+ }
14957
+ if (!body && !html) return {
14958
+ kind: "error",
14959
+ message: "Either a non-empty plain-text body or a non-empty HTML body is required."
14960
+ };
14961
+ return {
14962
+ ...body !== void 0 ? { body } : {},
14963
+ ...html !== void 0 ? { html } : {},
14964
+ kind: "ok"
14965
+ };
14966
+ }
14967
+ //#endregion
14119
14968
  //#region src/oclif/commands/reply.ts
14120
14969
  var ReplyCommand = class ReplyCommand extends Command {
14121
14970
  static description = `Reply to an inbound email.
@@ -14124,6 +14973,7 @@ var ReplyCommand = class ReplyCommand extends Command {
14124
14973
  static summary = "Reply to an inbound email";
14125
14974
  static examples = [
14126
14975
  "<%= config.bin %> reply --id <inbound-email-id> --body 'Thanks, got it.'",
14976
+ "<%= config.bin %> reply --id <inbound-email-id> --body-file ./reply.txt",
14127
14977
  "<%= config.bin %> reply --id <inbound-email-id> --html '<p>Thanks, got it.</p>' --wait",
14128
14978
  "<%= config.bin %> reply --id <inbound-email-id> --from 'Support <support@example.com>' --body 'Thanks!'"
14129
14979
  ];
@@ -14147,7 +14997,11 @@ var ReplyCommand = class ReplyCommand extends Command {
14147
14997
  required: true
14148
14998
  }),
14149
14999
  body: Flags.string({ description: "Plain-text reply body. Either --body or --html (or both) is required." }),
15000
+ "body-file": Flags.string({ description: "Read the plain-text reply body from a UTF-8 file. Mutually exclusive with --body and --body-stdin." }),
15001
+ "body-stdin": Flags.boolean({ description: "Read the plain-text reply body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
14150
15002
  html: Flags.string({ description: "HTML reply body. Either --body or --html (or both) is required." }),
15003
+ "html-file": Flags.string({ description: "Read the HTML reply body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
15004
+ "html-stdin": Flags.boolean({ description: "Read the HTML reply body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
14151
15005
  from: Flags.string({ description: "Optional From header override. Defaults to the inbound recipient." }),
14152
15006
  wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the reply for delivery." }),
14153
15007
  "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
@@ -14155,24 +15009,26 @@ var ReplyCommand = class ReplyCommand extends Command {
14155
15009
  };
14156
15010
  async run() {
14157
15011
  const { flags } = await this.parse(ReplyCommand);
14158
- if (!flags.body && !flags.html) throw new Errors.CLIError("Either --body or --html (or both) is required.");
15012
+ const bodies = resolveMessageBodies({
15013
+ body: flags.body,
15014
+ bodyFile: flags["body-file"],
15015
+ bodyStdin: flags["body-stdin"],
15016
+ html: flags.html,
15017
+ htmlFile: flags["html-file"],
15018
+ htmlStdin: flags["html-stdin"]
15019
+ });
15020
+ if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
14159
15021
  await runWithTiming(flags.time, async () => {
14160
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
14161
- const auth = resolveCliAuth({
15022
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
14162
15023
  apiKey: flags["api-key"],
14163
15024
  apiBaseUrl1: flags["api-base-url-1"],
14164
15025
  apiBaseUrl2: flags["api-base-url-2"],
14165
15026
  configDir: this.config.configDir
14166
15027
  });
14167
- const apiClient = new PrimitiveApiClient({
14168
- apiKey: auth.apiKey,
14169
- apiBaseUrl1: auth.apiBaseUrl1,
14170
- apiBaseUrl2: auth.apiBaseUrl2
14171
- });
14172
15028
  const result = await replyToEmail({
14173
15029
  body: {
14174
- ...flags.body !== void 0 ? { body_text: flags.body } : {},
14175
- ...flags.html !== void 0 ? { body_html: flags.html } : {},
15030
+ ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
15031
+ ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
14176
15032
  ...flags.from !== void 0 ? { from: flags.from } : {},
14177
15033
  ...flags.wait !== void 0 ? { wait: flags.wait } : {},
14178
15034
  ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
@@ -14247,6 +15103,7 @@ var SendCommand = class SendCommand extends Command {
14247
15103
  static summary = "Send an email (simplified, agent-friendly)";
14248
15104
  static examples = [
14249
15105
  "<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
15106
+ "<%= config.bin %> send --to alice@example.com --body-file ./message.txt",
14250
15107
  "<%= config.bin %> send --to alice@example.com --from support@yourcompany.com --subject 'Quick question' --body 'Are you free Thursday?'",
14251
15108
  "<%= config.bin %> send --to alice@example.com --html '<p>Hello!</p>'",
14252
15109
  "<%= config.bin %> send --to alice@example.com --body 'Confirmed' --wait",
@@ -14274,7 +15131,11 @@ var SendCommand = class SendCommand extends Command {
14274
15131
  from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
14275
15132
  subject: Flags.string({ description: "Subject line. Defaults to the first line of --body / --html when omitted." }),
14276
15133
  body: Flags.string({ description: "Plain-text message body. Either --body or --html (or both) is required." }),
15134
+ "body-file": Flags.string({ description: "Read the plain-text message body from a UTF-8 file. Mutually exclusive with --body and --body-stdin." }),
15135
+ "body-stdin": Flags.boolean({ description: "Read the plain-text message body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
14277
15136
  html: Flags.string({ description: "HTML message body. Either --body or --html (or both) is required." }),
15137
+ "html-file": Flags.string({ description: "Read the HTML message body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
15138
+ "html-stdin": Flags.boolean({ description: "Read the HTML message body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
14278
15139
  "in-reply-to": Flags.string({ description: "Message-Id of the parent email when threading a reply on the wire. For replying to an inbound message you received, prefer `primitive reply --id <inbound-id>`." }),
14279
15140
  wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the message for delivery." }),
14280
15141
  "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
@@ -14282,34 +15143,36 @@ var SendCommand = class SendCommand extends Command {
14282
15143
  };
14283
15144
  async run() {
14284
15145
  const { flags } = await this.parse(SendCommand);
14285
- if (!flags.body && !flags.html) throw new Errors.CLIError("Either --body or --html (or both) is required.");
15146
+ const bodies = resolveMessageBodies({
15147
+ body: flags.body,
15148
+ bodyFile: flags["body-file"],
15149
+ bodyStdin: flags["body-stdin"],
15150
+ html: flags.html,
15151
+ htmlFile: flags["html-file"],
15152
+ htmlStdin: flags["html-stdin"]
15153
+ });
15154
+ if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
14286
15155
  await runWithTiming(flags.time, async () => {
14287
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
14288
- const auth = resolveCliAuth({
15156
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
14289
15157
  apiKey: flags["api-key"],
14290
15158
  apiBaseUrl1: flags["api-base-url-1"],
14291
15159
  apiBaseUrl2: flags["api-base-url-2"],
14292
15160
  configDir: this.config.configDir
14293
15161
  });
14294
- const apiClient = new PrimitiveApiClient({
14295
- apiKey: auth.apiKey,
14296
- apiBaseUrl1: auth.apiBaseUrl1,
14297
- apiBaseUrl2: auth.apiBaseUrl2
14298
- });
14299
15162
  const authFailureContext = {
14300
15163
  auth,
14301
15164
  baseUrlOverridden,
14302
15165
  configDir: this.config.configDir
14303
15166
  };
14304
15167
  const from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
14305
- const subject = flags.subject ?? (flags.body ? deriveSubject(flags.body) : "Message");
15168
+ const subject = flags.subject ?? (bodies.body ? deriveSubject(bodies.body) : "Message");
14306
15169
  const result = await sendEmail({
14307
15170
  body: {
14308
15171
  from,
14309
15172
  to: flags.to,
14310
15173
  subject,
14311
- ...flags.body !== void 0 ? { body_text: flags.body } : {},
14312
- ...flags.html !== void 0 ? { body_html: flags.html } : {},
15174
+ ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
15175
+ ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
14313
15176
  ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {},
14314
15177
  ...flags.wait !== void 0 ? { wait: flags.wait } : {},
14315
15178
  ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
@@ -14568,7 +15431,11 @@ async function runSignupWithCredentialLock(params) {
14568
15431
  const startFn = deps.startCliSignup ?? startCliSignup;
14569
15432
  const verifyFn = deps.verifyCliSignup ?? verifyCliSignup;
14570
15433
  const checkExistingLoginFn = deps.checkExistingLogin ?? checkExistingLogin;
14571
- 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;
14572
15439
  let existing;
14573
15440
  try {
14574
15441
  existing = loadCliCredentials(configDir);
@@ -14592,7 +15459,6 @@ async function runSignupWithCredentialLock(params) {
14592
15459
  } else throw cliError(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before creating a new account.`);
14593
15460
  }
14594
15461
  if (flags.force) deletePendingCliSignup(configDir);
14595
- const apiClient = new PrimitiveApiClient({ apiBaseUrl1 });
14596
15462
  let start = flags.force ? null : loadPendingCliSignup(configDir, apiBaseUrl1);
14597
15463
  const resumed = Boolean(start);
14598
15464
  if (start) process$1.stderr.write(`Continuing pending Primitive CLI signup for ${start.email}.\n`);
@@ -14749,19 +15615,14 @@ var WhoamiCommand = class WhoamiCommand extends Command {
14749
15615
  async run() {
14750
15616
  const { flags } = await this.parse(WhoamiCommand);
14751
15617
  await runWithTiming(flags.time, async () => {
14752
- const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
14753
- const auth = resolveCliAuth({
15618
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
14754
15619
  apiKey: flags["api-key"],
14755
15620
  apiBaseUrl1: flags["api-base-url-1"],
14756
15621
  apiBaseUrl2: flags["api-base-url-2"],
14757
15622
  configDir: this.config.configDir
14758
15623
  });
14759
15624
  const result = await getAccount({
14760
- client: new PrimitiveApiClient({
14761
- apiKey: auth.apiKey,
14762
- apiBaseUrl1: auth.apiBaseUrl1,
14763
- apiBaseUrl2: auth.apiBaseUrl2
14764
- }).client,
15625
+ client: apiClient.client,
14765
15626
  responseStyle: "fields"
14766
15627
  });
14767
15628
  if (result.error) {
@@ -14849,6 +15710,7 @@ function renderFishCompletion(binName) {
14849
15710
  lines.push(`complete -c ${binName} -f -n '__fish_${binName}_topic_needs_subcommand ${fishEscape(topic)}' -a '${fishEscape(operation.command)}' -d '${fishEscape(operation.summary ?? `${operation.method} ${operation.path}`)}'`);
14850
15711
  for (const parameter of [...operation.pathParams, ...operation.queryParams]) 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)}'`);
14851
15712
  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)'`);
15713
+ if (!operation.binaryResponse) lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'envelope' -d 'Print the full response envelope, including pagination metadata'`);
14852
15714
  if (operation.hasJsonBody) 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'`);
14853
15715
  if (operation.binaryResponse) lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'output' -r -d 'Write binary response bytes to a file'`);
14854
15716
  }
@@ -14966,7 +15828,6 @@ const CANONICAL_OPERATION_ALIASES = {
14966
15828
  "functions:get": "functions:get-function",
14967
15829
  "functions:list": "functions:list-functions",
14968
15830
  "functions:list-secrets": "functions:list-function-secrets",
14969
- "functions:logs": "functions:list-function-logs",
14970
15831
  "sending:get": "sending:get-sent-email",
14971
15832
  "sending:list": "sending:list-sent-emails",
14972
15833
  "sending:permissions": "sending:get-send-permissions",
@@ -14979,6 +15840,7 @@ const CANONICAL_OPERATION_ALIASES = {
14979
15840
  };
14980
15841
  const DESCRIBE_OPERATION_ALIASES = {
14981
15842
  ...CANONICAL_OPERATION_ALIASES,
15843
+ "functions:logs": "functions:list-function-logs",
14982
15844
  reply: "sending:reply-to-email"
14983
15845
  };
14984
15846
  function resolveOperationAlias(id) {
@@ -14989,6 +15851,10 @@ const generatedCommands = Object.fromEntries(operationManifest.filter((operation
14989
15851
  const COMMANDS = {
14990
15852
  completion: CompletionCommand,
14991
15853
  "list-operations": ListOperationsCommand,
15854
+ "config:list": ConfigListCommand,
15855
+ "config:reset": ConfigResetCommand,
15856
+ "config:set": ConfigSetCommand,
15857
+ "config:use": ConfigUseCommand,
14992
15858
  describe: DescribeCommand,
14993
15859
  send: SendCommand,
14994
15860
  reply: ReplyCommand,
@@ -15012,7 +15878,8 @@ const COMMANDS = {
15012
15878
  if (!command) throw new Error(`Missing generated command target for alias ${alias}`);
15013
15879
  return [alias, command];
15014
15880
  })),
15015
- ...generatedCommands
15881
+ ...generatedCommands,
15882
+ "functions:logs": FunctionsLogsCommand
15016
15883
  };
15017
15884
  //#endregion
15018
15885
  export { CANONICAL_OPERATION_ALIASES, COMMANDS, lookupOperation };