@primitivedotdev/cli 0.26.4 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +24 -4
  2. package/dist/oclif/index.js +2629 -212
  3. package/package.json +12 -6
@@ -4,6 +4,8 @@ import { randomUUID } from "node:crypto";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { spawn } from "node:child_process";
6
6
  import { hostname } from "node:os";
7
+ import process$1 from "node:process";
8
+ import { createInterface } from "node:readline/promises";
7
9
  //#region \0rolldown/runtime.js
8
10
  var __defProp = Object.defineProperty;
9
11
  var __exportAll = (all, no_symbols) => {
@@ -647,6 +649,7 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
647
649
  getAccount: () => getAccount,
648
650
  getEmail: () => getEmail,
649
651
  getFunction: () => getFunction,
652
+ getFunctionTestRunTrace: () => getFunctionTestRunTrace,
650
653
  getSendPermissions: () => getSendPermissions,
651
654
  getSentEmail: () => getSentEmail,
652
655
  getStorageStats: () => getStorageStats,
@@ -664,11 +667,13 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
664
667
  replayDelivery: () => replayDelivery,
665
668
  replayEmailWebhooks: () => replayEmailWebhooks,
666
669
  replyToEmail: () => replyToEmail,
670
+ resendCliSignupVerification: () => resendCliSignupVerification,
667
671
  rotateWebhookSecret: () => rotateWebhookSecret,
668
672
  searchEmails: () => searchEmails,
669
673
  sendEmail: () => sendEmail,
670
674
  setFunctionSecret: () => setFunctionSecret,
671
675
  startCliLogin: () => startCliLogin,
676
+ startCliSignup: () => startCliSignup,
672
677
  testEndpoint: () => testEndpoint,
673
678
  testFunction: () => testFunction,
674
679
  updateAccount: () => updateAccount,
@@ -676,6 +681,7 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
676
681
  updateEndpoint: () => updateEndpoint,
677
682
  updateFilter: () => updateFilter,
678
683
  updateFunction: () => updateFunction,
684
+ verifyCliSignup: () => verifyCliSignup,
679
685
  verifyDomain: () => verifyDomain
680
686
  });
681
687
  /**
@@ -711,6 +717,54 @@ const pollCliLogin = (options) => (options.client ?? client).post({
711
717
  }
712
718
  });
713
719
  /**
720
+ * Start CLI account signup
721
+ *
722
+ * Starts a terminal-native CLI signup. The API validates the signup code,
723
+ * creates a pending signup session, sends an email verification code, and
724
+ * returns an opaque signup token used by the resend and verify steps. This
725
+ * endpoint does not require an API key.
726
+ *
727
+ */
728
+ const startCliSignup = (options) => (options.client ?? client).post({
729
+ url: "/cli/signup/start",
730
+ ...options,
731
+ headers: {
732
+ ...options.body !== void 0 && { "Content-Type": "application/json" },
733
+ ...options.headers
734
+ }
735
+ });
736
+ /**
737
+ * Resend CLI signup verification code
738
+ *
739
+ * Sends a new email verification code for a pending CLI signup session.
740
+ * This endpoint does not require an API key.
741
+ *
742
+ */
743
+ const resendCliSignupVerification = (options) => (options.client ?? client).post({
744
+ url: "/cli/signup/resend",
745
+ ...options,
746
+ headers: {
747
+ ...options.body !== void 0 && { "Content-Type": "application/json" },
748
+ ...options.headers
749
+ }
750
+ });
751
+ /**
752
+ * Verify CLI signup and create API key
753
+ *
754
+ * Verifies the email code for a CLI signup session, creates the account,
755
+ * redeems the reserved signup code, mints an org-scoped CLI API key, and
756
+ * returns the raw key exactly once. This endpoint does not require an API key.
757
+ *
758
+ */
759
+ const verifyCliSignup = (options) => (options.client ?? client).post({
760
+ url: "/cli/signup/verify",
761
+ ...options,
762
+ headers: {
763
+ ...options.body !== void 0 && { "Content-Type": "application/json" },
764
+ ...options.headers
765
+ }
766
+ });
767
+ /**
714
768
  * Revoke the current CLI API key
715
769
  *
716
770
  * Revokes the API key used to authenticate the request. CLI clients use
@@ -1451,8 +1505,9 @@ const listFunctions = (options) => (options?.client ?? client).get({
1451
1505
  * than relying on external imports.
1452
1506
  *
1453
1507
  * **Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`
1454
- * (optional) is capped at 5 MiB UTF-8 and is stored only on the
1455
- * edge runtime side; it is not persisted in Primitive's database.
1508
+ * (optional) is capped at 5 MiB UTF-8, stored with each deployment
1509
+ * attempt, and sent to the runtime so stack traces can resolve to
1510
+ * original source files.
1456
1511
  *
1457
1512
  * **Auto-wiring.** On successful deploy, Primitive automatically
1458
1513
  * creates a webhook endpoint that delivers inbound mail to the
@@ -1525,11 +1580,10 @@ const getFunction = (options) => (options.client ?? client).get({
1525
1580
  * passing the same `code` re-runs the deploy and refreshes the
1526
1581
  * binding set with the latest values from the secrets table.
1527
1582
  *
1528
- * On a 502 deploy failure, the previously-deployed code stays
1529
- * live; the runtime never serves a half-built bundle. The
1530
- * `deploy_error` field on the returned record carries the error
1531
- * that came back from the runtime so you can surface it to users
1532
- * without polling.
1583
+ * On deploy failure, the previously-deployed code stays live; the
1584
+ * runtime never serves a half-built bundle. The response uses
1585
+ * `error.code` `deploy_failed`, and the function's `deploy_error`
1586
+ * field carries the latest deploy error for dashboard/API reads.
1533
1587
  *
1534
1588
  */
1535
1589
  const updateFunction = (options) => (options.client ?? client).put({
@@ -1582,6 +1636,24 @@ const testFunction = (options) => (options.client ?? client).post({
1582
1636
  }
1583
1637
  });
1584
1638
  /**
1639
+ * Get a function test run trace
1640
+ *
1641
+ * Returns the current end-to-end trace for a function test run.
1642
+ * The trace is intentionally partial while the test is still in
1643
+ * flight: callers can poll this endpoint and watch it fill in
1644
+ * from send -> inbound -> webhook deliveries -> outbound
1645
+ * requests, logs, and replies.
1646
+ *
1647
+ */
1648
+ const getFunctionTestRunTrace = (options) => (options.client ?? client).get({
1649
+ security: [{
1650
+ scheme: "bearer",
1651
+ type: "http"
1652
+ }],
1653
+ url: "/functions/{id}/test-runs/{run_id}/trace",
1654
+ ...options
1655
+ });
1656
+ /**
1585
1657
  * List a function's secrets
1586
1658
  *
1587
1659
  * Returns metadata for every secret bound to the function, with
@@ -1705,7 +1777,7 @@ const openapiDocument = {
1705
1777
  "info": {
1706
1778
  "title": "Primitive API",
1707
1779
  "version": "1.0.0",
1708
- "description": "The Primitive API lets you manage domains, emails, webhook endpoints,\nfilters, and account settings programmatically.\n\n## Authentication\n\nAll endpoints require a Bearer token in the `Authorization` header:\n\n```\nAuthorization: Bearer prim_<your_api_key>\n```\n\nAPI keys are org-scoped. Create and manage them in your dashboard\nunder Settings > API Keys.\n\n## Rate Limiting\n\nThe API enforces a sliding window rate limit of **120 requests per\n60 seconds** per organization. When exceeded, the API returns `429`\nwith a `Retry-After` header indicating how many seconds to wait.\n\n## Pagination\n\nList endpoints use cursor-based pagination. Responses include a\n`meta` object with `total`, `limit`, and `cursor` fields. Pass the\n`cursor` value as a query parameter to fetch the next page. When\n`cursor` is `null`, there are no more results.\n\n## Response Format\n\nAll responses use a consistent envelope:\n\n```json\n{\n \"success\": true,\n \"data\": { ... },\n \"meta\": { \"total\": 42, \"limit\": 50, \"cursor\": \"...\" }\n}\n```\n\nErrors follow the same pattern:\n\n```json\n{\n \"success\": false,\n \"error\": { \"code\": \"not_found\", \"message\": \"Email not found\" }\n}\n```\n\n## Webhook signing\n\nOutbound webhook deliveries (configured via the `endpoints` API)\nare signed so receivers can verify they came from Primitive and\nhave not been tampered with in transit. The signing scheme is\ndeliberately simple so it can be reimplemented in any language\nin a few lines. The Node SDK's `verifyWebhookSignature` helper\nis the reference implementation; the wire details below let you\nwrite a verifier in Python, Go, Ruby, etc. without reading our\nsource.\n\n**Header**: `Primitive-Signature: t=<unix-seconds>,v1=<hex>`\n\nA legacy `MyMX-Signature` header is also sent on every delivery\nwith the same value, retained for back-compatibility with\nintegrations written before the rename. New code should read\n`Primitive-Signature`.\n\n**Signed string**: `${timestamp}.${rawBody}` where `timestamp`\nis the Unix-seconds integer from the `t=` parameter and\n`rawBody` is the exact bytes of the HTTP request body BEFORE\nany JSON decoding. Verify against the raw body, not a\nre-serialized parse, or you will silently mismatch on\ninsignificant whitespace.\n\n**Signature**: HMAC-SHA256 of the signed string, hex-encoded\n(lowercase). Use the account's webhook secret as the HMAC key,\nas a UTF-8 byte sequence.\n\n**Secret**: returned by `GET /account/webhook-secret`. The\nstring looks base64-shaped (e.g. `XNHBBW8VqoBjRfNs1tkZj11jTk...`)\nbut is NOT base64; use it AS-IS as a UTF-8 string for the HMAC\nkey. Base64-decoding before HMAC will silently produce\nmismatched signatures.\n\n**Tolerance**: by convention, reject deliveries whose `t=`\ntimestamp is more than 5 minutes off your wall-clock to defend\nagainst replay attacks. The Node SDK's helper enforces this by\ndefault.\n\n**Verification recipe** (any language):\n\n```\n1. Read the raw HTTP body (do not parse).\n2. Read `Primitive-Signature: t=<ts>,v1=<sig>`.\n3. Reject if abs(now - ts) > 300 seconds.\n4. expected = HMAC_SHA256_hex(secret_utf8, f\"{ts}.{rawBody}\")\n5. Constant-time compare expected to sig. Reject if not equal.\n```\n\nFor Node, use `verifyWebhookSignature` from\n`@primitivedotdev/sdk/webhook` (or the higher-level\n`handleWebhook` helper if you want a one-liner). For other\nlanguages, the recipe above is everything you need.\n\nTest deliveries: `POST /endpoints/{id}/test` triggers a fake\ndelivery to your endpoint URL, signed with your real account\nsecret, so you can confirm verification end-to-end without\nneeding real inbound mail. The test response carries the exact\n`signature` header value sent on the wire so you can compare\nstrings directly.\n",
1780
+ "description": "The Primitive API lets you manage domains, emails, webhook endpoints,\nfilters, and account settings programmatically.\n\n## Authentication\n\nMost endpoints require a Bearer token in the `Authorization` header:\n\n```\nAuthorization: Bearer prim_<your_api_key>\n```\n\nAPI keys are org-scoped. Create and manage them in your dashboard\nunder Settings > API Keys. CLI login and signup endpoints explicitly\ndeclare `security: []`; they do not require an API key because they\nare used to mint one.\n\n## Rate Limiting\n\nThe API enforces a sliding window rate limit of **120 requests per\n60 seconds** per organization. When exceeded, the API returns `429`\nwith a `Retry-After` header indicating how many seconds to wait.\n\n## Pagination\n\nList endpoints use cursor-based pagination. Responses include a\n`meta` object with `total`, `limit`, and `cursor` fields. Pass the\n`cursor` value as a query parameter to fetch the next page. When\n`cursor` is `null`, there are no more results.\n\n## Response Format\n\nAll responses use a consistent envelope:\n\n```json\n{\n \"success\": true,\n \"data\": { ... },\n \"meta\": { \"total\": 42, \"limit\": 50, \"cursor\": \"...\" }\n}\n```\n\nErrors follow the same pattern:\n\n```json\n{\n \"success\": false,\n \"error\": { \"code\": \"not_found\", \"message\": \"Email not found\" }\n}\n```\n\n## Webhook signing\n\nOutbound webhook deliveries (configured via the `endpoints` API)\nare signed so receivers can verify they came from Primitive and\nhave not been tampered with in transit. The signing scheme is\ndeliberately simple so it can be reimplemented in any language\nin a few lines. The Node SDK's `verifyWebhookSignature` helper\nis the reference implementation; the wire details below let you\nwrite a verifier in Python, Go, Ruby, etc. without reading our\nsource.\n\n**Header**: `Primitive-Signature: t=<unix-seconds>,v1=<hex>`\n\nA legacy `MyMX-Signature` header is also sent on every delivery\nwith the same value, retained for back-compatibility with\nintegrations written before the rename. New code should read\n`Primitive-Signature`.\n\n**Signed string**: `${timestamp}.${rawBody}` where `timestamp`\nis the Unix-seconds integer from the `t=` parameter and\n`rawBody` is the exact bytes of the HTTP request body BEFORE\nany JSON decoding. Verify against the raw body, not a\nre-serialized parse, or you will silently mismatch on\ninsignificant whitespace.\n\n**Signature**: HMAC-SHA256 of the signed string, hex-encoded\n(lowercase). Use the account's webhook secret as the HMAC key,\nas a UTF-8 byte sequence.\n\n**Secret**: returned by `GET /account/webhook-secret`. The\nstring looks base64-shaped (e.g. `XNHBBW8VqoBjRfNs1tkZj11jTk...`)\nbut is NOT base64; use it AS-IS as a UTF-8 string for the HMAC\nkey. Base64-decoding before HMAC will silently produce\nmismatched signatures.\n\n**Tolerance**: by convention, reject deliveries whose `t=`\ntimestamp is more than 5 minutes off your wall-clock to defend\nagainst replay attacks. The Node SDK's helper enforces this by\ndefault.\n\n**Verification recipe** (any language):\n\n```\n1. Read the raw HTTP body (do not parse).\n2. Read `Primitive-Signature: t=<ts>,v1=<sig>`.\n3. Reject if abs(now - ts) > 300 seconds.\n4. expected = HMAC_SHA256_hex(secret_utf8, f\"{ts}.{rawBody}\")\n5. Constant-time compare expected to sig. Reject if not equal.\n```\n\nFor Node, use `verifyWebhookSignature` from\n`@primitivedotdev/sdk/webhook` (or the higher-level\n`handleWebhook` helper if you want a one-liner). For other\nlanguages, the recipe above is everything you need.\n\nTest deliveries: `POST /endpoints/{id}/test` triggers a fake\ndelivery to your endpoint URL, signed with your real account\nsecret, so you can confirm verification end-to-end without\nneeding real inbound mail. The test response carries the exact\n`signature` header value sent on the wire so you can compare\nstrings directly.\n",
1709
1781
  "contact": {
1710
1782
  "name": "Primitive",
1711
1783
  "url": "https://primitive.dev"
@@ -1877,6 +1949,97 @@ const openapiDocument = {
1877
1949
  }
1878
1950
  }
1879
1951
  } },
1952
+ "/cli/signup/start": { "post": {
1953
+ "operationId": "startCliSignup",
1954
+ "summary": "Start CLI account signup",
1955
+ "description": "Starts a terminal-native CLI signup. The API validates the signup code,\ncreates a pending signup session, sends an email verification code, and\nreturns an opaque signup token used by the resend and verify steps. This\nendpoint does not require an API key.\n",
1956
+ "tags": ["CLI"],
1957
+ "security": [],
1958
+ "requestBody": {
1959
+ "required": true,
1960
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StartCliSignupInput" } } }
1961
+ },
1962
+ "responses": {
1963
+ "201": {
1964
+ "description": "CLI signup session created and verification email sent",
1965
+ "headers": { "Cache-Control": {
1966
+ "schema": { "type": "string" },
1967
+ "description": "Always `no-store`"
1968
+ } },
1969
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
1970
+ "type": "object",
1971
+ "properties": { "data": { "$ref": "#/components/schemas/CliSignupStartResult" } }
1972
+ }] } } }
1973
+ },
1974
+ "400": { "$ref": "#/components/responses/ValidationError" },
1975
+ "429": { "$ref": "#/components/responses/RateLimited" }
1976
+ }
1977
+ } },
1978
+ "/cli/signup/resend": { "post": {
1979
+ "operationId": "resendCliSignupVerification",
1980
+ "summary": "Resend CLI signup verification code",
1981
+ "description": "Sends a new email verification code for a pending CLI signup session.\nThis endpoint does not require an API key.\n",
1982
+ "tags": ["CLI"],
1983
+ "security": [],
1984
+ "requestBody": {
1985
+ "required": true,
1986
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResendCliSignupVerificationInput" } } }
1987
+ },
1988
+ "responses": {
1989
+ "200": {
1990
+ "description": "Verification email resent",
1991
+ "headers": { "Cache-Control": {
1992
+ "schema": { "type": "string" },
1993
+ "description": "Always `no-store`"
1994
+ } },
1995
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
1996
+ "type": "object",
1997
+ "properties": { "data": { "$ref": "#/components/schemas/CliSignupResendResult" } }
1998
+ }] } } }
1999
+ },
2000
+ "400": {
2001
+ "description": "Invalid token or expired token",
2002
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
2003
+ },
2004
+ "429": {
2005
+ "description": "Global rate limit exceeded or resend requested too quickly",
2006
+ "headers": { "Retry-After": {
2007
+ "schema": { "type": "integer" },
2008
+ "description": "Seconds to wait before retrying"
2009
+ } },
2010
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
2011
+ }
2012
+ }
2013
+ } },
2014
+ "/cli/signup/verify": { "post": {
2015
+ "operationId": "verifyCliSignup",
2016
+ "summary": "Verify CLI signup and create API key",
2017
+ "description": "Verifies the email code for a CLI signup session, creates the account,\nredeems the reserved signup code, mints an org-scoped CLI API key, and\nreturns the raw key exactly once. This endpoint does not require an API key.\n",
2018
+ "tags": ["CLI"],
2019
+ "security": [],
2020
+ "requestBody": {
2021
+ "required": true,
2022
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifyCliSignupInput" } } }
2023
+ },
2024
+ "responses": {
2025
+ "200": {
2026
+ "description": "CLI signup verified and API key created",
2027
+ "headers": { "Cache-Control": {
2028
+ "schema": { "type": "string" },
2029
+ "description": "Always `no-store`"
2030
+ } },
2031
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
2032
+ "type": "object",
2033
+ "properties": { "data": { "$ref": "#/components/schemas/CliSignupVerifyResult" } }
2034
+ }] } } }
2035
+ },
2036
+ "400": {
2037
+ "description": "Invalid request, invalid verification code, expired token, invalid signup code, rejected password, or account creation failure",
2038
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
2039
+ },
2040
+ "429": { "$ref": "#/components/responses/RateLimited" }
2041
+ }
2042
+ } },
1880
2043
  "/cli/logout": { "post": {
1881
2044
  "operationId": "cliLogout",
1882
2045
  "summary": "Revoke the current CLI API key",
@@ -3017,7 +3180,7 @@ const openapiDocument = {
3017
3180
  "post": {
3018
3181
  "operationId": "createFunction",
3019
3182
  "summary": "Deploy a function",
3020
- "description": "Creates and deploys a new function. The handler must be a single\nESM module whose default export is an object with an async\n`fetch(request, env)` method (Workers-style). The gateway\nHMAC-verifies the POST against the org's webhook secret before\ninvoking the handler; the request body parses to an\n`email.received` event (see `EmailReceivedEvent` and the\nWebhook payload section for the full schema). Code is bundled\nbefore being uploaded; ship a single self-contained file rather\nthan relying on external imports.\n\n**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`\n(optional) is capped at 5 MiB UTF-8 and is stored only on the\nedge runtime side; it is not persisted in Primitive's database.\n\n**Auto-wiring.** On successful deploy, Primitive automatically\ncreates a webhook endpoint that delivers inbound mail to the\nfunction. There is nothing to configure on the Endpoints API\nfor this to work; the gateway URL returned here is for\nreference only and is not directly callable from outside.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`) already\nbound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
3183
+ "description": "Creates and deploys a new function. The handler must be a single\nESM module whose default export is an object with an async\n`fetch(request, env)` method (Workers-style). The gateway\nHMAC-verifies the POST against the org's webhook secret before\ninvoking the handler; the request body parses to an\n`email.received` event (see `EmailReceivedEvent` and the\nWebhook payload section for the full schema). Code is bundled\nbefore being uploaded; ship a single self-contained file rather\nthan relying on external imports.\n\n**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`\n(optional) is capped at 5 MiB UTF-8, stored with each deployment\nattempt, and sent to the runtime so stack traces can resolve to\noriginal source files.\n\n**Auto-wiring.** On successful deploy, Primitive automatically\ncreates a webhook endpoint that delivers inbound mail to the\nfunction. There is nothing to configure on the Endpoints API\nfor this to work; the gateway URL returned here is for\nreference only and is not directly callable from outside.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`) already\nbound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
3021
3184
  "tags": ["Functions"],
3022
3185
  "requestBody": {
3023
3186
  "required": true,
@@ -3031,13 +3194,18 @@ const openapiDocument = {
3031
3194
  "properties": { "data": { "$ref": "#/components/schemas/CreateFunctionResult" } }
3032
3195
  }] } } }
3033
3196
  },
3034
- "400": { "$ref": "#/components/responses/ValidationError" },
3197
+ "400": {
3198
+ "description": "Invalid request parameters or customer-correctable deploy rejection",
3199
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
3200
+ },
3035
3201
  "401": { "$ref": "#/components/responses/Unauthorized" },
3036
3202
  "409": {
3037
3203
  "description": "A function with this name already exists in the org",
3038
3204
  "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
3039
3205
  },
3040
- "502": { "$ref": "#/components/responses/BadGateway" }
3206
+ "424": { "$ref": "#/components/responses/DeployFailed" },
3207
+ "429": { "$ref": "#/components/responses/DeployFailed" },
3208
+ "503": { "$ref": "#/components/responses/DeployFailed" }
3041
3209
  }
3042
3210
  }
3043
3211
  },
@@ -3063,7 +3231,7 @@ const openapiDocument = {
3063
3231
  "put": {
3064
3232
  "operationId": "updateFunction",
3065
3233
  "summary": "Update and redeploy a function",
3066
- "description": "Replaces the function's source code with the body's `code` and\ntriggers a redeploy. Same size limits as `POST /functions`.\nUse this verb to push secret writes into the running handler:\npassing the same `code` re-runs the deploy and refreshes the\nbinding set with the latest values from the secrets table.\n\nOn a 502 deploy failure, the previously-deployed code stays\nlive; the runtime never serves a half-built bundle. The\n`deploy_error` field on the returned record carries the error\nthat came back from the runtime so you can surface it to users\nwithout polling.\n",
3234
+ "description": "Replaces the function's source code with the body's `code` and\ntriggers a redeploy. Same size limits as `POST /functions`.\nUse this verb to push secret writes into the running handler:\npassing the same `code` re-runs the deploy and refreshes the\nbinding set with the latest values from the secrets table.\n\nOn deploy failure, the previously-deployed code stays live; the\nruntime never serves a half-built bundle. The response uses\n`error.code` `deploy_failed`, and the function's `deploy_error`\nfield carries the latest deploy error for dashboard/API reads.\n",
3067
3235
  "tags": ["Functions"],
3068
3236
  "requestBody": {
3069
3237
  "required": true,
@@ -3077,10 +3245,15 @@ const openapiDocument = {
3077
3245
  "properties": { "data": { "$ref": "#/components/schemas/FunctionDetail" } }
3078
3246
  }] } } }
3079
3247
  },
3080
- "400": { "$ref": "#/components/responses/ValidationError" },
3248
+ "400": {
3249
+ "description": "Invalid request parameters or customer-correctable deploy rejection",
3250
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
3251
+ },
3081
3252
  "401": { "$ref": "#/components/responses/Unauthorized" },
3082
3253
  "404": { "$ref": "#/components/responses/NotFound" },
3083
- "502": { "$ref": "#/components/responses/BadGateway" }
3254
+ "424": { "$ref": "#/components/responses/DeployFailed" },
3255
+ "429": { "$ref": "#/components/responses/DeployFailed" },
3256
+ "503": { "$ref": "#/components/responses/DeployFailed" }
3084
3257
  }
3085
3258
  },
3086
3259
  "delete": {
@@ -3140,6 +3313,37 @@ const openapiDocument = {
3140
3313
  }
3141
3314
  }
3142
3315
  },
3316
+ "/functions/{id}/test-runs/{run_id}/trace": {
3317
+ "parameters": [{ "$ref": "#/components/parameters/ResourceId" }, {
3318
+ "name": "run_id",
3319
+ "in": "path",
3320
+ "required": true,
3321
+ "description": "Function test run id returned by POST /functions/{id}/test.",
3322
+ "schema": {
3323
+ "type": "string",
3324
+ "format": "uuid"
3325
+ }
3326
+ }],
3327
+ "get": {
3328
+ "operationId": "getFunctionTestRunTrace",
3329
+ "summary": "Get a function test run trace",
3330
+ "description": "Returns the current end-to-end trace for a function test run.\nThe trace is intentionally partial while the test is still in\nflight: callers can poll this endpoint and watch it fill in\nfrom send -> inbound -> webhook deliveries -> outbound\nrequests, logs, and replies.\n",
3331
+ "tags": ["Functions"],
3332
+ "responses": {
3333
+ "200": {
3334
+ "description": "Function test run trace",
3335
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
3336
+ "type": "object",
3337
+ "properties": { "data": { "$ref": "#/components/schemas/FunctionTestRunTrace" } }
3338
+ }] } } }
3339
+ },
3340
+ "400": { "$ref": "#/components/responses/ValidationError" },
3341
+ "401": { "$ref": "#/components/responses/Unauthorized" },
3342
+ "403": { "$ref": "#/components/responses/Forbidden" },
3343
+ "404": { "$ref": "#/components/responses/NotFound" }
3344
+ }
3345
+ }
3346
+ },
3143
3347
  "/functions/{id}/secrets": {
3144
3348
  "parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
3145
3349
  "get": {
@@ -3399,6 +3603,19 @@ const openapiDocument = {
3399
3603
  }
3400
3604
  } }
3401
3605
  },
3606
+ "DeployFailed": {
3607
+ "description": "Function deploy could not be completed; previously deployed code remains live",
3608
+ "content": { "application/json": {
3609
+ "schema": { "$ref": "#/components/schemas/ErrorResponse" },
3610
+ "example": {
3611
+ "success": false,
3612
+ "error": {
3613
+ "code": "deploy_failed",
3614
+ "message": "Function deploy failed"
3615
+ }
3616
+ }
3617
+ } }
3618
+ },
3402
3619
  "RateLimited": {
3403
3620
  "description": "Rate limit exceeded",
3404
3621
  "headers": { "Retry-After": {
@@ -3758,6 +3975,162 @@ const openapiDocument = {
3758
3975
  "org_name"
3759
3976
  ]
3760
3977
  },
3978
+ "StartCliSignupInput": {
3979
+ "type": "object",
3980
+ "additionalProperties": false,
3981
+ "properties": {
3982
+ "email": {
3983
+ "type": "string",
3984
+ "format": "email",
3985
+ "maxLength": 254
3986
+ },
3987
+ "signup_code": {
3988
+ "type": "string",
3989
+ "minLength": 1,
3990
+ "maxLength": 128
3991
+ },
3992
+ "terms_accepted": {
3993
+ "type": "boolean",
3994
+ "const": true,
3995
+ "description": "Must be true to confirm acceptance of Primitive's Terms of Service and Privacy Policy"
3996
+ },
3997
+ "device_name": {
3998
+ "type": "string",
3999
+ "minLength": 1,
4000
+ "maxLength": 80,
4001
+ "description": "Human-readable device name used for the created CLI API key"
4002
+ },
4003
+ "metadata": {
4004
+ "type": "object",
4005
+ "additionalProperties": true,
4006
+ "description": "Optional client metadata stored with the signup session; serialized JSON must be 2048 bytes or fewer"
4007
+ }
4008
+ },
4009
+ "required": [
4010
+ "email",
4011
+ "signup_code",
4012
+ "terms_accepted"
4013
+ ]
4014
+ },
4015
+ "CliSignupStartResult": {
4016
+ "type": "object",
4017
+ "properties": {
4018
+ "signup_token": {
4019
+ "type": "string",
4020
+ "description": "Opaque token used to verify or resend the pending CLI signup"
4021
+ },
4022
+ "email": {
4023
+ "type": "string",
4024
+ "format": "email"
4025
+ },
4026
+ "expires_in": {
4027
+ "type": "integer",
4028
+ "description": "Seconds until the pending signup expires"
4029
+ },
4030
+ "resend_after": {
4031
+ "type": "integer",
4032
+ "description": "Minimum seconds before requesting another verification email"
4033
+ },
4034
+ "verification_code_length": {
4035
+ "type": "integer",
4036
+ "description": "Number of digits in the emailed verification code"
4037
+ }
4038
+ },
4039
+ "required": [
4040
+ "signup_token",
4041
+ "email",
4042
+ "expires_in",
4043
+ "resend_after",
4044
+ "verification_code_length"
4045
+ ]
4046
+ },
4047
+ "ResendCliSignupVerificationInput": {
4048
+ "type": "object",
4049
+ "additionalProperties": false,
4050
+ "properties": { "signup_token": {
4051
+ "type": "string",
4052
+ "minLength": 1
4053
+ } },
4054
+ "required": ["signup_token"]
4055
+ },
4056
+ "CliSignupResendResult": {
4057
+ "type": "object",
4058
+ "properties": {
4059
+ "email": {
4060
+ "type": "string",
4061
+ "format": "email"
4062
+ },
4063
+ "expires_in": {
4064
+ "type": "integer",
4065
+ "description": "Seconds until the pending signup expires"
4066
+ },
4067
+ "resend_after": {
4068
+ "type": "integer",
4069
+ "description": "Minimum seconds before requesting another verification email"
4070
+ },
4071
+ "verification_code_length": {
4072
+ "type": "integer",
4073
+ "description": "Number of digits in the emailed verification code"
4074
+ }
4075
+ },
4076
+ "required": [
4077
+ "email",
4078
+ "expires_in",
4079
+ "resend_after",
4080
+ "verification_code_length"
4081
+ ]
4082
+ },
4083
+ "VerifyCliSignupInput": {
4084
+ "type": "object",
4085
+ "additionalProperties": false,
4086
+ "properties": {
4087
+ "signup_token": {
4088
+ "type": "string",
4089
+ "minLength": 1
4090
+ },
4091
+ "verification_code": {
4092
+ "type": "string",
4093
+ "minLength": 1,
4094
+ "maxLength": 32
4095
+ },
4096
+ "password": {
4097
+ "type": "string",
4098
+ "minLength": 1,
4099
+ "maxLength": 1024
4100
+ }
4101
+ },
4102
+ "required": [
4103
+ "signup_token",
4104
+ "verification_code",
4105
+ "password"
4106
+ ]
4107
+ },
4108
+ "CliSignupVerifyResult": {
4109
+ "type": "object",
4110
+ "properties": {
4111
+ "api_key": {
4112
+ "type": "string",
4113
+ "description": "Newly-created API key for CLI authentication"
4114
+ },
4115
+ "key_id": {
4116
+ "type": "string",
4117
+ "format": "uuid"
4118
+ },
4119
+ "key_prefix": { "type": "string" },
4120
+ "org_id": {
4121
+ "type": "string",
4122
+ "format": "uuid"
4123
+ },
4124
+ "org_name": { "type": ["string", "null"] }
4125
+ },
4126
+ "required": [
4127
+ "api_key",
4128
+ "key_id",
4129
+ "key_prefix",
4130
+ "org_id",
4131
+ "org_name"
4132
+ ]
4133
+ },
3761
4134
  "CliLogoutInput": {
3762
4135
  "type": "object",
3763
4136
  "additionalProperties": false,
@@ -5224,7 +5597,7 @@ const openapiDocument = {
5224
5597
  "type": "string",
5225
5598
  "minLength": 1,
5226
5599
  "maxLength": 5242880,
5227
- "description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored only on the runtime side (not in Primitive's\ndatabase) and used to symbolicate stack traces in the\nfunction's logs.\n"
5600
+ "description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored with the deployment attempt and sent to the runtime\nto symbolicate stack traces in the function's logs.\n"
5228
5601
  }
5229
5602
  },
5230
5603
  "required": ["name", "code"]
@@ -5271,11 +5644,16 @@ const openapiDocument = {
5271
5644
  },
5272
5645
  "TestInvocationResult": {
5273
5646
  "type": "object",
5274
- "description": "Metadata returned by POST /functions/{id}/test. The send is\nqueued; the actual invocation lands on the function's\ninvocations list a few seconds later as the inbound mail\ntraverses the MX path.\n",
5647
+ "description": "Metadata returned by POST /functions/{id}/test. The send is\nqueued; poll `trace_url` to watch the run progress through\nsend -> inbound -> webhook deliveries -> outbound requests,\nlogs, and replies.\n",
5275
5648
  "properties": {
5276
- "inbound_domain": {
5649
+ "test_run_id": {
5277
5650
  "type": "string",
5278
- "description": "Verified inbound domain the test email was sent to."
5651
+ "format": "uuid",
5652
+ "description": "Durable test run id used to fetch the run trace."
5653
+ },
5654
+ "inbound_domain": {
5655
+ "type": "string",
5656
+ "description": "Verified inbound domain the test email was sent to."
5279
5657
  },
5280
5658
  "to": {
5281
5659
  "type": "string",
@@ -5302,16 +5680,339 @@ const openapiDocument = {
5302
5680
  "type": "string",
5303
5681
  "format": "uri",
5304
5682
  "description": "Function detail page where invocations show up live."
5683
+ },
5684
+ "trace_url": {
5685
+ "type": "string",
5686
+ "description": "Relative API URL for GET /functions/{id}/test-runs/{test_run_id}/trace."
5305
5687
  }
5306
5688
  },
5307
5689
  "required": [
5690
+ "test_run_id",
5308
5691
  "inbound_domain",
5309
5692
  "to",
5310
5693
  "from",
5311
5694
  "send_id",
5312
5695
  "subject",
5313
5696
  "poll_since",
5314
- "watch_url"
5697
+ "watch_url",
5698
+ "trace_url"
5699
+ ]
5700
+ },
5701
+ "FunctionTestRunState": {
5702
+ "type": "string",
5703
+ "description": "High-level state for a function test run trace:\n - `send_failed`: the initial test email send failed.\n - `waiting_for_send`: the test run was created but no send result has been recorded yet.\n - `waiting_for_inbound`: the test send was queued and the matching inbound email has not arrived yet.\n - `waiting_for_function`: the inbound email arrived and webhook/function processing is still in flight.\n - `completed`: the function webhook completed successfully.\n - `failed`: webhook delivery exhausted retries.\n",
5704
+ "enum": [
5705
+ "send_failed",
5706
+ "waiting_for_send",
5707
+ "waiting_for_inbound",
5708
+ "waiting_for_function",
5709
+ "completed",
5710
+ "failed"
5711
+ ]
5712
+ },
5713
+ "FunctionTestRun": {
5714
+ "type": "object",
5715
+ "properties": {
5716
+ "id": {
5717
+ "type": "string",
5718
+ "format": "uuid"
5719
+ },
5720
+ "function_id": {
5721
+ "type": "string",
5722
+ "format": "uuid"
5723
+ },
5724
+ "inbound_domain": { "type": "string" },
5725
+ "to": { "type": "string" },
5726
+ "from": { "type": "string" },
5727
+ "subject": { "type": "string" },
5728
+ "poll_since": {
5729
+ "type": "string",
5730
+ "format": "date-time"
5731
+ },
5732
+ "created_at": {
5733
+ "type": "string",
5734
+ "format": "date-time"
5735
+ },
5736
+ "sent_at": {
5737
+ "type": ["string", "null"],
5738
+ "format": "date-time"
5739
+ },
5740
+ "send_error": { "type": ["string", "null"] }
5741
+ },
5742
+ "required": [
5743
+ "id",
5744
+ "function_id",
5745
+ "inbound_domain",
5746
+ "to",
5747
+ "from",
5748
+ "subject",
5749
+ "poll_since",
5750
+ "created_at",
5751
+ "sent_at",
5752
+ "send_error"
5753
+ ]
5754
+ },
5755
+ "FunctionTestRunSend": {
5756
+ "type": ["object", "null"],
5757
+ "properties": {
5758
+ "id": {
5759
+ "type": "string",
5760
+ "format": "uuid"
5761
+ },
5762
+ "status": { "$ref": "#/components/schemas/SentEmailStatus" },
5763
+ "queue_id": { "type": ["string", "null"] },
5764
+ "created_at": {
5765
+ "type": "string",
5766
+ "format": "date-time"
5767
+ },
5768
+ "updated_at": {
5769
+ "type": "string",
5770
+ "format": "date-time"
5771
+ }
5772
+ },
5773
+ "required": [
5774
+ "id",
5775
+ "status",
5776
+ "queue_id",
5777
+ "created_at",
5778
+ "updated_at"
5779
+ ]
5780
+ },
5781
+ "FunctionTestRunInboundEmail": {
5782
+ "type": ["object", "null"],
5783
+ "properties": {
5784
+ "id": {
5785
+ "type": "string",
5786
+ "format": "uuid"
5787
+ },
5788
+ "status": { "$ref": "#/components/schemas/EmailStatus" },
5789
+ "received_at": {
5790
+ "type": "string",
5791
+ "format": "date-time"
5792
+ },
5793
+ "from": { "type": "string" },
5794
+ "to": { "type": "string" },
5795
+ "subject": { "type": ["string", "null"] },
5796
+ "webhook_status": { "$ref": "#/components/schemas/EmailWebhookStatus" },
5797
+ "webhook_attempt_count": { "type": "integer" },
5798
+ "webhook_last_status_code": { "type": ["integer", "null"] },
5799
+ "webhook_last_error": { "type": ["string", "null"] }
5800
+ },
5801
+ "required": [
5802
+ "id",
5803
+ "status",
5804
+ "received_at",
5805
+ "from",
5806
+ "to",
5807
+ "subject",
5808
+ "webhook_status",
5809
+ "webhook_attempt_count",
5810
+ "webhook_last_status_code",
5811
+ "webhook_last_error"
5812
+ ]
5813
+ },
5814
+ "FunctionTestRunDeliveryEndpoint": {
5815
+ "type": ["object", "null"],
5816
+ "properties": {
5817
+ "id": {
5818
+ "type": "string",
5819
+ "format": "uuid"
5820
+ },
5821
+ "kind": {
5822
+ "type": "string",
5823
+ "description": "Endpoint kind. Current traces may include `http` or `function`; future endpoint kinds may appear."
5824
+ },
5825
+ "function_id": {
5826
+ "type": ["string", "null"],
5827
+ "format": "uuid"
5828
+ },
5829
+ "function_name": { "type": ["string", "null"] },
5830
+ "domain_id": {
5831
+ "type": ["string", "null"],
5832
+ "format": "uuid"
5833
+ },
5834
+ "enabled": { "type": "boolean" },
5835
+ "deactivated_at": {
5836
+ "type": ["string", "null"],
5837
+ "format": "date-time"
5838
+ },
5839
+ "is_current_function": { "type": "boolean" }
5840
+ },
5841
+ "required": [
5842
+ "id",
5843
+ "kind",
5844
+ "function_id",
5845
+ "function_name",
5846
+ "domain_id",
5847
+ "enabled",
5848
+ "deactivated_at",
5849
+ "is_current_function"
5850
+ ]
5851
+ },
5852
+ "FunctionTestRunDelivery": {
5853
+ "type": "object",
5854
+ "properties": {
5855
+ "id": {
5856
+ "type": "string",
5857
+ "description": "Webhook delivery id."
5858
+ },
5859
+ "endpoint_id": {
5860
+ "type": "string",
5861
+ "format": "uuid"
5862
+ },
5863
+ "endpoint_url": {
5864
+ "type": "string",
5865
+ "format": "uri"
5866
+ },
5867
+ "status": {
5868
+ "type": "string",
5869
+ "enum": [
5870
+ "pending",
5871
+ "delivered",
5872
+ "header_confirmed",
5873
+ "failed"
5874
+ ]
5875
+ },
5876
+ "attempt_count": { "type": "integer" },
5877
+ "duration_ms": { "type": ["integer", "null"] },
5878
+ "last_error": { "type": ["string", "null"] },
5879
+ "last_error_code": { "type": ["string", "null"] },
5880
+ "created_at": {
5881
+ "type": "string",
5882
+ "format": "date-time"
5883
+ },
5884
+ "updated_at": {
5885
+ "type": "string",
5886
+ "format": "date-time"
5887
+ },
5888
+ "endpoint": { "$ref": "#/components/schemas/FunctionTestRunDeliveryEndpoint" }
5889
+ },
5890
+ "required": [
5891
+ "id",
5892
+ "endpoint_id",
5893
+ "endpoint_url",
5894
+ "status",
5895
+ "attempt_count",
5896
+ "duration_ms",
5897
+ "last_error",
5898
+ "last_error_code",
5899
+ "created_at",
5900
+ "updated_at",
5901
+ "endpoint"
5902
+ ]
5903
+ },
5904
+ "FunctionTestRunOutboundRequest": {
5905
+ "type": "object",
5906
+ "properties": {
5907
+ "id": {
5908
+ "type": "string",
5909
+ "format": "uuid"
5910
+ },
5911
+ "function_id": {
5912
+ "type": "string",
5913
+ "format": "uuid"
5914
+ },
5915
+ "webhook_delivery_id": { "type": ["string", "null"] },
5916
+ "email_id": {
5917
+ "type": ["string", "null"],
5918
+ "format": "uuid"
5919
+ },
5920
+ "endpoint_id": {
5921
+ "type": ["string", "null"],
5922
+ "format": "uuid"
5923
+ },
5924
+ "method": { "type": "string" },
5925
+ "url": {
5926
+ "type": "string",
5927
+ "format": "uri"
5928
+ },
5929
+ "host": { "type": "string" },
5930
+ "path": { "type": "string" },
5931
+ "status_code": { "type": ["integer", "null"] },
5932
+ "ok": { "type": ["boolean", "null"] },
5933
+ "duration_ms": { "type": "integer" },
5934
+ "error": { "type": ["string", "null"] },
5935
+ "ts": {
5936
+ "type": "string",
5937
+ "format": "date-time"
5938
+ }
5939
+ },
5940
+ "required": [
5941
+ "id",
5942
+ "function_id",
5943
+ "webhook_delivery_id",
5944
+ "email_id",
5945
+ "endpoint_id",
5946
+ "method",
5947
+ "url",
5948
+ "host",
5949
+ "path",
5950
+ "status_code",
5951
+ "ok",
5952
+ "duration_ms",
5953
+ "error",
5954
+ "ts"
5955
+ ]
5956
+ },
5957
+ "FunctionTestRunReply": {
5958
+ "type": "object",
5959
+ "properties": {
5960
+ "id": {
5961
+ "type": "string",
5962
+ "format": "uuid"
5963
+ },
5964
+ "status": { "$ref": "#/components/schemas/SentEmailStatus" },
5965
+ "to": { "type": "string" },
5966
+ "subject": { "type": "string" },
5967
+ "queue_id": { "type": ["string", "null"] },
5968
+ "created_at": {
5969
+ "type": "string",
5970
+ "format": "date-time"
5971
+ }
5972
+ },
5973
+ "required": [
5974
+ "id",
5975
+ "status",
5976
+ "to",
5977
+ "subject",
5978
+ "queue_id",
5979
+ "created_at"
5980
+ ]
5981
+ },
5982
+ "FunctionTestRunTrace": {
5983
+ "type": "object",
5984
+ "description": "End-to-end trace for a `POST /functions/{id}/test` run. The\nshape is stable, but many nested sections are null or empty\nuntil the corresponding phase has happened.\n",
5985
+ "properties": {
5986
+ "state": { "$ref": "#/components/schemas/FunctionTestRunState" },
5987
+ "test_run": { "$ref": "#/components/schemas/FunctionTestRun" },
5988
+ "test_send": { "$ref": "#/components/schemas/FunctionTestRunSend" },
5989
+ "inbound_email": { "$ref": "#/components/schemas/FunctionTestRunInboundEmail" },
5990
+ "deliveries": {
5991
+ "type": "array",
5992
+ "items": { "$ref": "#/components/schemas/FunctionTestRunDelivery" }
5993
+ },
5994
+ "outbound_requests": {
5995
+ "type": "array",
5996
+ "items": { "$ref": "#/components/schemas/FunctionTestRunOutboundRequest" }
5997
+ },
5998
+ "logs": {
5999
+ "type": "array",
6000
+ "items": { "$ref": "#/components/schemas/FunctionLogRow" }
6001
+ },
6002
+ "replies": {
6003
+ "type": "array",
6004
+ "items": { "$ref": "#/components/schemas/FunctionTestRunReply" }
6005
+ }
6006
+ },
6007
+ "required": [
6008
+ "state",
6009
+ "test_run",
6010
+ "test_send",
6011
+ "inbound_email",
6012
+ "deliveries",
6013
+ "outbound_requests",
6014
+ "logs",
6015
+ "replies"
5315
6016
  ]
5316
6017
  },
5317
6018
  "FunctionLogRow": {
@@ -5756,6 +6457,58 @@ const operationManifest = [
5756
6457
  "tag": "CLI",
5757
6458
  "tagCommand": "cli"
5758
6459
  },
6460
+ {
6461
+ "binaryResponse": false,
6462
+ "bodyRequired": true,
6463
+ "command": "resend-cli-signup-verification",
6464
+ "description": "Sends a new email verification code for a pending CLI signup session.\nThis endpoint does not require an API key.\n",
6465
+ "hasJsonBody": true,
6466
+ "method": "POST",
6467
+ "operationId": "resendCliSignupVerification",
6468
+ "path": "/cli/signup/resend",
6469
+ "pathParams": [],
6470
+ "queryParams": [],
6471
+ "requestSchema": {
6472
+ "type": "object",
6473
+ "additionalProperties": false,
6474
+ "properties": { "signup_token": {
6475
+ "type": "string",
6476
+ "minLength": 1
6477
+ } },
6478
+ "required": ["signup_token"]
6479
+ },
6480
+ "responseSchema": {
6481
+ "type": "object",
6482
+ "properties": {
6483
+ "email": {
6484
+ "type": "string",
6485
+ "format": "email"
6486
+ },
6487
+ "expires_in": {
6488
+ "type": "integer",
6489
+ "description": "Seconds until the pending signup expires"
6490
+ },
6491
+ "resend_after": {
6492
+ "type": "integer",
6493
+ "description": "Minimum seconds before requesting another verification email"
6494
+ },
6495
+ "verification_code_length": {
6496
+ "type": "integer",
6497
+ "description": "Number of digits in the emailed verification code"
6498
+ }
6499
+ },
6500
+ "required": [
6501
+ "email",
6502
+ "expires_in",
6503
+ "resend_after",
6504
+ "verification_code_length"
6505
+ ]
6506
+ },
6507
+ "sdkName": "resendCliSignupVerification",
6508
+ "summary": "Resend CLI signup verification code",
6509
+ "tag": "CLI",
6510
+ "tagCommand": "cli"
6511
+ },
5759
6512
  {
5760
6513
  "binaryResponse": false,
5761
6514
  "bodyRequired": false,
@@ -5802,28 +6555,180 @@ const operationManifest = [
5802
6555
  },
5803
6556
  "verification_uri_complete": {
5804
6557
  "type": "string",
5805
- "description": "Browser URL with the user code prefilled"
5806
- },
5807
- "expires_in": {
5808
- "type": "integer",
5809
- "description": "Seconds until the login session expires"
6558
+ "description": "Browser URL with the user code prefilled"
6559
+ },
6560
+ "expires_in": {
6561
+ "type": "integer",
6562
+ "description": "Seconds until the login session expires"
6563
+ },
6564
+ "interval": {
6565
+ "type": "integer",
6566
+ "description": "Minimum seconds between poll requests"
6567
+ }
6568
+ },
6569
+ "required": [
6570
+ "device_code",
6571
+ "user_code",
6572
+ "verification_uri",
6573
+ "verification_uri_complete",
6574
+ "expires_in",
6575
+ "interval"
6576
+ ]
6577
+ },
6578
+ "sdkName": "startCliLogin",
6579
+ "summary": "Start CLI browser login",
6580
+ "tag": "CLI",
6581
+ "tagCommand": "cli"
6582
+ },
6583
+ {
6584
+ "binaryResponse": false,
6585
+ "bodyRequired": true,
6586
+ "command": "start-cli-signup",
6587
+ "description": "Starts a terminal-native CLI signup. The API validates the signup code,\ncreates a pending signup session, sends an email verification code, and\nreturns an opaque signup token used by the resend and verify steps. This\nendpoint does not require an API key.\n",
6588
+ "hasJsonBody": true,
6589
+ "method": "POST",
6590
+ "operationId": "startCliSignup",
6591
+ "path": "/cli/signup/start",
6592
+ "pathParams": [],
6593
+ "queryParams": [],
6594
+ "requestSchema": {
6595
+ "type": "object",
6596
+ "additionalProperties": false,
6597
+ "properties": {
6598
+ "email": {
6599
+ "type": "string",
6600
+ "format": "email",
6601
+ "maxLength": 254
6602
+ },
6603
+ "signup_code": {
6604
+ "type": "string",
6605
+ "minLength": 1,
6606
+ "maxLength": 128
6607
+ },
6608
+ "terms_accepted": {
6609
+ "type": "boolean",
6610
+ "const": true,
6611
+ "description": "Must be true to confirm acceptance of Primitive's Terms of Service and Privacy Policy"
6612
+ },
6613
+ "device_name": {
6614
+ "type": "string",
6615
+ "minLength": 1,
6616
+ "maxLength": 80,
6617
+ "description": "Human-readable device name used for the created CLI API key"
6618
+ },
6619
+ "metadata": {
6620
+ "type": "object",
6621
+ "additionalProperties": true,
6622
+ "description": "Optional client metadata stored with the signup session; serialized JSON must be 2048 bytes or fewer"
6623
+ }
6624
+ },
6625
+ "required": [
6626
+ "email",
6627
+ "signup_code",
6628
+ "terms_accepted"
6629
+ ]
6630
+ },
6631
+ "responseSchema": {
6632
+ "type": "object",
6633
+ "properties": {
6634
+ "signup_token": {
6635
+ "type": "string",
6636
+ "description": "Opaque token used to verify or resend the pending CLI signup"
6637
+ },
6638
+ "email": {
6639
+ "type": "string",
6640
+ "format": "email"
6641
+ },
6642
+ "expires_in": {
6643
+ "type": "integer",
6644
+ "description": "Seconds until the pending signup expires"
6645
+ },
6646
+ "resend_after": {
6647
+ "type": "integer",
6648
+ "description": "Minimum seconds before requesting another verification email"
6649
+ },
6650
+ "verification_code_length": {
6651
+ "type": "integer",
6652
+ "description": "Number of digits in the emailed verification code"
6653
+ }
6654
+ },
6655
+ "required": [
6656
+ "signup_token",
6657
+ "email",
6658
+ "expires_in",
6659
+ "resend_after",
6660
+ "verification_code_length"
6661
+ ]
6662
+ },
6663
+ "sdkName": "startCliSignup",
6664
+ "summary": "Start CLI account signup",
6665
+ "tag": "CLI",
6666
+ "tagCommand": "cli"
6667
+ },
6668
+ {
6669
+ "binaryResponse": false,
6670
+ "bodyRequired": true,
6671
+ "command": "verify-cli-signup",
6672
+ "description": "Verifies the email code for a CLI signup session, creates the account,\nredeems the reserved signup code, mints an org-scoped CLI API key, and\nreturns the raw key exactly once. This endpoint does not require an API key.\n",
6673
+ "hasJsonBody": true,
6674
+ "method": "POST",
6675
+ "operationId": "verifyCliSignup",
6676
+ "path": "/cli/signup/verify",
6677
+ "pathParams": [],
6678
+ "queryParams": [],
6679
+ "requestSchema": {
6680
+ "type": "object",
6681
+ "additionalProperties": false,
6682
+ "properties": {
6683
+ "signup_token": {
6684
+ "type": "string",
6685
+ "minLength": 1
6686
+ },
6687
+ "verification_code": {
6688
+ "type": "string",
6689
+ "minLength": 1,
6690
+ "maxLength": 32
6691
+ },
6692
+ "password": {
6693
+ "type": "string",
6694
+ "minLength": 1,
6695
+ "maxLength": 1024
6696
+ }
6697
+ },
6698
+ "required": [
6699
+ "signup_token",
6700
+ "verification_code",
6701
+ "password"
6702
+ ]
6703
+ },
6704
+ "responseSchema": {
6705
+ "type": "object",
6706
+ "properties": {
6707
+ "api_key": {
6708
+ "type": "string",
6709
+ "description": "Newly-created API key for CLI authentication"
6710
+ },
6711
+ "key_id": {
6712
+ "type": "string",
6713
+ "format": "uuid"
6714
+ },
6715
+ "key_prefix": { "type": "string" },
6716
+ "org_id": {
6717
+ "type": "string",
6718
+ "format": "uuid"
5810
6719
  },
5811
- "interval": {
5812
- "type": "integer",
5813
- "description": "Minimum seconds between poll requests"
5814
- }
6720
+ "org_name": { "type": ["string", "null"] }
5815
6721
  },
5816
6722
  "required": [
5817
- "device_code",
5818
- "user_code",
5819
- "verification_uri",
5820
- "verification_uri_complete",
5821
- "expires_in",
5822
- "interval"
6723
+ "api_key",
6724
+ "key_id",
6725
+ "key_prefix",
6726
+ "org_id",
6727
+ "org_name"
5823
6728
  ]
5824
6729
  },
5825
- "sdkName": "startCliLogin",
5826
- "summary": "Start CLI browser login",
6730
+ "sdkName": "verifyCliSignup",
6731
+ "summary": "Verify CLI signup and create API key",
5827
6732
  "tag": "CLI",
5828
6733
  "tagCommand": "cli"
5829
6734
  },
@@ -7497,7 +8402,7 @@ const operationManifest = [
7497
8402
  "binaryResponse": false,
7498
8403
  "bodyRequired": true,
7499
8404
  "command": "create-function",
7500
- "description": "Creates and deploys a new function. The handler must be a single\nESM module whose default export is an object with an async\n`fetch(request, env)` method (Workers-style). The gateway\nHMAC-verifies the POST against the org's webhook secret before\ninvoking the handler; the request body parses to an\n`email.received` event (see `EmailReceivedEvent` and the\nWebhook payload section for the full schema). Code is bundled\nbefore being uploaded; ship a single self-contained file rather\nthan relying on external imports.\n\n**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`\n(optional) is capped at 5 MiB UTF-8 and is stored only on the\nedge runtime side; it is not persisted in Primitive's database.\n\n**Auto-wiring.** On successful deploy, Primitive automatically\ncreates a webhook endpoint that delivers inbound mail to the\nfunction. There is nothing to configure on the Endpoints API\nfor this to work; the gateway URL returned here is for\nreference only and is not directly callable from outside.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`) already\nbound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
8405
+ "description": "Creates and deploys a new function. The handler must be a single\nESM module whose default export is an object with an async\n`fetch(request, env)` method (Workers-style). The gateway\nHMAC-verifies the POST against the org's webhook secret before\ninvoking the handler; the request body parses to an\n`email.received` event (see `EmailReceivedEvent` and the\nWebhook payload section for the full schema). Code is bundled\nbefore being uploaded; ship a single self-contained file rather\nthan relying on external imports.\n\n**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`\n(optional) is capped at 5 MiB UTF-8, stored with each deployment\nattempt, and sent to the runtime so stack traces can resolve to\noriginal source files.\n\n**Auto-wiring.** On successful deploy, Primitive automatically\ncreates a webhook endpoint that delivers inbound mail to the\nfunction. There is nothing to configure on the Endpoints API\nfor this to work; the gateway URL returned here is for\nreference only and is not directly callable from outside.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`) already\nbound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
7501
8406
  "hasJsonBody": true,
7502
8407
  "method": "POST",
7503
8408
  "operationId": "createFunction",
@@ -7523,7 +8428,7 @@ const operationManifest = [
7523
8428
  "type": "string",
7524
8429
  "minLength": 1,
7525
8430
  "maxLength": 5242880,
7526
- "description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored only on the runtime side (not in Primitive's\ndatabase) and used to symbolicate stack traces in the\nfunction's logs.\n"
8431
+ "description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored with the deployment attempt and sent to the runtime\nto symbolicate stack traces in the function's logs.\n"
7527
8432
  }
7528
8433
  },
7529
8434
  "required": ["name", "code"]
@@ -7745,17 +8650,450 @@ const operationManifest = [
7745
8650
  }
7746
8651
  },
7747
8652
  "required": [
7748
- "id",
7749
- "name",
7750
- "code",
7751
- "deploy_status",
7752
- "gateway_url",
7753
- "created_at",
7754
- "updated_at"
8653
+ "id",
8654
+ "name",
8655
+ "code",
8656
+ "deploy_status",
8657
+ "gateway_url",
8658
+ "created_at",
8659
+ "updated_at"
8660
+ ]
8661
+ },
8662
+ "sdkName": "getFunction",
8663
+ "summary": "Get a function",
8664
+ "tag": "Functions",
8665
+ "tagCommand": "functions"
8666
+ },
8667
+ {
8668
+ "binaryResponse": false,
8669
+ "bodyRequired": false,
8670
+ "command": "get-function-test-run-trace",
8671
+ "description": "Returns the current end-to-end trace for a function test run.\nThe trace is intentionally partial while the test is still in\nflight: callers can poll this endpoint and watch it fill in\nfrom send -> inbound -> webhook deliveries -> outbound\nrequests, logs, and replies.\n",
8672
+ "hasJsonBody": false,
8673
+ "method": "GET",
8674
+ "operationId": "getFunctionTestRunTrace",
8675
+ "path": "/functions/{id}/test-runs/{run_id}/trace",
8676
+ "pathParams": [{
8677
+ "description": "Resource UUID",
8678
+ "enum": null,
8679
+ "name": "id",
8680
+ "required": true,
8681
+ "type": "string"
8682
+ }, {
8683
+ "description": "Function test run id returned by POST /functions/{id}/test.",
8684
+ "enum": null,
8685
+ "name": "run_id",
8686
+ "required": true,
8687
+ "type": "string"
8688
+ }],
8689
+ "queryParams": [],
8690
+ "requestSchema": null,
8691
+ "responseSchema": {
8692
+ "type": "object",
8693
+ "description": "End-to-end trace for a `POST /functions/{id}/test` run. The\nshape is stable, but many nested sections are null or empty\nuntil the corresponding phase has happened.\n",
8694
+ "properties": {
8695
+ "state": {
8696
+ "type": "string",
8697
+ "description": "High-level state for a function test run trace:\n - `send_failed`: the initial test email send failed.\n - `waiting_for_send`: the test run was created but no send result has been recorded yet.\n - `waiting_for_inbound`: the test send was queued and the matching inbound email has not arrived yet.\n - `waiting_for_function`: the inbound email arrived and webhook/function processing is still in flight.\n - `completed`: the function webhook completed successfully.\n - `failed`: webhook delivery exhausted retries.\n",
8698
+ "enum": [
8699
+ "send_failed",
8700
+ "waiting_for_send",
8701
+ "waiting_for_inbound",
8702
+ "waiting_for_function",
8703
+ "completed",
8704
+ "failed"
8705
+ ]
8706
+ },
8707
+ "test_run": {
8708
+ "type": "object",
8709
+ "properties": {
8710
+ "id": {
8711
+ "type": "string",
8712
+ "format": "uuid"
8713
+ },
8714
+ "function_id": {
8715
+ "type": "string",
8716
+ "format": "uuid"
8717
+ },
8718
+ "inbound_domain": { "type": "string" },
8719
+ "to": { "type": "string" },
8720
+ "from": { "type": "string" },
8721
+ "subject": { "type": "string" },
8722
+ "poll_since": {
8723
+ "type": "string",
8724
+ "format": "date-time"
8725
+ },
8726
+ "created_at": {
8727
+ "type": "string",
8728
+ "format": "date-time"
8729
+ },
8730
+ "sent_at": {
8731
+ "type": ["string", "null"],
8732
+ "format": "date-time"
8733
+ },
8734
+ "send_error": { "type": ["string", "null"] }
8735
+ },
8736
+ "required": [
8737
+ "id",
8738
+ "function_id",
8739
+ "inbound_domain",
8740
+ "to",
8741
+ "from",
8742
+ "subject",
8743
+ "poll_since",
8744
+ "created_at",
8745
+ "sent_at",
8746
+ "send_error"
8747
+ ]
8748
+ },
8749
+ "test_send": {
8750
+ "type": ["object", "null"],
8751
+ "properties": {
8752
+ "id": {
8753
+ "type": "string",
8754
+ "format": "uuid"
8755
+ },
8756
+ "status": {
8757
+ "type": "string",
8758
+ "description": "Lifecycle status of a sent_emails row. Possible values:\n\n - `queued`: pre-call INSERT; the outbound agent has not\n yet replied.\n - `submitted_to_agent`: agent accepted; `queue_id` is set.\n - `agent_failed`: agent rejected; `error_code` and\n `error_message` carry the reason.\n - `gate_denied`: a recipient-scope gate denied the send;\n the agent was never called. The `gates` array carries\n the denial detail. /send-mail returns 403 in this case\n so callers see the denial synchronously; /sent-emails\n additionally records the row for historical lookup,\n which is when this status appears in a listing.\n - `unknown`: terminal indeterminate; the on-box log\n poller couldn't classify the receiver's response.\n - `delivered` / `bounced` / `deferred` / `wait_timeout`:\n terminal delivery outcomes (see DeliveryStatus).\n",
8759
+ "enum": [
8760
+ "queued",
8761
+ "submitted_to_agent",
8762
+ "agent_failed",
8763
+ "gate_denied",
8764
+ "unknown",
8765
+ "delivered",
8766
+ "bounced",
8767
+ "deferred",
8768
+ "wait_timeout"
8769
+ ]
8770
+ },
8771
+ "queue_id": { "type": ["string", "null"] },
8772
+ "created_at": {
8773
+ "type": "string",
8774
+ "format": "date-time"
8775
+ },
8776
+ "updated_at": {
8777
+ "type": "string",
8778
+ "format": "date-time"
8779
+ }
8780
+ },
8781
+ "required": [
8782
+ "id",
8783
+ "status",
8784
+ "queue_id",
8785
+ "created_at",
8786
+ "updated_at"
8787
+ ]
8788
+ },
8789
+ "inbound_email": {
8790
+ "type": ["object", "null"],
8791
+ "properties": {
8792
+ "id": {
8793
+ "type": "string",
8794
+ "format": "uuid"
8795
+ },
8796
+ "status": {
8797
+ "type": "string",
8798
+ "description": "Lifecycle status of an INBOUND email (a row in the `emails`\ntable). Distinct from `SentEmailStatus`, which describes\nthe OUTBOUND lifecycle (the `sent_emails` table) and uses\na different vocabulary because the lifecycles differ.\nPossible values:\n\n - `pending`: the row was inserted at ingestion (mx_main)\n and has not yet completed the spam / filter / auth\n pipeline. Body and parsed fields are present; webhook\n delivery is not yet scheduled. Most rows transition out\n of `pending` within seconds.\n - `accepted`: the inbound passed the policy gates and is\n queued for webhook delivery. The `webhook_status` field\n tracks the separate webhook-delivery lifecycle from\n this point.\n - `completed`: terminal success. Webhook delivery\n attempted and acknowledged by every active endpoint, OR\n no endpoints are configured, so the row is durably\n archived.\n - `rejected`: terminal failure at ingestion (spam, blocked\n sender, filter rule, malformed). The body and metadata\n are stored for auditing but no webhook fires and the\n row is not repliable.\n\nSee also `webhook_status` (separate enum tracking the\nwebhook-delivery state machine) and `SentEmailStatus` (the\noutbound vocabulary).\n",
8799
+ "enum": [
8800
+ "pending",
8801
+ "accepted",
8802
+ "completed",
8803
+ "rejected"
8804
+ ]
8805
+ },
8806
+ "received_at": {
8807
+ "type": "string",
8808
+ "format": "date-time"
8809
+ },
8810
+ "from": { "type": "string" },
8811
+ "to": { "type": "string" },
8812
+ "subject": { "type": ["string", "null"] },
8813
+ "webhook_status": {
8814
+ "type": ["string", "null"],
8815
+ "description": "Webhook-delivery state for an inbound email. Tracks a\nSEPARATE lifecycle from the email's `status` field; the\nsame row carries both. Possible values:\n\n - `pending`: ingestion is past `pending` (the email itself\n is `accepted`) but the webhook fan-out has not yet\n started for this row.\n - `in_flight`: at least one delivery attempt is in flight.\n - `fired`: terminal success. Every active endpoint\n acknowledged the delivery (or accepted it after retries).\n - `failed`: terminal partial-failure. At least one endpoint\n exhausted its retry budget; some endpoints may still\n have succeeded.\n - `exhausted`: terminal failure. Every endpoint exhausted\n its retry budget without success.\n - `null`: no endpoints configured, so no webhook lifecycle\n applies.\n\nNote that the value `pending` here does NOT mean the email\nis `pending`; it means the email is past ingestion but\nwebhook delivery has not yet begun. Two overlapping uses\nof the word `pending` for distinct lifecycle phases.\n",
8816
+ "enum": [
8817
+ "pending",
8818
+ "in_flight",
8819
+ "fired",
8820
+ "failed",
8821
+ "exhausted",
8822
+ null
8823
+ ]
8824
+ },
8825
+ "webhook_attempt_count": { "type": "integer" },
8826
+ "webhook_last_status_code": { "type": ["integer", "null"] },
8827
+ "webhook_last_error": { "type": ["string", "null"] }
8828
+ },
8829
+ "required": [
8830
+ "id",
8831
+ "status",
8832
+ "received_at",
8833
+ "from",
8834
+ "to",
8835
+ "subject",
8836
+ "webhook_status",
8837
+ "webhook_attempt_count",
8838
+ "webhook_last_status_code",
8839
+ "webhook_last_error"
8840
+ ]
8841
+ },
8842
+ "deliveries": {
8843
+ "type": "array",
8844
+ "items": {
8845
+ "type": "object",
8846
+ "properties": {
8847
+ "id": {
8848
+ "type": "string",
8849
+ "description": "Webhook delivery id."
8850
+ },
8851
+ "endpoint_id": {
8852
+ "type": "string",
8853
+ "format": "uuid"
8854
+ },
8855
+ "endpoint_url": {
8856
+ "type": "string",
8857
+ "format": "uri"
8858
+ },
8859
+ "status": {
8860
+ "type": "string",
8861
+ "enum": [
8862
+ "pending",
8863
+ "delivered",
8864
+ "header_confirmed",
8865
+ "failed"
8866
+ ]
8867
+ },
8868
+ "attempt_count": { "type": "integer" },
8869
+ "duration_ms": { "type": ["integer", "null"] },
8870
+ "last_error": { "type": ["string", "null"] },
8871
+ "last_error_code": { "type": ["string", "null"] },
8872
+ "created_at": {
8873
+ "type": "string",
8874
+ "format": "date-time"
8875
+ },
8876
+ "updated_at": {
8877
+ "type": "string",
8878
+ "format": "date-time"
8879
+ },
8880
+ "endpoint": {
8881
+ "type": ["object", "null"],
8882
+ "properties": {
8883
+ "id": {
8884
+ "type": "string",
8885
+ "format": "uuid"
8886
+ },
8887
+ "kind": {
8888
+ "type": "string",
8889
+ "description": "Endpoint kind. Current traces may include `http` or `function`; future endpoint kinds may appear."
8890
+ },
8891
+ "function_id": {
8892
+ "type": ["string", "null"],
8893
+ "format": "uuid"
8894
+ },
8895
+ "function_name": { "type": ["string", "null"] },
8896
+ "domain_id": {
8897
+ "type": ["string", "null"],
8898
+ "format": "uuid"
8899
+ },
8900
+ "enabled": { "type": "boolean" },
8901
+ "deactivated_at": {
8902
+ "type": ["string", "null"],
8903
+ "format": "date-time"
8904
+ },
8905
+ "is_current_function": { "type": "boolean" }
8906
+ },
8907
+ "required": [
8908
+ "id",
8909
+ "kind",
8910
+ "function_id",
8911
+ "function_name",
8912
+ "domain_id",
8913
+ "enabled",
8914
+ "deactivated_at",
8915
+ "is_current_function"
8916
+ ]
8917
+ }
8918
+ },
8919
+ "required": [
8920
+ "id",
8921
+ "endpoint_id",
8922
+ "endpoint_url",
8923
+ "status",
8924
+ "attempt_count",
8925
+ "duration_ms",
8926
+ "last_error",
8927
+ "last_error_code",
8928
+ "created_at",
8929
+ "updated_at",
8930
+ "endpoint"
8931
+ ]
8932
+ }
8933
+ },
8934
+ "outbound_requests": {
8935
+ "type": "array",
8936
+ "items": {
8937
+ "type": "object",
8938
+ "properties": {
8939
+ "id": {
8940
+ "type": "string",
8941
+ "format": "uuid"
8942
+ },
8943
+ "function_id": {
8944
+ "type": "string",
8945
+ "format": "uuid"
8946
+ },
8947
+ "webhook_delivery_id": { "type": ["string", "null"] },
8948
+ "email_id": {
8949
+ "type": ["string", "null"],
8950
+ "format": "uuid"
8951
+ },
8952
+ "endpoint_id": {
8953
+ "type": ["string", "null"],
8954
+ "format": "uuid"
8955
+ },
8956
+ "method": { "type": "string" },
8957
+ "url": {
8958
+ "type": "string",
8959
+ "format": "uri"
8960
+ },
8961
+ "host": { "type": "string" },
8962
+ "path": { "type": "string" },
8963
+ "status_code": { "type": ["integer", "null"] },
8964
+ "ok": { "type": ["boolean", "null"] },
8965
+ "duration_ms": { "type": "integer" },
8966
+ "error": { "type": ["string", "null"] },
8967
+ "ts": {
8968
+ "type": "string",
8969
+ "format": "date-time"
8970
+ }
8971
+ },
8972
+ "required": [
8973
+ "id",
8974
+ "function_id",
8975
+ "webhook_delivery_id",
8976
+ "email_id",
8977
+ "endpoint_id",
8978
+ "method",
8979
+ "url",
8980
+ "host",
8981
+ "path",
8982
+ "status_code",
8983
+ "ok",
8984
+ "duration_ms",
8985
+ "error",
8986
+ "ts"
8987
+ ]
8988
+ }
8989
+ },
8990
+ "logs": {
8991
+ "type": "array",
8992
+ "items": {
8993
+ "type": "object",
8994
+ "description": "One row from GET /functions/{id}/logs. Represents a single\ncaptured log line emitted by the running handler (e.g. via\n`console.log` / `console.error`).\n",
8995
+ "properties": {
8996
+ "id": {
8997
+ "type": "string",
8998
+ "format": "uuid",
8999
+ "description": "Unique log row id (stable across pages)."
9000
+ },
9001
+ "function_id": {
9002
+ "type": "string",
9003
+ "format": "uuid",
9004
+ "description": "The function this log row belongs to."
9005
+ },
9006
+ "level": {
9007
+ "type": "string",
9008
+ "enum": [
9009
+ "debug",
9010
+ "log",
9011
+ "info",
9012
+ "warn",
9013
+ "error"
9014
+ ],
9015
+ "description": "Severity. `log` is the runtime's default for unannotated\n`console.log` calls; the other levels match standard\n`console.*` methods.\n"
9016
+ },
9017
+ "message": {
9018
+ "type": "string",
9019
+ "description": "The textual message body. The runtime stringifies non-string\narguments before persisting, so this is always a plain\nstring.\n"
9020
+ },
9021
+ "ts": {
9022
+ "type": "string",
9023
+ "format": "date-time",
9024
+ "description": "When the handler emitted this line. Newest-first ordering\non this column drives pagination; clock is the runtime's,\nnot the gateway's.\n"
9025
+ },
9026
+ "metadata": {
9027
+ "type": ["object", "null"],
9028
+ "additionalProperties": true,
9029
+ "description": "Optional structured payload the runtime attaches alongside\nthe message (e.g. extra args passed to `console.log`).\nShape is opaque; treat keys as untyped.\n"
9030
+ }
9031
+ },
9032
+ "required": [
9033
+ "id",
9034
+ "function_id",
9035
+ "level",
9036
+ "message",
9037
+ "ts"
9038
+ ]
9039
+ }
9040
+ },
9041
+ "replies": {
9042
+ "type": "array",
9043
+ "items": {
9044
+ "type": "object",
9045
+ "properties": {
9046
+ "id": {
9047
+ "type": "string",
9048
+ "format": "uuid"
9049
+ },
9050
+ "status": {
9051
+ "type": "string",
9052
+ "description": "Lifecycle status of a sent_emails row. Possible values:\n\n - `queued`: pre-call INSERT; the outbound agent has not\n yet replied.\n - `submitted_to_agent`: agent accepted; `queue_id` is set.\n - `agent_failed`: agent rejected; `error_code` and\n `error_message` carry the reason.\n - `gate_denied`: a recipient-scope gate denied the send;\n the agent was never called. The `gates` array carries\n the denial detail. /send-mail returns 403 in this case\n so callers see the denial synchronously; /sent-emails\n additionally records the row for historical lookup,\n which is when this status appears in a listing.\n - `unknown`: terminal indeterminate; the on-box log\n poller couldn't classify the receiver's response.\n - `delivered` / `bounced` / `deferred` / `wait_timeout`:\n terminal delivery outcomes (see DeliveryStatus).\n",
9053
+ "enum": [
9054
+ "queued",
9055
+ "submitted_to_agent",
9056
+ "agent_failed",
9057
+ "gate_denied",
9058
+ "unknown",
9059
+ "delivered",
9060
+ "bounced",
9061
+ "deferred",
9062
+ "wait_timeout"
9063
+ ]
9064
+ },
9065
+ "to": { "type": "string" },
9066
+ "subject": { "type": "string" },
9067
+ "queue_id": { "type": ["string", "null"] },
9068
+ "created_at": {
9069
+ "type": "string",
9070
+ "format": "date-time"
9071
+ }
9072
+ },
9073
+ "required": [
9074
+ "id",
9075
+ "status",
9076
+ "to",
9077
+ "subject",
9078
+ "queue_id",
9079
+ "created_at"
9080
+ ]
9081
+ }
9082
+ }
9083
+ },
9084
+ "required": [
9085
+ "state",
9086
+ "test_run",
9087
+ "test_send",
9088
+ "inbound_email",
9089
+ "deliveries",
9090
+ "outbound_requests",
9091
+ "logs",
9092
+ "replies"
7755
9093
  ]
7756
9094
  },
7757
- "sdkName": "getFunction",
7758
- "summary": "Get a function",
9095
+ "sdkName": "getFunctionTestRunTrace",
9096
+ "summary": "Get a function test run trace",
7759
9097
  "tag": "Functions",
7760
9098
  "tagCommand": "functions"
7761
9099
  },
@@ -8075,8 +9413,13 @@ const operationManifest = [
8075
9413
  },
8076
9414
  "responseSchema": {
8077
9415
  "type": "object",
8078
- "description": "Metadata returned by POST /functions/{id}/test. The send is\nqueued; the actual invocation lands on the function's\ninvocations list a few seconds later as the inbound mail\ntraverses the MX path.\n",
9416
+ "description": "Metadata returned by POST /functions/{id}/test. The send is\nqueued; poll `trace_url` to watch the run progress through\nsend -> inbound -> webhook deliveries -> outbound requests,\nlogs, and replies.\n",
8079
9417
  "properties": {
9418
+ "test_run_id": {
9419
+ "type": "string",
9420
+ "format": "uuid",
9421
+ "description": "Durable test run id used to fetch the run trace."
9422
+ },
8080
9423
  "inbound_domain": {
8081
9424
  "type": "string",
8082
9425
  "description": "Verified inbound domain the test email was sent to."
@@ -8106,16 +9449,22 @@ const operationManifest = [
8106
9449
  "type": "string",
8107
9450
  "format": "uri",
8108
9451
  "description": "Function detail page where invocations show up live."
9452
+ },
9453
+ "trace_url": {
9454
+ "type": "string",
9455
+ "description": "Relative API URL for GET /functions/{id}/test-runs/{test_run_id}/trace."
8109
9456
  }
8110
9457
  },
8111
9458
  "required": [
9459
+ "test_run_id",
8112
9460
  "inbound_domain",
8113
9461
  "to",
8114
9462
  "from",
8115
9463
  "send_id",
8116
9464
  "subject",
8117
9465
  "poll_since",
8118
- "watch_url"
9466
+ "watch_url",
9467
+ "trace_url"
8119
9468
  ]
8120
9469
  },
8121
9470
  "sdkName": "testFunction",
@@ -8127,7 +9476,7 @@ const operationManifest = [
8127
9476
  "binaryResponse": false,
8128
9477
  "bodyRequired": true,
8129
9478
  "command": "update-function",
8130
- "description": "Replaces the function's source code with the body's `code` and\ntriggers a redeploy. Same size limits as `POST /functions`.\nUse this verb to push secret writes into the running handler:\npassing the same `code` re-runs the deploy and refreshes the\nbinding set with the latest values from the secrets table.\n\nOn a 502 deploy failure, the previously-deployed code stays\nlive; the runtime never serves a half-built bundle. The\n`deploy_error` field on the returned record carries the error\nthat came back from the runtime so you can surface it to users\nwithout polling.\n",
9479
+ "description": "Replaces the function's source code with the body's `code` and\ntriggers a redeploy. Same size limits as `POST /functions`.\nUse this verb to push secret writes into the running handler:\npassing the same `code` re-runs the deploy and refreshes the\nbinding set with the latest values from the secrets table.\n\nOn deploy failure, the previously-deployed code stays live; the\nruntime never serves a half-built bundle. The response uses\n`error.code` `deploy_failed`, and the function's `deploy_error`\nfield carries the latest deploy error for dashboard/API reads.\n",
8131
9480
  "hasJsonBody": true,
8132
9481
  "method": "PUT",
8133
9482
  "operationId": "updateFunction",
@@ -9386,7 +10735,7 @@ const CREDENTIALS_FILE = "credentials.json";
9386
10735
  const CREDENTIALS_LOCK_DIR = "credentials.lock";
9387
10736
  const CREDENTIALS_LOCK_STALE_MS = 1800 * 1e3;
9388
10737
  const MALFORMED_CREDENTIALS_HINT = "Run `primitive logout` and then `primitive login`.";
9389
- function isRecord(value) {
10738
+ function isRecord$1(value) {
9390
10739
  return value !== null && typeof value === "object" && !Array.isArray(value);
9391
10740
  }
9392
10741
  function requireString(value, key) {
@@ -9409,7 +10758,7 @@ var StaleCredentialFormatError = class extends Error {
9409
10758
  }
9410
10759
  };
9411
10760
  function parseCredentials(raw) {
9412
- if (!isRecord(raw)) throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
10761
+ if (!isRecord$1(raw)) throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
9413
10762
  if (typeof raw.api_base_url_1 !== "string" && typeof raw.base_url === "string") throw new StaleCredentialFormatError();
9414
10763
  const orgName = raw.org_name;
9415
10764
  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}`);
@@ -9582,7 +10931,7 @@ function formatFunctionEndpointRedirect(match) {
9582
10931
  return [
9583
10932
  "This is a function endpoint. Function endpoints are tested differently. Run:",
9584
10933
  "",
9585
- ` primitive functions:test-function --id ${match.functionId}`,
10934
+ ` primitive functions test --id ${match.functionId}`,
9586
10935
  "",
9587
10936
  `(pass the function id, not the endpoint id. endpoint_id=${match.endpointId} function_id=${match.functionId})`
9588
10937
  ].join("\n");
@@ -9753,26 +11102,26 @@ function coerceParameterValue(parameter, value) {
9753
11102
  if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") return value;
9754
11103
  throw new Errors.CLIError(`Unsupported flag value for --${parameter.name}`);
9755
11104
  }
9756
- function cliError$4(message) {
11105
+ function cliError$5(message) {
9757
11106
  return new Errors.CLIError(message, { exit: 1 });
9758
11107
  }
9759
11108
  function parseJson(source, flagLabel) {
9760
11109
  try {
9761
11110
  return JSON.parse(source);
9762
11111
  } catch (error) {
9763
- throw cliError$4(`${flagLabel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
11112
+ throw cliError$5(`${flagLabel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
9764
11113
  }
9765
11114
  }
9766
11115
  function readJsonBody(flags) {
9767
11116
  const bodyFile = flags["body-file"];
9768
11117
  const rawBody = flags["raw-body"];
9769
- if (bodyFile && rawBody) throw cliError$4("Use either --raw-body or --body-file, not both");
11118
+ if (bodyFile && rawBody) throw cliError$5("Use either --raw-body or --body-file, not both");
9770
11119
  if (typeof bodyFile === "string") {
9771
11120
  let contents;
9772
11121
  try {
9773
11122
  contents = readFileSync(bodyFile, "utf8");
9774
11123
  } catch (error) {
9775
- throw cliError$4(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
11124
+ throw cliError$5(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
9776
11125
  }
9777
11126
  return parseJson(contents, `--body-file ${bodyFile}`);
9778
11127
  }
@@ -9782,7 +11131,7 @@ function readTextFileFlag(path, flagLabel) {
9782
11131
  try {
9783
11132
  return readFileSync(path, "utf8");
9784
11133
  } catch (error) {
9785
- throw cliError$4(`Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`);
11134
+ throw cliError$5(`Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`);
9786
11135
  }
9787
11136
  }
9788
11137
  function extractErrorPayload(raw) {
@@ -9954,14 +11303,14 @@ function collectValues(parameters, flags) {
9954
11303
  return values;
9955
11304
  }
9956
11305
  const OPERATION_HINTS = {
9957
- createFunction: "Tip: prefer `primitive functions:deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
9958
- updateFunction: "Tip: prefer `primitive functions:redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
9959
- createFunctionSecret: "Tip: prefer `primitive functions:set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
9960
- setFunctionSecret: "Tip: prefer `primitive functions:set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON."
11306
+ createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
11307
+ updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
11308
+ createFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
11309
+ setFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON."
9961
11310
  };
9962
11311
  function createOperationCommand(operation) {
9963
11312
  const { flags, bodyFieldFlagToProperty } = buildFlags(operation);
9964
- const baseDescription = operation.description ?? `${operation.method} ${operation.path}`;
11313
+ const baseDescription = operation.description !== null && operation.description !== void 0 ? canonicalizeCliReferences(operation.description) : `${operation.method} ${operation.path}`;
9965
11314
  const schemaSummary = operation.hasJsonBody ? renderRequestSchemaSummary(operation.requestSchema) : null;
9966
11315
  const hint = OPERATION_HINTS[operation.sdkName];
9967
11316
  const descriptionWithSchema = schemaSummary ? `${baseDescription}\n\n${schemaSummary}` : baseDescription;
@@ -10067,12 +11416,15 @@ function createOperationCommand(operation) {
10067
11416
  return OperationCommand;
10068
11417
  }
10069
11418
  const EMPTY_RESULT_HINTS = {
10070
- listDeliveries: "(no results) No webhook deliveries logged yet. If you have an endpoint configured but expected to see test fires here: test deliveries from `primitive endpoints:test-endpoint` are NOT logged in this list, they're synchronous and visible only in the test-endpoint command's response. Real deliveries are logged when an inbound `email.received` event fans out to your endpoints. If you have no endpoints, run `primitive endpoints:list-endpoints` to check.",
10071
- listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints:create-endpoint --url <your-url>`.",
10072
- listEmails: "(no results) No inbound emails received yet on this account. Send one to a verified domain to populate this list. For a compact view, prefer `primitive emails:latest`.",
10073
- listDomains: "(no results) No domains on this account. Add one with `primitive domains:add-domain --domain <yourdomain.example>`.",
11419
+ listDeliveries: "(no results) No webhook deliveries logged yet. If you have an endpoint configured but expected to see test fires here: test deliveries from `primitive endpoints test` are NOT logged in this list, they're synchronous and visible only in the test-endpoint command's response. Real deliveries are logged when an inbound `email.received` event fans out to your endpoints. If you have no endpoints, run `primitive endpoints list` to check.",
11420
+ listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints create --url <your-url>`.",
11421
+ listEmails: "(no results) No inbound emails received yet on this account. Send one to a verified domain to populate this list. For a compact view, prefer `primitive emails latest`.",
11422
+ listDomains: "(no results) No domains on this account. Add one with `primitive domains add --domain <yourdomain.example>`.",
10074
11423
  listFilters: "(no results) No filter rules configured."
10075
11424
  };
11425
+ function canonicalizeCliReferences(description) {
11426
+ 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
+ }
10076
11428
  //#endregion
10077
11429
  //#region src/oclif/commands/doctor.ts
10078
11430
  const MIN_NODE_MAJOR = 22;
@@ -10233,7 +11585,7 @@ async function checkDomains(opts) {
10233
11585
  if (result.error) return {
10234
11586
  status: "warn",
10235
11587
  message: "could not list domains",
10236
- hint: "Run `primitive domains:list-domains` for the full error envelope."
11588
+ hint: "Run `primitive domains list` for the full error envelope."
10237
11589
  };
10238
11590
  const rows = result.data?.data ?? [];
10239
11591
  const active = rows.filter((row) => row.is_active === true);
@@ -10245,7 +11597,7 @@ async function checkDomains(opts) {
10245
11597
  if (active.length === 0) return {
10246
11598
  status: "warn",
10247
11599
  message: `${rows.length} domain(s), none active`,
10248
- hint: "Run `primitive domains:verify-domain --id <id>` for any domain you intend to send / receive on."
11600
+ hint: "Run `primitive domains verify --id <id>` for any domain you intend to send / receive on."
10249
11601
  };
10250
11602
  return {
10251
11603
  status: "ok",
@@ -10374,11 +11726,11 @@ function formatHeader(idWidth) {
10374
11726
  return `${"ID".padEnd(idWidth)} ${"RECEIVED (UTC)".padEnd(RECEIVED_DISPLAY_WIDTH)} ${"FROM".padEnd(ADDRESS_DISPLAY_WIDTH)} ${"TO".padEnd(ADDRESS_DISPLAY_WIDTH)} SUBJECT`;
10375
11727
  }
10376
11728
  var EmailsLatestCommand = class EmailsLatestCommand extends Command {
10377
- static description = `Print the N most recent inbound emails as a one-line-per-row text table. Designed for quick triage and visual scanning. For programmatic access, use \`primitive emails:list-emails\` (full JSON envelope, cursor pagination, filters) or pass \`--json\` here for the same raw shape without pagination/filters.
11729
+ static description = `Print the N most recent inbound emails as a one-line-per-row text table. Designed for quick triage and visual scanning. For programmatic access, use \`primitive emails list\` (full JSON envelope, cursor pagination, filters) or pass \`--json\` here for the same raw shape without pagination/filters.
10378
11730
 
10379
11731
  ID display is TTY-aware. When STDOUT is a terminal, the table truncates each row's id to the first ${ID_DISPLAY_WIDTH_SHORT} characters for readability. When STDOUT is piped or redirected (the row stream is being consumed by another command), the full UUID is printed so the id can be fed straight back into \`emails:get-email\`, \`emails:delete-email\`, etc. without a separate \`--json\` round-trip.
10380
11732
 
10381
- Output streams: the column header line is written to STDERR so the row data on STDOUT stays grep/awk-friendly. \`--json\` writes everything (including the envelope) to STDOUT and is equivalent to running \`emails:list-emails --limit N\` for the same N.`;
11733
+ Output streams: the column header line is written to STDERR so the row data on STDOUT stays grep/awk-friendly. \`--json\` writes everything (including the envelope) to STDOUT and is equivalent to running \`emails list --limit N\` for the same N.`;
10382
11734
  static summary = "Show the most recent inbound emails as a compact table";
10383
11735
  static examples = [
10384
11736
  "<%= config.bin %> emails latest",
@@ -10558,7 +11910,7 @@ function sleep$1(ms) {
10558
11910
  //#endregion
10559
11911
  //#region src/oclif/commands/emails-wait.ts
10560
11912
  const DEFAULT_WAIT_TIMEOUT_SECONDS$1 = 300;
10561
- function cliError$3(message) {
11913
+ function cliError$4(message) {
10562
11914
  return new Errors.CLIError(message, { exit: 1 });
10563
11915
  }
10564
11916
  var EmailsWaitCommand = class EmailsWaitCommand extends Command {
@@ -10638,7 +11990,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
10638
11990
  try {
10639
11991
  since = sinceFromFlags(flags);
10640
11992
  } catch (error) {
10641
- throw cliError$3(error instanceof Error ? error.message : String(error));
11993
+ throw cliError$4(error instanceof Error ? error.message : String(error));
10642
11994
  }
10643
11995
  const filters = filtersFromFlags(flags);
10644
11996
  const deadline = flags.timeout === 0 ? null : Date.now() + flags.timeout * 1e3;
@@ -10689,7 +12041,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
10689
12041
  };
10690
12042
  //#endregion
10691
12043
  //#region src/oclif/commands/emails-watch.ts
10692
- function cliError$2(message) {
12044
+ function cliError$3(message) {
10693
12045
  return new Errors.CLIError(message, { exit: 1 });
10694
12046
  }
10695
12047
  var EmailsWatchCommand = class EmailsWatchCommand extends Command {
@@ -10766,7 +12118,7 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
10766
12118
  try {
10767
12119
  since = sinceFromFlags(flags);
10768
12120
  } catch (error) {
10769
- throw cliError$2(error instanceof Error ? error.message : String(error));
12121
+ throw cliError$3(error instanceof Error ? error.message : String(error));
10770
12122
  }
10771
12123
  const filters = filtersFromFlags(flags);
10772
12124
  const deadline = flags.seconds ? Date.now() + flags.seconds * 1e3 : null;
@@ -10852,41 +12204,319 @@ function emitRawSendMailFetchWarning(bundleText, write) {
10852
12204
  //#endregion
10853
12205
  //#region src/oclif/secret-flags.ts
10854
12206
  const SECRET_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
10855
- function parseSecretFlags(raw) {
12207
+ function resolveSecretFlags(input) {
10856
12208
  const secrets = [];
10857
12209
  const seenKeys = /* @__PURE__ */ new Set();
10858
- for (const entry of raw) {
10859
- const eq = entry.indexOf("=");
10860
- if (eq === -1) return {
12210
+ const env = input.env ?? process.env;
12211
+ const readFile = input.readFile ?? defaultReadFile;
12212
+ const readStdin = input.readStdin ?? defaultReadStdin;
12213
+ const envFileCache = /* @__PURE__ */ new Map();
12214
+ const reserveSecretKey = (key, sourceLabel) => {
12215
+ const keyError = validateKey(key, sourceLabel);
12216
+ if (keyError) return keyError;
12217
+ if (seenKeys.has(key)) return duplicateKeyError(key);
12218
+ seenKeys.add(key);
12219
+ return null;
12220
+ };
12221
+ const addSecret = (key, value, sourceLabel) => {
12222
+ const keyError = reserveSecretKey(key, sourceLabel);
12223
+ if (keyError) return keyError;
12224
+ secrets.push({
12225
+ key,
12226
+ value
12227
+ });
12228
+ return null;
12229
+ };
12230
+ for (const entry of input.inline ?? []) {
12231
+ const parsed = parseKeyValueFlag(entry, "--secret");
12232
+ if (parsed.kind === "error") return parsed;
12233
+ const error = addSecret(parsed.key, parsed.value, "--secret");
12234
+ if (error) return error;
12235
+ }
12236
+ for (const key of input.fromEnv ?? []) {
12237
+ const keyError = reserveSecretKey(key, "--secret-from-env");
12238
+ if (keyError) return keyError;
12239
+ const value = env[key];
12240
+ if (value === void 0) return {
10861
12241
  kind: "error",
10862
- message: `--secret expects KEY=VALUE (got ${JSON.stringify(entry)}). Example: --secret API_TOKEN=abc123`
12242
+ message: `--secret-from-env ${key} could not read ${key}: environment variable is not set.`
10863
12243
  };
10864
- const key = entry.slice(0, eq);
10865
- const value = entry.slice(eq + 1);
10866
- if (key.length === 0) return {
12244
+ secrets.push({
12245
+ key,
12246
+ value
12247
+ });
12248
+ }
12249
+ for (const entry of input.fromFile ?? []) {
12250
+ const parsed = parseKeyValueFlag(entry, "--secret-from-file");
12251
+ if (parsed.kind === "error") return parsed;
12252
+ const keyError = reserveSecretKey(parsed.key, "--secret-from-file");
12253
+ if (keyError) return keyError;
12254
+ const file = readSecretFile(parsed.value, "--secret-from-file", readFile);
12255
+ if (file.kind === "error") return file;
12256
+ secrets.push({
12257
+ key: parsed.key,
12258
+ value: file.value
12259
+ });
12260
+ }
12261
+ for (const entry of input.fromEnvFile ?? []) {
12262
+ const parsed = parseEnvFileKeyRef(entry, "--secret-from-env-file");
12263
+ if (parsed.kind === "error") return parsed;
12264
+ const keyError = reserveSecretKey(parsed.key, "--secret-from-env-file");
12265
+ if (keyError) return keyError;
12266
+ const file = readEnvFile(parsed.path, readFile, envFileCache);
12267
+ if (file.kind === "error") return file;
12268
+ const value = file.values.get(parsed.key);
12269
+ if (value === void 0) return {
10867
12270
  kind: "error",
10868
- message: `--secret is missing a KEY before '=' (got ${JSON.stringify(entry)}). Example: --secret API_TOKEN=abc123`
12271
+ message: `--secret-from-env-file ${entry} could not read ${parsed.key}: key is not present in ${parsed.path}.`
10869
12272
  };
10870
- if (!SECRET_KEY_RE.test(key)) return {
12273
+ secrets.push({
12274
+ key: parsed.key,
12275
+ value
12276
+ });
12277
+ }
12278
+ if (input.fromStdin !== void 0) {
12279
+ const keyError = reserveSecretKey(input.fromStdin, "--secret-from-stdin");
12280
+ if (keyError) return keyError;
12281
+ const stdin = readSecretStdin("--secret-from-stdin", readStdin);
12282
+ if (stdin.kind === "error") return stdin;
12283
+ secrets.push({
12284
+ key: input.fromStdin,
12285
+ value: stdin.value
12286
+ });
12287
+ }
12288
+ return {
12289
+ kind: "ok",
12290
+ secrets
12291
+ };
12292
+ }
12293
+ function resolveSingleSecretValue(input) {
12294
+ if ([
12295
+ input.value !== void 0 ? "--value" : null,
12296
+ input.valueFromEnv !== void 0 ? "--value-from-env" : null,
12297
+ input.valueFile !== void 0 ? "--value-file" : null,
12298
+ input.valueFromEnvFile !== void 0 ? "--value-from-env-file" : null,
12299
+ input.stdin === true ? "--stdin" : null
12300
+ ].filter((v) => v !== null).length !== 1) return {
12301
+ kind: "error",
12302
+ message: "Pass exactly one of --value, --value-from-env, --value-file, --value-from-env-file, or --stdin."
12303
+ };
12304
+ const env = input.env ?? process.env;
12305
+ const readFile = input.readFile ?? defaultReadFile;
12306
+ const readStdin = input.readStdin ?? defaultReadStdin;
12307
+ if (input.value !== void 0) return {
12308
+ kind: "ok",
12309
+ value: input.value
12310
+ };
12311
+ if (input.valueFromEnv !== void 0) {
12312
+ const value = env[input.valueFromEnv];
12313
+ if (value === void 0) return {
10871
12314
  kind: "error",
10872
- message: `--secret KEY ${JSON.stringify(key)} does not match ${SECRET_KEY_RE.source} (uppercase letters, digits, underscores; first character is a letter or underscore).`
12315
+ message: `--value-from-env ${input.valueFromEnv} could not read ${input.valueFromEnv}: environment variable is not set.`
12316
+ };
12317
+ return {
12318
+ kind: "ok",
12319
+ value
10873
12320
  };
10874
- if (seenKeys.has(key)) return {
12321
+ }
12322
+ if (input.valueFile !== void 0) return readSecretFile(input.valueFile, "--value-file", readFile);
12323
+ if (input.valueFromEnvFile !== void 0) {
12324
+ const parsed = parseSingleValueEnvFileRef(input.valueFromEnvFile, input.key, "--value-from-env-file");
12325
+ if (parsed.kind === "error") return parsed;
12326
+ const file = readEnvFile(parsed.path, readFile, /* @__PURE__ */ new Map());
12327
+ if (file.kind === "error") return file;
12328
+ const value = file.values.get(parsed.key);
12329
+ if (value === void 0) return {
10875
12330
  kind: "error",
10876
- message: `--secret KEY ${JSON.stringify(key)} was passed more than once. Each key may only appear once per command.`
12331
+ message: `--value-from-env-file ${input.valueFromEnvFile} could not read ${parsed.key}: key is not present in ${parsed.path}.`
10877
12332
  };
10878
- seenKeys.add(key);
10879
- secrets.push({
10880
- key,
12333
+ return {
12334
+ kind: "ok",
10881
12335
  value
10882
- });
12336
+ };
10883
12337
  }
12338
+ if (input.stdin === true) return readSecretStdin("--stdin", readStdin);
12339
+ return {
12340
+ kind: "error",
12341
+ message: "Pass exactly one of --value, --value-from-env, --value-file, --value-from-env-file, or --stdin."
12342
+ };
12343
+ }
12344
+ function defaultReadFile(path) {
12345
+ return readFileSync(path, "utf8");
12346
+ }
12347
+ function defaultReadStdin() {
12348
+ 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
+ return readFileSync(0, "utf8");
12350
+ }
12351
+ function parseKeyValueFlag(entry, flagLabel) {
12352
+ const eq = entry.indexOf("=");
12353
+ if (eq === -1) return {
12354
+ kind: "error",
12355
+ message: `${flagLabel} expects KEY=VALUE (got ${JSON.stringify(entry)}). Example: ${flagLabel} API_TOKEN=abc123`
12356
+ };
12357
+ const key = entry.slice(0, eq);
12358
+ const value = entry.slice(eq + 1);
12359
+ if (key.length === 0) return {
12360
+ kind: "error",
12361
+ message: `${flagLabel} is missing a KEY before '=' (got ${JSON.stringify(entry)}). Example: ${flagLabel} API_TOKEN=abc123`
12362
+ };
10884
12363
  return {
10885
12364
  kind: "ok",
10886
- secrets
12365
+ key,
12366
+ value
12367
+ };
12368
+ }
12369
+ function validateKey(key, flagLabel) {
12370
+ if (!SECRET_KEY_RE.test(key)) return {
12371
+ kind: "error",
12372
+ message: `${flagLabel} KEY ${JSON.stringify(key)} does not match ${SECRET_KEY_RE.source} (uppercase letters, digits, underscores; first character is a letter or underscore).`
12373
+ };
12374
+ return null;
12375
+ }
12376
+ function duplicateKeyError(key) {
12377
+ return {
12378
+ kind: "error",
12379
+ message: `Secret KEY ${JSON.stringify(key)} was passed more than once. Each key may only appear once per command.`
12380
+ };
12381
+ }
12382
+ function readSecretFile(path, flagLabel, readFile) {
12383
+ try {
12384
+ return {
12385
+ kind: "ok",
12386
+ value: readFile(path)
12387
+ };
12388
+ } catch (error) {
12389
+ return {
12390
+ kind: "error",
12391
+ message: `Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`
12392
+ };
12393
+ }
12394
+ }
12395
+ function readSecretStdin(flagLabel, readStdin) {
12396
+ try {
12397
+ return {
12398
+ kind: "ok",
12399
+ value: stripOneTrailingLineEnding(readStdin())
12400
+ };
12401
+ } catch (error) {
12402
+ return {
12403
+ kind: "error",
12404
+ message: `Could not read ${flagLabel}: ${error instanceof Error ? error.message : String(error)}`
12405
+ };
12406
+ }
12407
+ }
12408
+ function stripOneTrailingLineEnding(value) {
12409
+ if (!value.endsWith("\n")) return value;
12410
+ const withoutLf = value.slice(0, -1);
12411
+ return withoutLf.endsWith("\r") ? withoutLf.slice(0, -1) : withoutLf;
12412
+ }
12413
+ function parseEnvFileKeyRef(entry, flagLabel) {
12414
+ const sep = entry.lastIndexOf(":");
12415
+ if (sep <= 0 || sep === entry.length - 1) return {
12416
+ kind: "error",
12417
+ message: `${flagLabel} expects FILE:KEY (got ${JSON.stringify(entry)}). Example: ${flagLabel} .env.local:OPENAI_KEY`
12418
+ };
12419
+ const path = entry.slice(0, sep);
12420
+ const key = entry.slice(sep + 1);
12421
+ const keyError = validateKey(key, flagLabel);
12422
+ if (keyError) return keyError;
12423
+ return {
12424
+ kind: "ok",
12425
+ key,
12426
+ path
12427
+ };
12428
+ }
12429
+ function parseSingleValueEnvFileRef(entry, fallbackKey, flagLabel) {
12430
+ const sep = entry.lastIndexOf(":");
12431
+ if (sep === -1) return {
12432
+ kind: "ok",
12433
+ key: fallbackKey,
12434
+ path: entry
12435
+ };
12436
+ if (sep <= 0 || sep === entry.length - 1) return {
12437
+ kind: "error",
12438
+ message: `${flagLabel} expects FILE or FILE:KEY (got ${JSON.stringify(entry)}). Example: ${flagLabel} .env.local or ${flagLabel} .env.local:OPENAI_KEY`
12439
+ };
12440
+ const path = entry.slice(0, sep);
12441
+ const key = entry.slice(sep + 1);
12442
+ const keyError = validateKey(key, flagLabel);
12443
+ if (keyError) return keyError;
12444
+ return {
12445
+ kind: "ok",
12446
+ key,
12447
+ path
12448
+ };
12449
+ }
12450
+ function readEnvFile(path, readFile, cache) {
12451
+ const cached = cache.get(path);
12452
+ if (cached) return {
12453
+ kind: "ok",
12454
+ values: cached
12455
+ };
12456
+ let contents;
12457
+ try {
12458
+ contents = readFile(path);
12459
+ } catch (error) {
12460
+ return {
12461
+ kind: "error",
12462
+ message: `Could not read env file ${path}: ${error instanceof Error ? error.message : String(error)}`
12463
+ };
12464
+ }
12465
+ const values = parseEnvFile(contents);
12466
+ cache.set(path, values);
12467
+ return {
12468
+ kind: "ok",
12469
+ values
10887
12470
  };
10888
12471
  }
10889
- const SECRET_FLAG_SECURITY_NOTE = "Note: values passed on the command line are visible in shell history (e.g. ~/.bash_history) and to other users via `ps aux` / /proc/[pid]/cmdline. For sensitive values prefer `--secret KEY=\"$VAR\"` where `$VAR` is set out-of-band (read -s, a secrets manager, etc.).";
12472
+ function parseEnvFile(contents) {
12473
+ const values = /* @__PURE__ */ new Map();
12474
+ const normalized = contents.replace(/^\uFEFF/, "");
12475
+ for (const rawLine of normalized.split(/\r?\n/)) {
12476
+ let line = rawLine.trimStart();
12477
+ if (line.length === 0 || line.startsWith("#")) continue;
12478
+ if (line.startsWith("export ")) line = line.slice(7).trimStart();
12479
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
12480
+ if (!match) continue;
12481
+ values.set(match[1], parseEnvValue(match[2] ?? ""));
12482
+ }
12483
+ return values;
12484
+ }
12485
+ function parseEnvValue(raw) {
12486
+ const value = raw.trimStart();
12487
+ if (value.startsWith("'")) {
12488
+ const end = value.indexOf("'", 1);
12489
+ return end === -1 ? value.slice(1) : value.slice(1, end);
12490
+ }
12491
+ if (value.startsWith("\"")) return parseDoubleQuotedEnvValue(value);
12492
+ return value.replace(/\s+#.*$/, "").trimEnd();
12493
+ }
12494
+ function parseDoubleQuotedEnvValue(value) {
12495
+ let out = "";
12496
+ let escaped = false;
12497
+ for (let i = 1; i < value.length; i++) {
12498
+ const ch = value[i];
12499
+ if (escaped) {
12500
+ if (ch === "n") out += "\n";
12501
+ else if (ch === "r") out += "\r";
12502
+ else if (ch === "t") out += " ";
12503
+ else out += ch;
12504
+ escaped = false;
12505
+ continue;
12506
+ }
12507
+ if (ch === "\\") {
12508
+ escaped = true;
12509
+ continue;
12510
+ }
12511
+ if (ch === "\"") break;
12512
+ out += ch;
12513
+ }
12514
+ if (escaped) out += "\\";
12515
+ return out;
12516
+ }
12517
+ const SECRET_FLAG_SECURITY_NOTE = "Note: values passed on the command line are visible in shell history (e.g. ~/.bash_history) and to other users via `ps aux` / /proc/[pid]/cmdline. For sensitive values prefer --secret-from-env, --secret-from-file, --secret-from-env-file, or --secret-from-stdin.";
12518
+ const SECRET_SOURCE_FLAGS_DESCRIPTION = "Safe sources: --secret-from-env KEY reads process.env[KEY], --secret-from-file KEY=PATH reads the full UTF-8 file contents, --secret-from-env-file FILE:KEY reads KEY from a dotenv-style file, and --secret-from-stdin KEY reads the value from stdin.";
12519
+ const SINGLE_SECRET_VALUE_SOURCE_DESCRIPTION = "Instead of --value, use --value-from-env ENV_VAR, --value-file PATH, --value-from-env-file FILE[:KEY], or --stdin to avoid putting the secret value in shell history or process argv. If KEY is omitted from --value-from-env-file, the command's --key is used.";
10890
12520
  //#endregion
10891
12521
  //#region src/oclif/commands/functions-deploy.ts
10892
12522
  async function runDeployWithSecrets(api, params) {
@@ -10988,22 +12618,23 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
10988
12618
  \`functions:create-function\` if you need the full flag surface
10989
12619
  (raw-body JSON, etc.).
10990
12620
 
10991
- Pass --secret KEY=VALUE (repeatable) to seed secret bindings in the
10992
- same command. Keys must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase
10993
- letters, digits, underscores; first character is a letter or
10994
- underscore). With one or more --secret flags the deploy fans out to
10995
- multiple API calls: create-function, set-secret per pair, then a
10996
- final update-function with the same bundle so the running handler
10997
- picks up the bindings. If a secret write fails after the create
10998
- step the function exists with whatever secrets succeeded and the
10999
- redeploy has NOT fired; re-run \`primitive functions:set-secret\`
11000
- for the missing keys, then \`primitive functions:redeploy\` to
11001
- push them live.`;
12621
+ Pass secret source flags to seed bindings in the same command. Keys
12622
+ must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters, digits,
12623
+ underscores; first character is a letter or underscore). With one
12624
+ or more secrets the deploy fans out to multiple API calls:
12625
+ create-function, set-secret per pair, then a final update-function
12626
+ with the same bundle so the running handler picks up the bindings.
12627
+ If a secret write fails after the create step the function exists
12628
+ with whatever secrets succeeded and the redeploy has NOT fired;
12629
+ re-run \`primitive functions set-secret\` for the missing keys, then
12630
+ \`primitive functions redeploy\` to push them live. ${SECRET_SOURCE_FLAGS_DESCRIPTION}`;
11002
12631
  static summary = "Deploy a new function from a bundled handler file";
11003
12632
  static examples = [
11004
- "<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js",
11005
- "<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
11006
- "<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com"
12633
+ "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js",
12634
+ "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
12635
+ "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
12636
+ "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret-from-env OPENAI_KEY --secret-from-env-file .env.local:OWNER_EMAIL",
12637
+ "printf '%s' \"$OPENAI_KEY\" | <%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret-from-stdin OPENAI_KEY"
11007
12638
  ];
11008
12639
  static flags = {
11009
12640
  "api-key": Flags.string({
@@ -11028,17 +12659,36 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
11028
12659
  description: "Path to the bundled ESM handler file (single self-contained module). Loaded as the `code` body field.",
11029
12660
  required: true
11030
12661
  }),
11031
- "source-map-file": Flags.string({ description: "Optional path to a source map for the bundle. Stored only on the runtime side and used to symbolicate stack traces." }),
12662
+ "source-map-file": Flags.string({ description: "Optional path to a source map for the bundle. Stored with the deployment attempt and used to symbolicate stack traces in function logs." }),
11032
12663
  secret: Flags.string({
11033
12664
  description: `Secret KEY=VALUE to seed on the deployed function. Repeatable. KEY must match \`^[A-Z_][A-Z0-9_]*$\`; VALUE may contain \`=\` (only the first \`=\` is treated as a delimiter). Each KEY may only appear once per command. Passing one or more --secret flags fans out the deploy to create-function, set-secret per pair, then a final redeploy so the running handler picks up the bindings. ${SECRET_FLAG_SECURITY_NOTE}`,
11034
12665
  multiple: true
11035
12666
  }),
12667
+ "secret-from-env": Flags.string({
12668
+ description: "Secret KEY to read from the environment and seed on the deployed function. Repeatable. Example: --secret-from-env OPENAI_KEY reads process.env.OPENAI_KEY.",
12669
+ multiple: true
12670
+ }),
12671
+ "secret-from-file": Flags.string({
12672
+ description: "Secret KEY=PATH to read from a UTF-8 file and seed on the deployed function. Repeatable. The full file contents become the value.",
12673
+ multiple: true
12674
+ }),
12675
+ "secret-from-env-file": Flags.string({
12676
+ description: "Secret FILE:KEY to read from a dotenv-style file and seed on the deployed function. Repeatable. Example: --secret-from-env-file .env.local:OPENAI_KEY.",
12677
+ multiple: true
12678
+ }),
12679
+ "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." }),
11036
12680
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
11037
12681
  };
11038
12682
  async run() {
11039
12683
  const { flags } = await this.parse(FunctionsDeployCommand);
11040
12684
  await runWithTiming(flags.time, async () => {
11041
- const parsedSecrets = parseSecretFlags(flags.secret ?? []);
12685
+ const parsedSecrets = resolveSecretFlags({
12686
+ fromEnv: flags["secret-from-env"] ?? [],
12687
+ fromEnvFile: flags["secret-from-env-file"] ?? [],
12688
+ fromFile: flags["secret-from-file"] ?? [],
12689
+ fromStdin: flags["secret-from-stdin"],
12690
+ inline: flags.secret ?? []
12691
+ });
11042
12692
  if (parsedSecrets.kind === "error") {
11043
12693
  process.stderr.write(`${parsedSecrets.message}\n`);
11044
12694
  process.exitCode = 1;
@@ -11103,10 +12753,10 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
11103
12753
  const succeeded = outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)";
11104
12754
  const pending = outcome.pendingKeys.length > 0 ? outcome.pendingKeys.join(", ") : "(none)";
11105
12755
  const allMissing = [outcome.failedKey, ...outcome.pendingKeys].join(", ");
11106
- process.stderr.write(`Function ${outcome.created.name} (${outcome.created.id}) was created, but writing secret ${outcome.failedKey} failed; succeeded keys so far: ${succeeded}; keys not yet attempted: ${pending}. The redeploy is NOT yet live. Re-run \`primitive functions:set-secret\` for each of [${allMissing}], then \`primitive functions:redeploy --id ${outcome.created.id} --file <bundle>\` to push them live.\n`);
12756
+ process.stderr.write(`Function ${outcome.created.name} (${outcome.created.id}) was created, but writing secret ${outcome.failedKey} failed; succeeded keys so far: ${succeeded}; keys not yet attempted: ${pending}. The redeploy is NOT yet live. Re-run \`primitive functions set-secret\` for each of [${allMissing}], then \`primitive functions redeploy --id ${outcome.created.id} --file <bundle>\` to push them live.\n`);
11107
12757
  } else if (outcome.stage === "redeploy") {
11108
12758
  const succeeded = outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)";
11109
- process.stderr.write(`Function ${outcome.created.name} (${outcome.created.id}) was created and secrets [${succeeded}] were written, but the final redeploy failed; the new bindings are NOT yet live. Re-run \`primitive functions:redeploy --id ${outcome.created.id} --file <bundle>\` once the cause is fixed.\n`);
12759
+ process.stderr.write(`Function ${outcome.created.name} (${outcome.created.id}) was created and secrets [${succeeded}] were written, but the final redeploy failed; the new bindings are NOT yet live. Re-run \`primitive functions redeploy --id ${outcome.created.id} --file <bundle>\` once the cause is fixed.\n`);
11110
12760
  }
11111
12761
  writeErrorWithHints(outcome.payload);
11112
12762
  removeStaleSavedCredentialOnUnauthorized({
@@ -11122,14 +12772,16 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
11122
12772
  }
11123
12773
  };
11124
12774
  //#endregion
11125
- //#region src/oclif/commands/functions-init.ts
11126
- const SDK_VERSION_RANGE = "^0.27.1";
11127
- const CLI_VERSION_RANGE = "^0.26.0";
12775
+ //#region src/oclif/function-templates.ts
12776
+ const DEFAULT_FUNCTION_TEMPLATE_ID = "email-reply";
12777
+ const PRIMITIVE_TEAM_AUTHOR = {
12778
+ id: "primitive-team",
12779
+ name: "Primitive Team",
12780
+ url: "https://primitive.dev"
12781
+ };
12782
+ const SDK_VERSION_RANGE = "^0.29.0";
12783
+ const CLI_VERSION_RANGE = "^0.29.0";
11128
12784
  const ESBUILD_VERSION_RANGE = "^0.27.0";
11129
- const VALID_NAME = /^[a-z0-9][a-z0-9_-]{0,62}$/;
11130
- function isValidFunctionName(name) {
11131
- return VALID_NAME.test(name);
11132
- }
11133
12785
  function renderHandler() {
11134
12786
  return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
11135
12787
  //
@@ -11269,8 +12921,8 @@ function renderPackageJson(name) {
11269
12921
  type: "module",
11270
12922
  scripts: {
11271
12923
  build: "node build.mjs",
11272
- deploy: `npm run build && primitive functions:deploy --name ${name} --file ./dist/handler.js`,
11273
- redeploy: "npm run build && primitive functions:redeploy --id $PRIMITIVE_FUNCTION_ID --file ./dist/handler.js"
12924
+ deploy: `npm run build && primitive functions deploy --name ${name} --file ./dist/handler.js`,
12925
+ redeploy: "npm run build && primitive functions redeploy --id $PRIMITIVE_FUNCTION_ID --file ./dist/handler.js"
11274
12926
  },
11275
12927
  dependencies: { "@primitivedotdev/sdk": SDK_VERSION_RANGE },
11276
12928
  devDependencies: {
@@ -11341,7 +12993,7 @@ npm run build
11341
12993
  npm run deploy
11342
12994
  \`\`\`
11343
12995
 
11344
- The deploy step calls \`primitive functions:deploy\` (provided by the
12996
+ The deploy step calls \`primitive functions deploy\` (provided by the
11345
12997
  \`@primitivedotdev/cli\` package; install with
11346
12998
  \`npm install -g @primitivedotdev/cli\` or run via
11347
12999
  \`npx @primitivedotdev/cli@latest <command>\`). It requires
@@ -11350,7 +13002,7 @@ Run \`primitive login\` once to save a key in your CLI config if you
11350
13002
  prefer that to an env var.
11351
13003
  `;
11352
13004
  }
11353
- function scaffoldFiles(name) {
13005
+ function renderEmailReplyTemplateFiles(name) {
11354
13006
  return [
11355
13007
  {
11356
13008
  contents: renderHandler(),
@@ -11378,9 +13030,79 @@ function scaffoldFiles(name) {
11378
13030
  }
11379
13031
  ];
11380
13032
  }
13033
+ const FUNCTION_TEMPLATES = [{
13034
+ author: PRIMITIVE_TEAM_AUTHOR,
13035
+ dependencies: ["@primitivedotdev/sdk"],
13036
+ description: "A deployable TypeScript email handler that validates email.received events, skips likely loops, and replies with the Primitive SDK.",
13037
+ devDependencies: [
13038
+ "@primitivedotdev/cli",
13039
+ "esbuild",
13040
+ "typescript"
13041
+ ],
13042
+ files: ({ name }) => renderEmailReplyTemplateFiles(name),
13043
+ id: DEFAULT_FUNCTION_TEMPLATE_ID,
13044
+ secrets: [],
13045
+ summary: "Reply to inbound email with the Primitive SDK.",
13046
+ tags: [
13047
+ "email",
13048
+ "reply",
13049
+ "typescript",
13050
+ "worker"
13051
+ ],
13052
+ title: "Email Reply"
13053
+ }];
13054
+ function functionTemplateIds(templates) {
13055
+ return templates.map((template) => template.id);
13056
+ }
13057
+ function findFunctionTemplate(templates, id) {
13058
+ return templates.find((template) => template.id === id) ?? null;
13059
+ }
13060
+ function serializeFunctionTemplate(template) {
13061
+ return {
13062
+ author: { ...template.author },
13063
+ dependencies: [...template.dependencies],
13064
+ description: template.description,
13065
+ devDependencies: [...template.devDependencies],
13066
+ id: template.id,
13067
+ secrets: [...template.secrets],
13068
+ summary: template.summary,
13069
+ tags: [...template.tags],
13070
+ title: template.title
13071
+ };
13072
+ }
13073
+ function formatFunctionTemplateList(templates) {
13074
+ const lines = ["Available Primitive Function templates:", ""];
13075
+ for (const template of templates) {
13076
+ lines.push(`${template.id}`);
13077
+ lines.push(` title: ${template.title}`);
13078
+ lines.push(` author: ${template.author.name}`);
13079
+ lines.push(` summary: ${template.summary}`);
13080
+ lines.push(` tags: ${template.tags.length > 0 ? template.tags.join(", ") : "none"}`);
13081
+ lines.push(` secrets: ${template.secrets.length > 0 ? template.secrets.join(", ") : "none"}`);
13082
+ lines.push("");
13083
+ }
13084
+ lines.push("Use `primitive functions init <name> --template <id>`.");
13085
+ return lines.join("\n");
13086
+ }
13087
+ //#endregion
13088
+ //#region src/oclif/commands/functions-init.ts
13089
+ const VALID_NAME = /^[a-z0-9][a-z0-9_-]{0,62}$/;
13090
+ function isValidFunctionName(name) {
13091
+ return VALID_NAME.test(name);
13092
+ }
13093
+ function unknownTemplateError(templateId) {
13094
+ const available = functionTemplateIds(FUNCTION_TEMPLATES).join(", ");
13095
+ return new Errors.CLIError(`Unknown function template "${templateId}". Available templates: ${available}. Run \`primitive functions templates\` for details.`, { exit: 1 });
13096
+ }
13097
+ function scaffoldFiles(name, templateId = DEFAULT_FUNCTION_TEMPLATE_ID) {
13098
+ const template = findFunctionTemplate(FUNCTION_TEMPLATES, templateId);
13099
+ if (!template) throw unknownTemplateError(templateId);
13100
+ return template.files({ name });
13101
+ }
11381
13102
  function writeScaffold(params) {
11382
13103
  if (!isValidFunctionName(params.name)) throw new Errors.CLIError(`Invalid function name "${params.name}". Use lowercase letters, digits, hyphens, or underscores (1-63 chars, must start with a letter or digit).`, { exit: 1 });
11383
- const files = scaffoldFiles(params.name);
13104
+ const templateId = params.templateId ?? "email-reply";
13105
+ const files = scaffoldFiles(params.name, templateId);
11384
13106
  const written = [];
11385
13107
  try {
11386
13108
  mkdirSync(params.outDir, { recursive: false });
@@ -11411,36 +13133,49 @@ function writeScaffold(params) {
11411
13133
  return { written };
11412
13134
  }
11413
13135
  var FunctionsInitCommand = class FunctionsInitCommand extends Command {
11414
- static description = `Scaffold a new Primitive Function project in ./<name>/ with handler.ts, package.json, build.mjs, tsconfig.json, .gitignore, and README.md.
13136
+ static description = `Scaffold a new Primitive Function project from a Primitive-owned template.
11415
13137
 
11416
13138
  The scaffolded handler imports \`createPrimitiveClient\` from
11417
13139
  \`@primitivedotdev/sdk/api\` and demonstrates the canonical pattern:
11418
13140
  parse the email.received event, send a reply via the SDK, return a
11419
13141
  JSON envelope. The build script uses esbuild's JS API and emits
11420
- ./dist/handler.js, ready to hand to \`primitive functions:deploy --file\`.
13142
+ ./dist/handler.js, ready to hand to \`primitive functions deploy --file\`.
11421
13143
 
11422
13144
  Refuses to overwrite an existing directory. Use --out-dir to pick a
11423
- different target path than ./<name>/.`;
11424
- static summary = "Scaffold a new Primitive Function project ready for functions:deploy";
11425
- static examples = ["<%= config.bin %> functions:init my-fn", "<%= config.bin %> functions:init my-fn --out-dir ./functions/my-fn"];
13145
+ different target path than ./<name>/. Run \`primitive functions templates\`
13146
+ to inspect available templates.`;
13147
+ static summary = "Scaffold a new Primitive Function project ready for functions deploy";
13148
+ static examples = [
13149
+ "<%= config.bin %> functions init my-fn",
13150
+ "<%= config.bin %> functions init my-fn --template email-reply",
13151
+ "<%= config.bin %> functions init my-fn --out-dir ./functions/my-fn"
13152
+ ];
11426
13153
  static args = { name: Args.string({
11427
13154
  description: "Function name. Lowercase letters, digits, hyphens, underscores. 1-63 chars. Used as the directory name (when --out-dir is not set) and as the package.json name.",
11428
13155
  required: true
11429
13156
  }) };
11430
- static flags = { "out-dir": Flags.string({ description: "Directory to scaffold into. Defaults to ./<name>/. Must not already exist." }) };
13157
+ static flags = {
13158
+ "out-dir": Flags.string({ description: "Directory to scaffold into. Defaults to ./<name>/. Must not already exist." }),
13159
+ template: Flags.string({
13160
+ default: DEFAULT_FUNCTION_TEMPLATE_ID,
13161
+ description: "Function template id. Run `primitive functions templates` to list templates.",
13162
+ options: functionTemplateIds(FUNCTION_TEMPLATES)
13163
+ })
13164
+ };
11431
13165
  async run() {
11432
13166
  const { args, flags } = await this.parse(FunctionsInitCommand);
11433
13167
  const outDir = resolve(flags["out-dir"] ?? `./${args.name}`);
11434
13168
  writeScaffold({
11435
13169
  name: args.name,
11436
- outDir
13170
+ outDir,
13171
+ templateId: flags.template
11437
13172
  });
11438
- this.log(`Scaffolded ${outDir}.`);
13173
+ this.log(`Scaffolded ${outDir} from ${flags.template} template.`);
11439
13174
  this.log("Next:");
11440
13175
  this.log(` cd ${outDir}`);
11441
13176
  this.log(" npm install");
11442
13177
  this.log(" npm run build");
11443
- this.log(` primitive functions:deploy --name ${args.name} --file ./dist/handler.js`);
13178
+ this.log(` primitive functions deploy --name ${args.name} --file ./dist/handler.js`);
11444
13179
  }
11445
13180
  };
11446
13181
  //#endregion
@@ -11516,17 +13251,19 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
11516
13251
  the bindings table fresh on every call, so passing the existing
11517
13252
  bundle picks up any secret writes since the last deploy.
11518
13253
 
11519
- Pass --secret KEY=VALUE (repeatable) to write secrets BEFORE the
11520
- redeploy fires; one update-function call then refreshes every new
11521
- binding. Keys must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters,
11522
- digits, underscores; first character is a letter or underscore).
11523
- With one or more --secret flags the redeploy fans out to multiple
11524
- API calls (set-secret per pair, then update-function).`;
13254
+ Pass secret source flags to write secrets BEFORE the redeploy fires;
13255
+ one update-function call then refreshes every new binding. Keys must
13256
+ match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters, digits, underscores;
13257
+ first character is a letter or underscore). With one or more secrets
13258
+ the redeploy fans out to multiple API calls (set-secret per pair,
13259
+ then update-function). ${SECRET_SOURCE_FLAGS_DESCRIPTION}`;
11525
13260
  static summary = "Redeploy a function from a bundled handler file";
11526
13261
  static examples = [
11527
- "<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js",
11528
- "<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js --source-map-file ./bundle.js.map",
11529
- "<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com"
13262
+ "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js",
13263
+ "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --source-map-file ./bundle.js.map",
13264
+ "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
13265
+ "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --secret-from-env OPENAI_KEY --secret-from-file PRIVATE_KEY=./private-key.pem",
13266
+ "printf '%s' \"$OPENAI_KEY\" | <%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --secret-from-stdin OPENAI_KEY"
11530
13267
  ];
11531
13268
  static flags = {
11532
13269
  "api-key": Flags.string({
@@ -11556,12 +13293,31 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
11556
13293
  description: `Secret KEY=VALUE to write on the function before the redeploy fires. Repeatable. KEY must match \`^[A-Z_][A-Z0-9_]*$\`; VALUE may contain \`=\` (only the first \`=\` is treated as a delimiter). Each KEY may only appear once per command. Passing one or more --secret flags fans out to set-secret per pair then a single update-function call so the new bindings land in the same redeploy. ${SECRET_FLAG_SECURITY_NOTE}`,
11557
13294
  multiple: true
11558
13295
  }),
13296
+ "secret-from-env": Flags.string({
13297
+ description: "Secret KEY to read from the environment and write before the redeploy. Repeatable. Example: --secret-from-env OPENAI_KEY reads process.env.OPENAI_KEY.",
13298
+ multiple: true
13299
+ }),
13300
+ "secret-from-file": Flags.string({
13301
+ description: "Secret KEY=PATH to read from a UTF-8 file and write before the redeploy. Repeatable. The full file contents become the value.",
13302
+ multiple: true
13303
+ }),
13304
+ "secret-from-env-file": Flags.string({
13305
+ description: "Secret FILE:KEY to read from a dotenv-style file and write before the redeploy. Repeatable. Example: --secret-from-env-file .env.local:OPENAI_KEY.",
13306
+ multiple: true
13307
+ }),
13308
+ "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." }),
11559
13309
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
11560
13310
  };
11561
13311
  async run() {
11562
13312
  const { flags } = await this.parse(FunctionsRedeployCommand);
11563
13313
  await runWithTiming(flags.time, async () => {
11564
- const parsedSecrets = parseSecretFlags(flags.secret ?? []);
13314
+ const parsedSecrets = resolveSecretFlags({
13315
+ fromEnv: flags["secret-from-env"] ?? [],
13316
+ fromEnvFile: flags["secret-from-env-file"] ?? [],
13317
+ fromFile: flags["secret-from-file"] ?? [],
13318
+ fromStdin: flags["secret-from-stdin"],
13319
+ inline: flags.secret ?? []
13320
+ });
11565
13321
  if (parsedSecrets.kind === "error") {
11566
13322
  process.stderr.write(`${parsedSecrets.message}\n`);
11567
13323
  process.exitCode = 1;
@@ -11617,10 +13373,10 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
11617
13373
  const succeeded = outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)";
11618
13374
  const pending = outcome.pendingKeys.length > 0 ? outcome.pendingKeys.join(", ") : "(none)";
11619
13375
  const allMissing = [outcome.failedKey, ...outcome.pendingKeys].join(", ");
11620
- process.stderr.write(`Writing secret ${outcome.failedKey} failed before the redeploy; succeeded keys so far: ${succeeded}; keys not yet attempted: ${pending}. The new bundle has NOT been deployed. Re-run \`primitive functions:set-secret\` for each of [${allMissing}], then \`primitive functions:redeploy --id ${flags.id} --file <bundle>\` to push them live.\n`);
13376
+ process.stderr.write(`Writing secret ${outcome.failedKey} failed before the redeploy; succeeded keys so far: ${succeeded}; keys not yet attempted: ${pending}. The new bundle has NOT been deployed. Re-run \`primitive functions set-secret\` for each of [${allMissing}], then \`primitive functions redeploy --id ${flags.id} --file <bundle>\` to push them live.\n`);
11621
13377
  } else if (outcome.stage === "redeploy") {
11622
13378
  const succeeded = outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)";
11623
- process.stderr.write(`Secrets [${succeeded}] were written, but the redeploy step failed; the new bindings are NOT yet live. Re-run \`primitive functions:redeploy --id ${flags.id} --file <bundle>\` once the cause is fixed.\n`);
13379
+ process.stderr.write(`Secrets [${succeeded}] were written, but the redeploy step failed; the new bindings are NOT yet live. Re-run \`primitive functions redeploy --id ${flags.id} --file <bundle>\` once the cause is fixed.\n`);
11624
13380
  }
11625
13381
  writeErrorWithHints(outcome.payload);
11626
13382
  removeStaleSavedCredentialOnUnauthorized({
@@ -11702,7 +13458,7 @@ async function runSetSecret(api, params) {
11702
13458
  };
11703
13459
  }
11704
13460
  var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command {
11705
- static description = `Write a function secret and optionally redeploy so the new value lands in the running handler. Agent-grade shortcut for functions:set-function-secret + functions:redeploy.
13461
+ static description = `Write a function secret and optionally redeploy so the new value lands in the running handler. Agent-grade shortcut for functions set-function-secret + functions redeploy.
11706
13462
 
11707
13463
  Without --redeploy this is a plain secret upsert: the value is
11708
13464
  encrypted at rest but is NOT visible to the running handler until
@@ -11712,9 +13468,15 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
11712
13468
 
11713
13469
  Keys must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters, digits,
11714
13470
  underscores; first character is a letter or underscore). System-
11715
- managed keys are reserved and rejected.`;
13471
+ managed keys are reserved and rejected. ${SINGLE_SECRET_VALUE_SOURCE_DESCRIPTION}`;
11716
13472
  static summary = "Write a function secret (optionally redeploying to push it live)";
11717
- static examples = ["<%= config.bin %> functions:set-secret --id <fn-id> --key API_TOKEN --value abc123", "<%= config.bin %> functions:set-secret --id <fn-id> --key API_TOKEN --value abc123 --redeploy"];
13473
+ static examples = [
13474
+ "<%= config.bin %> functions set-secret --id <fn-id> --key API_TOKEN --value abc123",
13475
+ "<%= config.bin %> functions set-secret --id <fn-id> --key API_TOKEN --value abc123 --redeploy",
13476
+ "<%= config.bin %> functions set-secret --id <fn-id> --key OPENAI_KEY --value-from-env OPENAI_KEY --redeploy",
13477
+ "<%= config.bin %> functions set-secret --id <fn-id> --key OPENAI_KEY --value-from-env-file .env.local --redeploy",
13478
+ "printf '%s' \"$OPENAI_KEY\" | <%= config.bin %> functions set-secret --id <fn-id> --key OPENAI_KEY --stdin --redeploy"
13479
+ ];
11718
13480
  static flags = {
11719
13481
  "api-key": Flags.string({
11720
13482
  description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
@@ -11738,11 +13500,12 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
11738
13500
  description: "Secret key. Uppercase letters, digits, underscores; must start with a letter or underscore. System-managed keys are reserved.",
11739
13501
  required: true
11740
13502
  }),
11741
- value: Flags.string({
11742
- description: "Secret value (up to 4096 UTF-8 bytes). Encrypted at rest.",
11743
- required: true
11744
- }),
11745
- redeploy: Flags.boolean({ description: "Also redeploy the function with its current code so the new value lands in the running handler. Without this, the secret is written but not visible to the handler until the next deploy. Note: source maps are stored only on the runtime side and getFunction does not return them, so this redeploy drops any previously-uploaded source map. If preserving stack-trace symbolication matters, use `functions:redeploy --file <bundle.js> --source-map-file <bundle.js.map>` instead." }),
13503
+ value: Flags.string({ description: "Secret value (up to 4096 UTF-8 bytes). Encrypted at rest. Visible in shell history and process argv; prefer a non-argv source for sensitive values." }),
13504
+ "value-from-env": Flags.string({ description: "Environment variable to read as the secret value. Example: --value-from-env OPENAI_KEY reads process.env.OPENAI_KEY." }),
13505
+ "value-file": Flags.string({ description: "UTF-8 file to read as the secret value. The full file contents become the value." }),
13506
+ "value-from-env-file": Flags.string({ description: "Dotenv-style file to read as the secret value. Use FILE to read --key from that file, or FILE:KEY to read a different key." }),
13507
+ stdin: Flags.boolean({ description: "Read the secret value from stdin. A single trailing line ending is stripped. Example: printf '%s' \"$OPENAI_KEY\" | primitive functions set-secret --id <fn-id> --key OPENAI_KEY --stdin" }),
13508
+ redeploy: Flags.boolean({ description: "Also redeploy the function with its current code so the new value lands in the running handler. Without this, the secret is written but not visible to the handler until the next deploy. Note: when --redeploy re-uploads the function's current live code without a sourceMap field, the API preserves the current stored source map. Use `functions redeploy --file <bundle.js> --source-map-file <bundle.js.map>` to replace or restore a map." }),
11746
13509
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
11747
13510
  };
11748
13511
  async run() {
@@ -11765,6 +13528,19 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
11765
13528
  baseUrlOverridden,
11766
13529
  configDir: this.config.configDir
11767
13530
  };
13531
+ const resolvedValue = resolveSingleSecretValue({
13532
+ key: flags.key,
13533
+ value: flags.value,
13534
+ valueFile: flags["value-file"],
13535
+ valueFromEnv: flags["value-from-env"],
13536
+ valueFromEnvFile: flags["value-from-env-file"],
13537
+ stdin: flags.stdin
13538
+ });
13539
+ if (resolvedValue.kind === "error") {
13540
+ process.stderr.write(`${resolvedValue.message}\n`);
13541
+ process.exitCode = 1;
13542
+ return;
13543
+ }
11768
13544
  const outcome = await runSetSecret({
11769
13545
  getFunction: (p) => getFunction({
11770
13546
  client: apiClient.client,
@@ -11790,11 +13566,11 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
11790
13566
  id: flags.id,
11791
13567
  key: flags.key,
11792
13568
  redeploy: flags.redeploy === true,
11793
- value: flags.value
13569
+ value: resolvedValue.value
11794
13570
  });
11795
13571
  if (outcome.kind === "error") {
11796
- if (outcome.stage === "get-function") process.stderr.write("Secret was written, but reading current function code for redeploy failed; the secret is NOT yet live. Re-run with --redeploy, or call `primitive functions:redeploy --id <id> --file <bundle>` once you have the bundle.\n");
11797
- else if (outcome.stage === "redeploy") process.stderr.write("Secret was written, but the redeploy step failed; the secret is NOT yet live. Inspect the function's deploy_error and re-run `primitive functions:redeploy --id <id> --file <bundle>` once the cause is fixed.\n");
13572
+ if (outcome.stage === "get-function") process.stderr.write("Secret was written, but reading current function code for redeploy failed; the secret is NOT yet live. Re-run with --redeploy, or call `primitive functions redeploy --id <id> --file <bundle>` once you have the bundle.\n");
13573
+ else if (outcome.stage === "redeploy") process.stderr.write("Secret was written, but the redeploy step failed; the secret is NOT yet live. Inspect the function's deploy_error and re-run `primitive functions redeploy --id <id> --file <bundle>` once the cause is fixed.\n");
11798
13574
  writeErrorWithHints(outcome.payload);
11799
13575
  removeStaleSavedCredentialOnUnauthorized({
11800
13576
  ...authFailureContext,
@@ -11805,20 +13581,122 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
11805
13581
  }
11806
13582
  this.log(JSON.stringify(outcome.result, null, 2));
11807
13583
  });
11808
- }
11809
- };
11810
- //#endregion
11811
- //#region src/oclif/commands/functions-test-function.ts
11812
- const DEFAULT_WAIT_TIMEOUT_SECONDS = 60;
11813
- const TERMINAL_WEBHOOK_STATUSES = new Set(["fired", "exhausted"]);
13584
+ }
13585
+ };
13586
+ //#endregion
13587
+ //#region src/oclif/commands/functions-templates.ts
13588
+ var FunctionsTemplatesCommand = class FunctionsTemplatesCommand extends Command {
13589
+ static enableJsonFlag = true;
13590
+ static description = `List Primitive Function templates available to \`primitive functions init\`.
13591
+
13592
+ The default table is optimized for quick terminal discovery. Use
13593
+ --json when an agent or script needs stable metadata for searching,
13594
+ ranking, or choosing a template programmatically.`;
13595
+ static summary = "List available Primitive Function templates";
13596
+ static examples = [
13597
+ "<%= config.bin %> functions templates",
13598
+ "<%= config.bin %> functions templates --json",
13599
+ "<%= config.bin %> functions init my-fn --template email-reply"
13600
+ ];
13601
+ static flags = {};
13602
+ async run() {
13603
+ const { flags } = await this.parse(FunctionsTemplatesCommand);
13604
+ if (flags.json) return FUNCTION_TEMPLATES.map(serializeFunctionTemplate);
13605
+ this.log(formatFunctionTemplateList(FUNCTION_TEMPLATES));
13606
+ }
13607
+ };
13608
+ //#endregion
13609
+ //#region src/oclif/commands/functions-test-function.ts
13610
+ const DEFAULT_WAIT_TIMEOUT_SECONDS = 60;
13611
+ const TERMINAL_WEBHOOK_STATUSES = new Set(["fired", "exhausted"]);
13612
+ function buildFunctionTestOutcome(params) {
13613
+ const outcome = {
13614
+ elapsed_seconds: params.elapsedSeconds,
13615
+ function_id: params.functionId,
13616
+ inbound_domain: params.invocation.inbound_domain,
13617
+ inbound_id: params.inboundId,
13618
+ inbound_to: params.invocation.to,
13619
+ poll_since: params.invocation.poll_since,
13620
+ test_run_id: params.invocation.test_run_id,
13621
+ test_send_id: params.invocation.send_id,
13622
+ test_subject: params.invocation.subject,
13623
+ trace_url: params.invocation.trace_url,
13624
+ watch_url: params.invocation.watch_url,
13625
+ webhook_attempt_count: params.detail.webhook_attempt_count,
13626
+ webhook_last_error: params.detail.webhook_last_error,
13627
+ webhook_last_status_code: params.detail.webhook_last_status_code,
13628
+ webhook_status: params.detail.webhook_status
13629
+ };
13630
+ if (params.showSends) outcome.sent_emails = params.detail.replies;
13631
+ return outcome;
13632
+ }
13633
+ function stringOrNull(value) {
13634
+ return typeof value === "string" && value.length > 0 ? value : null;
13635
+ }
13636
+ function findMatchingFunctionEndpoints(params) {
13637
+ const matches = [];
13638
+ for (const endpoint of params.endpoints) {
13639
+ if (endpoint.kind !== "function") continue;
13640
+ if (endpoint.enabled === false) continue;
13641
+ if (endpoint.deactivated_at !== null && endpoint.deactivated_at !== void 0) continue;
13642
+ const id = stringOrNull(endpoint.id);
13643
+ if (!id) continue;
13644
+ const domainId = stringOrNull(endpoint.domain_id);
13645
+ if (domainId !== null && (params.inboundDomainId === null || domainId !== params.inboundDomainId)) continue;
13646
+ const functionId = stringOrNull(endpoint.function_id);
13647
+ matches.push({
13648
+ function_id: functionId,
13649
+ id,
13650
+ is_current_function: functionId === params.currentFunctionId,
13651
+ scope: domainId === null ? "catch-all" : "domain"
13652
+ });
13653
+ }
13654
+ return matches;
13655
+ }
13656
+ function formatFunctionEndpointNoiseWarning(params) {
13657
+ if (params.endpoints.filter((endpoint) => !endpoint.is_current_function).length === 0) return null;
13658
+ const lines = [`Warning: ${params.endpoints.length} function endpoints may receive mail for ${params.toAddress}:`];
13659
+ for (const endpoint of params.endpoints) {
13660
+ const scope = endpoint.scope === "catch-all" ? "catch-all" : `scoped to ${params.inboundDomain}`;
13661
+ const current = endpoint.is_current_function ? " (this function)" : "";
13662
+ const target = endpoint.function_id ? ` -> function ${endpoint.function_id}` : "";
13663
+ lines.push(`- endpoint ${endpoint.id}${target}, ${scope}${current}`);
13664
+ }
13665
+ return lines.join("\n");
13666
+ }
13667
+ async function maybeWriteEndpointNoiseWarning(params) {
13668
+ try {
13669
+ const [domainsResult, endpointsResult] = await Promise.all([listDomains({
13670
+ client: params.apiClient.client,
13671
+ responseStyle: "fields"
13672
+ }), listEndpoints({
13673
+ client: params.apiClient.client,
13674
+ responseStyle: "fields"
13675
+ })]);
13676
+ if (endpointsResult.error) return;
13677
+ if (domainsResult.error) return;
13678
+ const inboundDomainId = domainsResult.data?.data?.find((domain) => domain.domain?.toLowerCase() === params.invocation.inbound_domain.toLowerCase())?.id ?? null;
13679
+ const endpointsEnvelope = endpointsResult.data;
13680
+ const warning = formatFunctionEndpointNoiseWarning({
13681
+ endpoints: findMatchingFunctionEndpoints({
13682
+ currentFunctionId: params.currentFunctionId,
13683
+ endpoints: endpointsEnvelope?.data ?? [],
13684
+ inboundDomainId
13685
+ }),
13686
+ inboundDomain: params.invocation.inbound_domain,
13687
+ toAddress: params.invocation.to
13688
+ });
13689
+ if (warning) params.writeStderr(`${warning}\n`);
13690
+ } catch {}
13691
+ }
11814
13692
  var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Command {
11815
13693
  static description = "Send a real test email through MX to trigger this function. With --wait, blocks until the function has processed the inbound; with --show-sends, also prints any outbound sends the function emitted in response.";
11816
13694
  static summary = "Trigger a test invocation; with --wait, watch it land";
11817
13695
  static examples = [
11818
- "<%= config.bin %> functions:test-function --id <fn-id>",
11819
- "<%= config.bin %> functions:test-function --id <fn-id> --local-part summarize",
11820
- "<%= config.bin %> functions:test-function --id <fn-id> --wait --show-sends",
11821
- "<%= config.bin %> functions:test-function --id <fn-id> --local-part summarize --wait --timeout 120"
13696
+ "<%= config.bin %> functions test --id <fn-id>",
13697
+ "<%= config.bin %> functions test --id <fn-id> --local-part summarize",
13698
+ "<%= config.bin %> functions test --id <fn-id> --wait --show-sends",
13699
+ "<%= config.bin %> functions test --id <fn-id> --local-part summarize --wait --timeout 120"
11822
13700
  ];
11823
13701
  static flags = {
11824
13702
  "api-key": Flags.string({
@@ -11894,6 +13772,14 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
11894
13772
  this.log(JSON.stringify(invocation, null, 2));
11895
13773
  return;
11896
13774
  }
13775
+ await maybeWriteEndpointNoiseWarning({
13776
+ apiClient,
13777
+ currentFunctionId: flags.id,
13778
+ invocation,
13779
+ writeStderr: (chunk) => {
13780
+ process.stderr.write(chunk);
13781
+ }
13782
+ });
11897
13783
  const startedAt = Date.now();
11898
13784
  const timeoutMs = flags.timeout * 1e3;
11899
13785
  const pollIntervalMs = flags["poll-interval"] * 1e3;
@@ -11956,17 +13842,14 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
11956
13842
  }
11957
13843
  if (!detail) this.error(`Timed out after ${flags.timeout}s waiting for function webhook to fire for inbound ${inboundId}. Browse ${invocation.watch_url} for the live view.`, { exit: 2 });
11958
13844
  const elapsedSeconds = Math.round((Date.now() - startedAt) / 1e3);
11959
- const outcome = {
11960
- function_id: flags.id,
11961
- inbound_id: inboundId,
11962
- inbound_to: invocation.to,
11963
- webhook_status: detail.webhook_status,
11964
- webhook_attempt_count: detail.webhook_attempt_count,
11965
- webhook_last_status_code: detail.webhook_last_status_code,
11966
- webhook_last_error: detail.webhook_last_error,
11967
- elapsed_seconds: elapsedSeconds
11968
- };
11969
- if (shouldShowSends) outcome.sent_emails = detail.replies;
13845
+ const outcome = buildFunctionTestOutcome({
13846
+ detail,
13847
+ elapsedSeconds,
13848
+ functionId: flags.id,
13849
+ inboundId,
13850
+ invocation,
13851
+ showSends: shouldShowSends
13852
+ });
11970
13853
  this.log(JSON.stringify(outcome, null, 2));
11971
13854
  if (detail.webhook_status === "exhausted") process.exitCode = 1;
11972
13855
  });
@@ -11975,7 +13858,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
11975
13858
  //#endregion
11976
13859
  //#region src/oclif/commands/login.ts
11977
13860
  const MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS = 60;
11978
- function cliError$1(message) {
13861
+ function cliError$2(message) {
11979
13862
  return new Errors.CLIError(message, { exit: 1 });
11980
13863
  }
11981
13864
  function sleep(ms) {
@@ -11994,10 +13877,10 @@ function openBrowser(url) {
11994
13877
  child.on("error", () => void 0);
11995
13878
  child.unref();
11996
13879
  }
11997
- function unwrapData$1(value) {
13880
+ function unwrapData$2(value) {
11998
13881
  return value?.data ?? null;
11999
13882
  }
12000
- function retryAfterSeconds(result) {
13883
+ function retryAfterSeconds$1(result) {
12001
13884
  const raw = result.response?.headers.get("retry-after");
12002
13885
  if (!raw) return null;
12003
13886
  const parsed = Number.parseInt(raw, 10);
@@ -12061,7 +13944,7 @@ var LoginCommand = class LoginCommand extends Command {
12061
13944
  try {
12062
13945
  releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
12063
13946
  } catch (error) {
12064
- throw cliError$1(error instanceof Error ? error.message : String(error));
13947
+ throw cliError$2(error instanceof Error ? error.message : String(error));
12065
13948
  }
12066
13949
  try {
12067
13950
  await this.runWithCredentialLock(flags);
@@ -12090,8 +13973,8 @@ var LoginCommand = class LoginCommand extends Command {
12090
13973
  if (existingStatus.status === "removed_stale") process.stderr.write("Continuing with a new Primitive CLI login...\n");
12091
13974
  else if (existingStatus.status === "blocked") {
12092
13975
  writeErrorWithHints(existingStatus.payload);
12093
- throw cliError$1(existingStatus.message);
12094
- } else throw cliError$1(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before logging in again.`);
13976
+ throw cliError$2(existingStatus.message);
13977
+ } else throw cliError$2(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before logging in again.`);
12095
13978
  }
12096
13979
  const apiClient = new PrimitiveApiClient({ apiBaseUrl1 });
12097
13980
  const started = await startCliLogin({
@@ -12101,10 +13984,10 @@ var LoginCommand = class LoginCommand extends Command {
12101
13984
  });
12102
13985
  if (started.error) {
12103
13986
  writeErrorWithHints(extractErrorPayload(started.error));
12104
- throw cliError$1("Could not start Primitive CLI login.");
13987
+ throw cliError$2("Could not start Primitive CLI login.");
12105
13988
  }
12106
- const start = unwrapData$1(started.data);
12107
- if (!start) throw cliError$1("Primitive API returned an empty CLI login response.");
13989
+ const start = unwrapData$2(started.data);
13990
+ if (!start) throw cliError$2("Primitive API returned an empty CLI login response.");
12108
13991
  process.stderr.write(`Your login code is: ${start.user_code}\n`);
12109
13992
  if (!flags["no-browser"]) {
12110
13993
  openBrowser(start.verification_uri_complete);
@@ -12124,8 +14007,8 @@ var LoginCommand = class LoginCommand extends Command {
12124
14007
  responseStyle: "fields"
12125
14008
  });
12126
14009
  if (polled.data) {
12127
- const login = unwrapData$1(polled.data);
12128
- if (!login) throw cliError$1("Primitive API returned an empty CLI poll response.");
14010
+ const login = unwrapData$2(polled.data);
14011
+ if (!login) throw cliError$2("Primitive API returned an empty CLI poll response.");
12129
14012
  saveCliCredentials(this.config.configDir, {
12130
14013
  api_key: login.api_key,
12131
14014
  api_base_url_1: apiBaseUrl1,
@@ -12147,25 +14030,25 @@ var LoginCommand = class LoginCommand extends Command {
12147
14030
  continue;
12148
14031
  }
12149
14032
  if (code === API_ERROR_CODES.slowDown) {
12150
- interval = Math.min(retryAfterSeconds(polled) ?? interval + 5, MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
14033
+ interval = Math.min(retryAfterSeconds$1(polled) ?? interval + 5, MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
12151
14034
  nextPollDelay = interval;
12152
14035
  continue;
12153
14036
  }
12154
- if (code === API_ERROR_CODES.accessDenied) throw cliError$1("Primitive CLI login was denied in the browser.");
12155
- if (code === API_ERROR_CODES.expiredToken) throw cliError$1("Primitive CLI login expired. Run `primitive login` again.");
12156
- if (code === API_ERROR_CODES.invalidDeviceCode) throw cliError$1("Primitive CLI login device code is invalid. Run `primitive login` again.");
14037
+ if (code === API_ERROR_CODES.accessDenied) throw cliError$2("Primitive CLI login was denied in the browser.");
14038
+ if (code === API_ERROR_CODES.expiredToken) throw cliError$2("Primitive CLI login expired. Run `primitive login` again.");
14039
+ if (code === API_ERROR_CODES.invalidDeviceCode) throw cliError$2("Primitive CLI login device code is invalid. Run `primitive login` again.");
12157
14040
  writeErrorWithHints(payload);
12158
- throw cliError$1("Primitive CLI login failed while polling for approval.");
14041
+ throw cliError$2("Primitive CLI login failed while polling for approval.");
12159
14042
  }
12160
- throw cliError$1("Primitive CLI login expired. Run `primitive login` again.");
14043
+ throw cliError$2("Primitive CLI login expired. Run `primitive login` again.");
12161
14044
  }
12162
14045
  };
12163
14046
  //#endregion
12164
14047
  //#region src/oclif/commands/logout.ts
12165
- function cliError(message) {
14048
+ function cliError$1(message) {
12166
14049
  return new Errors.CLIError(message, { exit: 1 });
12167
14050
  }
12168
- function unwrapData(value) {
14051
+ function unwrapData$1(value) {
12169
14052
  return value?.data ?? null;
12170
14053
  }
12171
14054
  var LogoutCommand = class LogoutCommand extends Command {
@@ -12183,7 +14066,7 @@ var LogoutCommand = class LogoutCommand extends Command {
12183
14066
  try {
12184
14067
  releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
12185
14068
  } catch (error) {
12186
- throw cliError(error instanceof Error ? error.message : String(error));
14069
+ throw cliError$1(error instanceof Error ? error.message : String(error));
12187
14070
  }
12188
14071
  try {
12189
14072
  await this.runWithCredentialLock(flags);
@@ -12202,7 +14085,7 @@ var LogoutCommand = class LogoutCommand extends Command {
12202
14085
  process.exitCode = 1;
12203
14086
  return;
12204
14087
  }
12205
- if (!credentials) throw cliError("Not logged in. Run `primitive login` to create saved CLI credentials.");
14088
+ if (!credentials) throw cliError$1("Not logged in. Run `primitive login` to create saved CLI credentials.");
12206
14089
  const apiBaseUrl1 = flags["api-base-url-1"] ? normalizeApiBaseUrl1(flags["api-base-url-1"]) : credentials.api_base_url_1;
12207
14090
  const apiClient = new PrimitiveApiClient({
12208
14091
  apiKey: credentials.api_key,
@@ -12224,15 +14107,101 @@ var LogoutCommand = class LogoutCommand extends Command {
12224
14107
  return;
12225
14108
  }
12226
14109
  writeErrorWithHints(payload);
12227
- throw cliError("Could not revoke the saved Primitive CLI API key.");
14110
+ throw cliError$1("Could not revoke the saved Primitive CLI API key.");
12228
14111
  }
12229
- const logout = unwrapData(result.data);
14112
+ const logout = unwrapData$1(result.data);
12230
14113
  deleteCliCredentials(this.config.configDir);
12231
14114
  const keyId = logout?.key_id ?? credentials.key_id;
12232
14115
  process.stderr.write(`Logged out and revoked API key ${keyId}.\n`);
12233
14116
  }
12234
14117
  };
12235
14118
  //#endregion
14119
+ //#region src/oclif/commands/reply.ts
14120
+ var ReplyCommand = class ReplyCommand extends Command {
14121
+ static description = `Reply to an inbound email.
14122
+
14123
+ The API derives recipients, the Re: subject, and threading headers from the inbound email id. Use \`primitive send --in-reply-to <message-id>\` only when you need to thread against a raw Message-Id instead of an inbound email stored by Primitive.`;
14124
+ static summary = "Reply to an inbound email";
14125
+ static examples = [
14126
+ "<%= config.bin %> reply --id <inbound-email-id> --body 'Thanks, got it.'",
14127
+ "<%= config.bin %> reply --id <inbound-email-id> --html '<p>Thanks, got it.</p>' --wait",
14128
+ "<%= config.bin %> reply --id <inbound-email-id> --from 'Support <support@example.com>' --body 'Thanks!'"
14129
+ ];
14130
+ static flags = {
14131
+ "api-key": Flags.string({
14132
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
14133
+ env: "PRIMITIVE_API_KEY"
14134
+ }),
14135
+ "api-base-url-1": Flags.string({
14136
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
14137
+ env: "PRIMITIVE_API_BASE_URL_1",
14138
+ hidden: true
14139
+ }),
14140
+ "api-base-url-2": Flags.string({
14141
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
14142
+ env: "PRIMITIVE_API_BASE_URL_2",
14143
+ hidden: true
14144
+ }),
14145
+ id: Flags.string({
14146
+ description: "Inbound email id to reply to.",
14147
+ required: true
14148
+ }),
14149
+ body: Flags.string({ description: "Plain-text reply body. Either --body or --html (or both) is required." }),
14150
+ html: Flags.string({ description: "HTML reply body. Either --body or --html (or both) is required." }),
14151
+ from: Flags.string({ description: "Optional From header override. Defaults to the inbound recipient." }),
14152
+ 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
+ "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
14154
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
14155
+ };
14156
+ async run() {
14157
+ const { flags } = await this.parse(ReplyCommand);
14158
+ if (!flags.body && !flags.html) throw new Errors.CLIError("Either --body or --html (or both) is required.");
14159
+ 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({
14162
+ apiKey: flags["api-key"],
14163
+ apiBaseUrl1: flags["api-base-url-1"],
14164
+ apiBaseUrl2: flags["api-base-url-2"],
14165
+ configDir: this.config.configDir
14166
+ });
14167
+ const apiClient = new PrimitiveApiClient({
14168
+ apiKey: auth.apiKey,
14169
+ apiBaseUrl1: auth.apiBaseUrl1,
14170
+ apiBaseUrl2: auth.apiBaseUrl2
14171
+ });
14172
+ const result = await replyToEmail({
14173
+ body: {
14174
+ ...flags.body !== void 0 ? { body_text: flags.body } : {},
14175
+ ...flags.html !== void 0 ? { body_html: flags.html } : {},
14176
+ ...flags.from !== void 0 ? { from: flags.from } : {},
14177
+ ...flags.wait !== void 0 ? { wait: flags.wait } : {},
14178
+ ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
14179
+ },
14180
+ client: apiClient.client,
14181
+ path: { id: flags.id },
14182
+ responseStyle: "fields"
14183
+ });
14184
+ if (result.error) {
14185
+ const errorPayload = extractErrorPayload(result.error);
14186
+ writeErrorWithHints(errorPayload);
14187
+ removeStaleSavedCredentialOnUnauthorized({
14188
+ auth,
14189
+ baseUrlOverridden,
14190
+ configDir: this.config.configDir,
14191
+ payload: errorPayload
14192
+ });
14193
+ process.exitCode = 1;
14194
+ return;
14195
+ }
14196
+ const envelope = result.data;
14197
+ writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
14198
+ process.stderr.write(chunk);
14199
+ } });
14200
+ this.log(JSON.stringify(envelope?.data ?? null, null, 2));
14201
+ });
14202
+ }
14203
+ };
14204
+ //#endregion
12236
14205
  //#region src/oclif/commands/send.ts
12237
14206
  const SUBJECT_MAX_LENGTH = 200;
12238
14207
  function deriveSubject(body) {
@@ -12264,17 +14233,17 @@ async function pickDefaultFromAddress(apiClient, authFailureContext) {
12264
14233
  throw new Errors.CLIError(`Could not look up your verified domains to default --from. Pass --from explicitly. Underlying error: ${formatErrorPayload(errorPayload)}`);
12265
14234
  }
12266
14235
  const first = result.data?.data?.find(isVerifiedDomain);
12267
- if (!first) throw new Errors.CLIError("No active verified outbound domain found on this account; pass --from explicitly. To set up outbound, claim a domain via `primitive domains:add-domain` and verify it.");
14236
+ if (!first) throw new Errors.CLIError("No active verified outbound domain found on this account; pass --from explicitly. To set up outbound, claim a domain via `primitive domains add` and verify it.");
12268
14237
  return `agent@${first.domain}`;
12269
14238
  }
12270
14239
  var SendCommand = class SendCommand extends Command {
12271
- static description = `Send an outbound email. Agent-grade shortcut for sending:send-email with sensible defaults.
14240
+ static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
12272
14241
 
12273
14242
  --from defaults to agent@<your-first-verified-outbound-domain> when omitted.
12274
14243
  --subject defaults to the first line of the body when omitted.
12275
14244
 
12276
14245
  For the full flag set (custom message-id threading on the wire,
12277
- references arrays, etc.), use \`primitive sending:send-email\`.`;
14246
+ references arrays, etc.), use \`primitive sending send\`.`;
12278
14247
  static summary = "Send an email (simplified, agent-friendly)";
12279
14248
  static examples = [
12280
14249
  "<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
@@ -12306,7 +14275,7 @@ var SendCommand = class SendCommand extends Command {
12306
14275
  subject: Flags.string({ description: "Subject line. Defaults to the first line of --body / --html when omitted." }),
12307
14276
  body: Flags.string({ description: "Plain-text message body. Either --body or --html (or both) is required." }),
12308
14277
  html: Flags.string({ description: "HTML message body. Either --body or --html (or both) is required." }),
12309
- "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 sending:reply-to-email --id <inbound-id>`." }),
14278
+ "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>`." }),
12310
14279
  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." }),
12311
14280
  "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
12312
14281
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
@@ -12367,6 +14336,394 @@ var SendCommand = class SendCommand extends Command {
12367
14336
  }
12368
14337
  };
12369
14338
  //#endregion
14339
+ //#region src/oclif/commands/signup.ts
14340
+ const INVALID_VERIFICATION_CODE = "invalid_verification_code";
14341
+ const CLERK_PASSWORD_REJECTED = "clerk_password_rejected";
14342
+ const EXPIRED_TOKEN = "expired_token";
14343
+ const INVALID_SIGNUP_TOKEN = "invalid_signup_token";
14344
+ const SLOW_DOWN = "slow_down";
14345
+ const PENDING_SIGNUP_FILE = "signup.json";
14346
+ function cliError(message) {
14347
+ return new Errors.CLIError(message, { exit: 1 });
14348
+ }
14349
+ function unwrapData(value) {
14350
+ return value?.data ?? null;
14351
+ }
14352
+ function isRecord(value) {
14353
+ return value !== null && typeof value === "object" && !Array.isArray(value);
14354
+ }
14355
+ function pendingSignupFromJson(value) {
14356
+ if (!isRecord(value)) return null;
14357
+ if (typeof value.signup_token !== "string" || typeof value.email !== "string" || typeof value.expires_in !== "number" || typeof value.resend_after !== "number" || typeof value.verification_code_length !== "number" || typeof value.api_base_url_1 !== "string" || typeof value.created_at !== "string" || typeof value.expires_at !== "string") return null;
14358
+ return {
14359
+ api_base_url_1: value.api_base_url_1,
14360
+ created_at: value.created_at,
14361
+ email: value.email,
14362
+ expires_at: value.expires_at,
14363
+ expires_in: value.expires_in,
14364
+ resend_after: value.resend_after,
14365
+ signup_token: value.signup_token,
14366
+ verification_code_length: value.verification_code_length
14367
+ };
14368
+ }
14369
+ function pendingSignupPath(configDir) {
14370
+ return join(configDir, PENDING_SIGNUP_FILE);
14371
+ }
14372
+ function deletePendingCliSignup(configDir) {
14373
+ rmSync(pendingSignupPath(configDir), { force: true });
14374
+ }
14375
+ function pendingSignupFromStart(start, apiBaseUrl1) {
14376
+ return {
14377
+ ...start,
14378
+ api_base_url_1: apiBaseUrl1,
14379
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
14380
+ expires_at: new Date(Date.now() + start.expires_in * 1e3).toISOString()
14381
+ };
14382
+ }
14383
+ function savePendingCliSignup(configDir, start, apiBaseUrl1) {
14384
+ mkdirSync(configDir, {
14385
+ mode: 448,
14386
+ recursive: true
14387
+ });
14388
+ const pending = pendingSignupFromStart(start, apiBaseUrl1);
14389
+ const path = pendingSignupPath(configDir);
14390
+ const tempPath = join(configDir, `${PENDING_SIGNUP_FILE}.${process$1.pid}.${randomUUID()}.tmp`);
14391
+ try {
14392
+ writeFileSync(tempPath, `${JSON.stringify(pending, null, 2)}\n`, { mode: 384 });
14393
+ chmodSync(tempPath, 384);
14394
+ renameSync(tempPath, path);
14395
+ chmodSync(path, 384);
14396
+ return pending;
14397
+ } catch (error) {
14398
+ rmSync(tempPath, { force: true });
14399
+ throw error;
14400
+ }
14401
+ }
14402
+ function loadPendingCliSignup(configDir, apiBaseUrl1) {
14403
+ const path = pendingSignupPath(configDir);
14404
+ let contents;
14405
+ try {
14406
+ contents = readFileSync(path, "utf8");
14407
+ } catch (error) {
14408
+ if (error && typeof error === "object" && error.code === "ENOENT") return null;
14409
+ throw error;
14410
+ }
14411
+ let pending;
14412
+ try {
14413
+ pending = pendingSignupFromJson(JSON.parse(contents));
14414
+ } catch {
14415
+ pending = null;
14416
+ }
14417
+ if (!pending) {
14418
+ deletePendingCliSignup(configDir);
14419
+ return null;
14420
+ }
14421
+ if (pending.api_base_url_1 !== apiBaseUrl1) return null;
14422
+ if (new Date(pending.expires_at).getTime() <= Date.now()) {
14423
+ deletePendingCliSignup(configDir);
14424
+ return null;
14425
+ }
14426
+ return {
14427
+ ...pending,
14428
+ expires_in: Math.max(0, Math.ceil((new Date(pending.expires_at).getTime() - Date.now()) / 1e3))
14429
+ };
14430
+ }
14431
+ function retryAfterSeconds(result) {
14432
+ const raw = result.response?.headers.get("retry-after");
14433
+ if (!raw) return null;
14434
+ const parsed = Number.parseInt(raw, 10);
14435
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
14436
+ }
14437
+ function normalizeAnswer(value) {
14438
+ return value.trim();
14439
+ }
14440
+ async function promptLine(question) {
14441
+ const rl = createInterface({
14442
+ input: process$1.stdin,
14443
+ output: process$1.stderr
14444
+ });
14445
+ try {
14446
+ return normalizeAnswer(await rl.question(question));
14447
+ } finally {
14448
+ rl.close();
14449
+ }
14450
+ }
14451
+ async function promptHidden(question) {
14452
+ if (!process$1.stdin.isTTY || !process$1.stderr.isTTY || !process$1.stdin.setRawMode) throw cliError("Password input requires an interactive terminal with hidden input support.");
14453
+ return new Promise((resolve, reject) => {
14454
+ const input = process$1.stdin;
14455
+ let value = "";
14456
+ const cleanup = () => {
14457
+ input.setRawMode(false);
14458
+ input.pause();
14459
+ input.off("data", onData);
14460
+ };
14461
+ const finish = () => {
14462
+ cleanup();
14463
+ process$1.stderr.write("\n");
14464
+ resolve(value);
14465
+ };
14466
+ const onData = (chunk) => {
14467
+ const text = chunk.toString("utf8");
14468
+ for (const char of text) {
14469
+ if (char === "") {
14470
+ cleanup();
14471
+ process$1.stderr.write("\n");
14472
+ reject(cliError("Signup cancelled."));
14473
+ return;
14474
+ }
14475
+ if (char === "\r" || char === "\n") {
14476
+ finish();
14477
+ return;
14478
+ }
14479
+ if (char === "\b" || char === "") {
14480
+ value = value.slice(0, -1);
14481
+ continue;
14482
+ }
14483
+ value += char;
14484
+ }
14485
+ };
14486
+ process$1.stderr.write(question);
14487
+ input.setRawMode(true);
14488
+ input.resume();
14489
+ input.on("data", onData);
14490
+ });
14491
+ }
14492
+ function formatSignupSeconds(seconds) {
14493
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) return "soon";
14494
+ if (seconds < 60) return `${Math.ceil(seconds)} seconds`;
14495
+ const minutes = Math.ceil(seconds / 60);
14496
+ return `${minutes} minute${minutes === 1 ? "" : "s"}`;
14497
+ }
14498
+ function shouldRetrySignupPassword(errorCode) {
14499
+ return errorCode === CLERK_PASSWORD_REJECTED;
14500
+ }
14501
+ function signupErrorMessage(payload) {
14502
+ if (!isRecord(payload)) return null;
14503
+ const message = (isRecord(payload.error) ? payload.error : payload).message;
14504
+ return typeof message === "string" && message.trim() ? message : null;
14505
+ }
14506
+ async function promptRequired(question) {
14507
+ while (true) {
14508
+ const value = await promptLine(question);
14509
+ if (value) return value;
14510
+ process$1.stderr.write("Please enter a value.\n");
14511
+ }
14512
+ }
14513
+ async function promptRequiredPassword(question) {
14514
+ while (true) {
14515
+ const value = await promptHidden(question);
14516
+ if (value) return value;
14517
+ process$1.stderr.write("Please enter a password.\n");
14518
+ }
14519
+ }
14520
+ async function promptNewPassword() {
14521
+ while (true) {
14522
+ const password = await promptRequiredPassword("Password: ");
14523
+ if (password === await promptRequiredPassword("Confirm password: ")) return password;
14524
+ process$1.stderr.write("Passwords did not match. Try again.\n");
14525
+ }
14526
+ }
14527
+ async function confirmTerms() {
14528
+ process$1.stderr.write("By creating an account, you agree to Primitive's Terms of Service and Privacy Policy:\n");
14529
+ process$1.stderr.write(" https://primitive.dev/terms\n");
14530
+ process$1.stderr.write(" https://primitive.dev/privacy\n");
14531
+ const answer = (await promptRequired("Type 'yes' to continue: ")).toLowerCase();
14532
+ if (answer !== "yes" && answer !== "y") throw cliError("You must accept the terms to create an account.");
14533
+ }
14534
+ async function resendVerificationCode(params) {
14535
+ const resent = await (params.deps.resendCliSignupVerification ?? resendCliSignupVerification)({
14536
+ body: { signup_token: params.start.signup_token },
14537
+ client: params.apiClient.client,
14538
+ responseStyle: "fields"
14539
+ });
14540
+ if (resent.data) {
14541
+ const resend = unwrapData(resent.data);
14542
+ const next = resend ? {
14543
+ email: resend.email,
14544
+ expires_in: resend.expires_in,
14545
+ resend_after: resend.resend_after,
14546
+ signup_token: params.start.signup_token,
14547
+ verification_code_length: resend.verification_code_length
14548
+ } : params.start;
14549
+ savePendingCliSignup(params.configDir, next, params.apiBaseUrl1);
14550
+ process$1.stderr.write(`Sent a new ${next.verification_code_length}-digit verification code. It expires in ${formatSignupSeconds(next.expires_in)}.\n`);
14551
+ return next;
14552
+ }
14553
+ const payload = extractErrorPayload(resent.error);
14554
+ if (extractErrorCode(payload) === SLOW_DOWN) {
14555
+ const suffix = ` Wait ${formatSignupSeconds(retryAfterSeconds(resent) ?? params.start.resend_after)} before trying again.`;
14556
+ process$1.stderr.write(`Verification email was sent recently.${suffix}\n`);
14557
+ return params.start;
14558
+ }
14559
+ writeErrorWithHints(payload);
14560
+ throw cliError("Could not resend Primitive CLI signup verification email.");
14561
+ }
14562
+ async function runSignupWithCredentialLock(params) {
14563
+ const { configDir, flags } = params;
14564
+ const deps = params.deps ?? {};
14565
+ const promptRequiredFn = deps.promptRequired ?? promptRequired;
14566
+ const promptNewPasswordFn = deps.promptNewPassword ?? promptNewPassword;
14567
+ const confirmTermsFn = deps.confirmTerms ?? confirmTerms;
14568
+ const startFn = deps.startCliSignup ?? startCliSignup;
14569
+ const verifyFn = deps.verifyCliSignup ?? verifyCliSignup;
14570
+ const checkExistingLoginFn = deps.checkExistingLogin ?? checkExistingLogin;
14571
+ const apiBaseUrl1 = normalizeApiBaseUrl1(flags["api-base-url-1"]);
14572
+ let existing;
14573
+ try {
14574
+ existing = loadCliCredentials(configDir);
14575
+ } catch (error) {
14576
+ if (!flags.force) throw error;
14577
+ const detail = error instanceof Error ? error.message : String(error);
14578
+ process$1.stderr.write(`Replacing unreadable Primitive CLI credentials because --force was set: ${detail}\n`);
14579
+ existing = null;
14580
+ }
14581
+ if (existing && flags.force) process$1.stderr.write("Replacing saved Primitive CLI credentials after signup because --force was set.\n");
14582
+ else if (existing) {
14583
+ const existingStatus = await checkExistingLoginFn({
14584
+ apiBaseUrl1: flags["api-base-url-1"],
14585
+ configDir,
14586
+ credentials: existing
14587
+ });
14588
+ if (existingStatus.status === "removed_stale") process$1.stderr.write("Continuing with Primitive CLI signup...\n");
14589
+ else if (existingStatus.status === "blocked") {
14590
+ writeErrorWithHints(existingStatus.payload);
14591
+ throw cliError(existingStatus.message);
14592
+ } else throw cliError(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before creating a new account.`);
14593
+ }
14594
+ if (flags.force) deletePendingCliSignup(configDir);
14595
+ const apiClient = new PrimitiveApiClient({ apiBaseUrl1 });
14596
+ let start = flags.force ? null : loadPendingCliSignup(configDir, apiBaseUrl1);
14597
+ const resumed = Boolean(start);
14598
+ if (start) process$1.stderr.write(`Continuing pending Primitive CLI signup for ${start.email}.\n`);
14599
+ else {
14600
+ const signupCode = await promptRequiredFn("Signup code: ");
14601
+ await confirmTermsFn();
14602
+ const email = await promptRequiredFn("Email: ");
14603
+ const started = await startFn({
14604
+ body: {
14605
+ device_name: flags["device-name"] ?? hostname(),
14606
+ email,
14607
+ signup_code: signupCode,
14608
+ terms_accepted: true
14609
+ },
14610
+ client: apiClient.client,
14611
+ responseStyle: "fields"
14612
+ });
14613
+ if (started.error) {
14614
+ writeErrorWithHints(extractErrorPayload(started.error));
14615
+ throw cliError("Could not start Primitive CLI signup.");
14616
+ }
14617
+ const startResult = unwrapData(started.data);
14618
+ if (!startResult) throw cliError("Primitive API returned an empty CLI signup response.");
14619
+ start = savePendingCliSignup(configDir, startResult, apiBaseUrl1);
14620
+ }
14621
+ if (resumed) process$1.stderr.write(`Check your email for the ${start.verification_code_length}-digit verification code sent to ${start.email}.\n`);
14622
+ else process$1.stderr.write(`Sent a ${start.verification_code_length}-digit verification code to ${start.email}.\n`);
14623
+ process$1.stderr.write(`The code expires in ${formatSignupSeconds(start.expires_in)}.\n`);
14624
+ process$1.stderr.write(`Enter the code from the email, or type \`resend\` to send a new code after ${formatSignupSeconds(start.resend_after)}.\n`);
14625
+ while (true) {
14626
+ const verificationCode = await promptRequiredFn(`Verification code (${start.verification_code_length} digits): `);
14627
+ if (verificationCode.toLowerCase() === "resend") {
14628
+ start = await resendVerificationCode({
14629
+ apiBaseUrl1,
14630
+ apiClient,
14631
+ configDir,
14632
+ deps,
14633
+ start
14634
+ });
14635
+ continue;
14636
+ }
14637
+ let password = await promptNewPasswordFn();
14638
+ while (true) {
14639
+ const verified = await verifyFn({
14640
+ body: {
14641
+ password,
14642
+ signup_token: start.signup_token,
14643
+ verification_code: verificationCode
14644
+ },
14645
+ client: apiClient.client,
14646
+ responseStyle: "fields"
14647
+ });
14648
+ if (verified.data) {
14649
+ const signup = unwrapData(verified.data);
14650
+ if (!signup) throw cliError("Primitive API returned an empty CLI signup verification response.");
14651
+ saveCliCredentials(configDir, {
14652
+ api_key: signup.api_key,
14653
+ api_base_url_1: apiBaseUrl1,
14654
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
14655
+ key_id: signup.key_id,
14656
+ key_prefix: signup.key_prefix,
14657
+ org_id: signup.org_id,
14658
+ org_name: signup.org_name
14659
+ });
14660
+ deletePendingCliSignup(configDir);
14661
+ const org = signup.org_name ? ` (${signup.org_name})` : "";
14662
+ process$1.stderr.write(`Created account and logged in to org ${signup.org_id}${org}.\n`);
14663
+ process$1.stderr.write(`Saved credentials to ${credentialsPath(configDir)}.\n`);
14664
+ return;
14665
+ }
14666
+ const payload = extractErrorPayload(verified.error);
14667
+ const code = extractErrorCode(payload);
14668
+ if (code === INVALID_VERIFICATION_CODE) {
14669
+ process$1.stderr.write("Invalid verification code. Try again or type `resend`.\n");
14670
+ break;
14671
+ }
14672
+ if (shouldRetrySignupPassword(code)) {
14673
+ const message = signupErrorMessage(payload);
14674
+ if (message) process$1.stderr.write(`Password rejected: ${message}\n`);
14675
+ process$1.stderr.write("Choose a different password and try again.\n");
14676
+ password = await promptNewPasswordFn();
14677
+ continue;
14678
+ }
14679
+ if (code === EXPIRED_TOKEN || code === INVALID_SIGNUP_TOKEN) deletePendingCliSignup(configDir);
14680
+ writeErrorWithHints(payload);
14681
+ throw cliError("Primitive CLI signup failed while verifying the account.");
14682
+ }
14683
+ }
14684
+ }
14685
+ var SignupCommand = class SignupCommand extends Command {
14686
+ static description = "Create a Primitive account from the terminal, verify your email, and save an org-scoped CLI API key locally.";
14687
+ static summary = "Create an account and log in";
14688
+ static examples = [
14689
+ "<%= config.bin %> signup",
14690
+ "<%= config.bin %> signup --device-name work-laptop",
14691
+ "<%= config.bin %> signup --force"
14692
+ ];
14693
+ static flags = {
14694
+ "api-base-url-1": Flags.string({
14695
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
14696
+ env: "PRIMITIVE_API_BASE_URL_1",
14697
+ hidden: true
14698
+ }),
14699
+ "device-name": Flags.string({ description: "Device name used for the created CLI API key" }),
14700
+ force: Flags.boolean({
14701
+ char: "f",
14702
+ description: "Replace saved credentials without first verifying the existing login"
14703
+ })
14704
+ };
14705
+ async run() {
14706
+ const { flags } = await this.parse(SignupCommand);
14707
+ let releaseCredentialsLock;
14708
+ try {
14709
+ releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
14710
+ } catch (error) {
14711
+ throw cliError(error instanceof Error ? error.message : String(error));
14712
+ }
14713
+ try {
14714
+ await this.runWithCredentialLock(flags);
14715
+ } finally {
14716
+ releaseCredentialsLock();
14717
+ }
14718
+ }
14719
+ async runWithCredentialLock(flags) {
14720
+ await runSignupWithCredentialLock({
14721
+ configDir: this.config.configDir,
14722
+ flags
14723
+ });
14724
+ }
14725
+ };
14726
+ //#endregion
12370
14727
  //#region src/oclif/commands/whoami.ts
12371
14728
  var WhoamiCommand = class WhoamiCommand extends Command {
12372
14729
  static description = `Print the account currently authenticated by the API key. Useful as a credentials smoke test: confirms the key is live and shows which account it belongs to.`;
@@ -12509,7 +14866,7 @@ var ListOperationsCommand = class extends Command {
12509
14866
  }
12510
14867
  };
12511
14868
  function lookupOperation(id) {
12512
- const trimmed = id.trim();
14869
+ const trimmed = resolveOperationAlias(id.trim());
12513
14870
  const sep = trimmed.indexOf(":");
12514
14871
  const tag = sep === -1 ? "" : trimmed.slice(0, sep);
12515
14872
  const cmd = sep === -1 ? trimmed : trimmed.slice(sep + 1);
@@ -12525,7 +14882,7 @@ function lookupOperation(id) {
12525
14882
  }
12526
14883
  var DescribeCommand = class DescribeCommand extends Command {
12527
14884
  static args = { command: Args.string({
12528
- description: "Command id to describe, in `<topic>:<command>` form (e.g. `emails:get-email`). Run `primitive list-operations | jq -r '.[] | \"\\(.tagCommand):\\(.command)\"'` to enumerate.",
14885
+ description: "Command id to describe, e.g. `emails:list` or `emails:get-email`. Run `primitive list-operations | jq -r '.[] | \"\\(.tagCommand):\\(.command)\"'` to enumerate generated operation ids.",
12529
14886
  required: true
12530
14887
  }) };
12531
14888
  static description = `Print the full operation manifest entry for a single API command, including the path, request schema, response schema, and per-field descriptions sourced from the OpenAPI spec.
@@ -12533,15 +14890,15 @@ var DescribeCommand = class DescribeCommand extends Command {
12533
14890
  The manifest entry's \`responseSchema\` carries the inlined JSON Schema for the operation's 200/201 \`data\` envelope contents (\`$ref\`s resolved). Use it to look up what specific response fields mean. Examples:
12534
14891
 
12535
14892
  # Which of EmailDetail's sender-shaped fields is canonical?
12536
- primitive describe emails:get-email | jq '.responseSchema.properties | keys'
12537
- primitive describe emails:get-email | jq -r '.responseSchema.properties.from_email.description'
14893
+ primitive describe emails:get | jq '.responseSchema.properties | keys'
14894
+ primitive describe emails:get | jq -r '.responseSchema.properties.from_email.description'
12538
14895
 
12539
14896
  # What does each value of SentEmailStatus mean?
12540
- primitive describe sending:get-sent-email | jq -r '.responseSchema.properties.status.description'
14897
+ primitive describe sent:get | jq -r '.responseSchema.properties.status.description'
12541
14898
 
12542
14899
  \`requestSchema\` is the same shape for the request body when one exists. For a single field across many operations at once, use \`primitive list-operations | jq\` instead.`;
12543
14900
  static summary = "Describe a single API operation in detail";
12544
- static examples = ["<%= config.bin %> describe emails:get-email", "<%= config.bin %> describe sending:send-email"];
14901
+ static examples = ["<%= config.bin %> describe emails:get", "<%= config.bin %> describe sent:get"];
12545
14902
  async run() {
12546
14903
  const { args } = await this.parse(DescribeCommand);
12547
14904
  const { match, candidates } = lookupOperation(args.command);
@@ -12577,13 +14934,66 @@ var CompletionCommand = class CompletionCommand extends Command {
12577
14934
  function commandId(operation) {
12578
14935
  return `${operation.tagCommand}:${operation.command}`;
12579
14936
  }
14937
+ const CANONICAL_OPERATION_ALIASES = {
14938
+ "account:show": "account:get-account",
14939
+ "account:storage": "account:get-storage-stats",
14940
+ "account:webhook-secret": "account:get-webhook-secret",
14941
+ "deliveries:list": "webhook-deliveries:list-deliveries",
14942
+ "deliveries:replay": "webhook-deliveries:replay-delivery",
14943
+ "domains:add": "domains:add-domain",
14944
+ "domains:delete": "domains:delete-domain",
14945
+ "domains:list": "domains:list-domains",
14946
+ "domains:update": "domains:update-domain",
14947
+ "domains:verify": "domains:verify-domain",
14948
+ "emails:delete": "emails:delete-email",
14949
+ "emails:discard-content": "emails:discard-email-content",
14950
+ "emails:download-raw": "emails:download-raw-email",
14951
+ "emails:get": "emails:get-email",
14952
+ "emails:list": "emails:list-emails",
14953
+ "emails:replay-webhooks": "emails:replay-email-webhooks",
14954
+ "emails:search": "emails:search-emails",
14955
+ "endpoints:create": "endpoints:create-endpoint",
14956
+ "endpoints:delete": "endpoints:delete-endpoint",
14957
+ "endpoints:list": "endpoints:list-endpoints",
14958
+ "endpoints:test": "endpoints:test-endpoint",
14959
+ "endpoints:update": "endpoints:update-endpoint",
14960
+ "filters:create": "filters:create-filter",
14961
+ "filters:delete": "filters:delete-filter",
14962
+ "filters:list": "filters:list-filters",
14963
+ "filters:update": "filters:update-filter",
14964
+ "functions:delete": "functions:delete-function",
14965
+ "functions:delete-secret": "functions:delete-function-secret",
14966
+ "functions:get": "functions:get-function",
14967
+ "functions:list": "functions:list-functions",
14968
+ "functions:list-secrets": "functions:list-function-secrets",
14969
+ "functions:logs": "functions:list-function-logs",
14970
+ "sending:get": "sending:get-sent-email",
14971
+ "sending:list": "sending:list-sent-emails",
14972
+ "sending:permissions": "sending:get-send-permissions",
14973
+ "sending:reply": "sending:reply-to-email",
14974
+ "sending:send": "sending:send-email",
14975
+ "sent:get": "sending:get-sent-email",
14976
+ "sent:list": "sending:list-sent-emails",
14977
+ "webhook-deliveries:list": "webhook-deliveries:list-deliveries",
14978
+ "webhook-deliveries:replay": "webhook-deliveries:replay-delivery"
14979
+ };
14980
+ const DESCRIBE_OPERATION_ALIASES = {
14981
+ ...CANONICAL_OPERATION_ALIASES,
14982
+ reply: "sending:reply-to-email"
14983
+ };
14984
+ function resolveOperationAlias(id) {
14985
+ return DESCRIBE_OPERATION_ALIASES[id] ?? id;
14986
+ }
12580
14987
  const OVERRIDDEN_OPERATION_IDS = new Set(["functions:test-function"]);
14988
+ const generatedCommands = Object.fromEntries(operationManifest.filter((operation) => !OVERRIDDEN_OPERATION_IDS.has(commandId(operation))).map((operation) => [commandId(operation), createOperationCommand(operation)]));
12581
14989
  const COMMANDS = {
12582
14990
  completion: CompletionCommand,
12583
14991
  "list-operations": ListOperationsCommand,
12584
14992
  describe: DescribeCommand,
12585
14993
  send: SendCommand,
14994
+ reply: ReplyCommand,
12586
14995
  login: LoginCommand,
14996
+ signup: SignupCommand,
12587
14997
  logout: LogoutCommand,
12588
14998
  whoami: WhoamiCommand,
12589
14999
  doctor: DoctorCommand,
@@ -12591,11 +15001,18 @@ const COMMANDS = {
12591
15001
  "emails:watch": EmailsWatchCommand,
12592
15002
  "emails:wait": EmailsWaitCommand,
12593
15003
  "functions:init": FunctionsInitCommand,
15004
+ "functions:templates": FunctionsTemplatesCommand,
12594
15005
  "functions:deploy": FunctionsDeployCommand,
12595
15006
  "functions:redeploy": FunctionsRedeployCommand,
12596
15007
  "functions:set-secret": FunctionsSetSecretCommand,
15008
+ "functions:test": FunctionsTestFunctionCommand,
12597
15009
  "functions:test-function": FunctionsTestFunctionCommand,
12598
- ...Object.fromEntries(operationManifest.filter((operation) => !OVERRIDDEN_OPERATION_IDS.has(commandId(operation))).map((operation) => [commandId(operation), createOperationCommand(operation)]))
15010
+ ...Object.fromEntries(Object.entries(CANONICAL_OPERATION_ALIASES).map(([alias, target]) => {
15011
+ const command = generatedCommands[target];
15012
+ if (!command) throw new Error(`Missing generated command target for alias ${alias}`);
15013
+ return [alias, command];
15014
+ })),
15015
+ ...generatedCommands
12599
15016
  };
12600
15017
  //#endregion
12601
- export { COMMANDS, lookupOperation };
15018
+ export { CANONICAL_OPERATION_ALIASES, COMMANDS, lookupOperation };