@primitivedotdev/cli 0.26.3 → 0.28.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 +21 -4
  2. package/dist/oclif/index.js +1206 -111
  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) => {
@@ -664,11 +666,13 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
664
666
  replayDelivery: () => replayDelivery,
665
667
  replayEmailWebhooks: () => replayEmailWebhooks,
666
668
  replyToEmail: () => replyToEmail,
669
+ resendCliSignupVerification: () => resendCliSignupVerification,
667
670
  rotateWebhookSecret: () => rotateWebhookSecret,
668
671
  searchEmails: () => searchEmails,
669
672
  sendEmail: () => sendEmail,
670
673
  setFunctionSecret: () => setFunctionSecret,
671
674
  startCliLogin: () => startCliLogin,
675
+ startCliSignup: () => startCliSignup,
672
676
  testEndpoint: () => testEndpoint,
673
677
  testFunction: () => testFunction,
674
678
  updateAccount: () => updateAccount,
@@ -676,6 +680,7 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
676
680
  updateEndpoint: () => updateEndpoint,
677
681
  updateFilter: () => updateFilter,
678
682
  updateFunction: () => updateFunction,
683
+ verifyCliSignup: () => verifyCliSignup,
679
684
  verifyDomain: () => verifyDomain
680
685
  });
681
686
  /**
@@ -711,6 +716,54 @@ const pollCliLogin = (options) => (options.client ?? client).post({
711
716
  }
712
717
  });
713
718
  /**
719
+ * Start CLI account signup
720
+ *
721
+ * Starts a terminal-native CLI signup. The API validates the signup code,
722
+ * creates a pending signup session, sends an email verification code, and
723
+ * returns an opaque signup token used by the resend and verify steps. This
724
+ * endpoint does not require an API key.
725
+ *
726
+ */
727
+ const startCliSignup = (options) => (options.client ?? client).post({
728
+ url: "/cli/signup/start",
729
+ ...options,
730
+ headers: {
731
+ ...options.body !== void 0 && { "Content-Type": "application/json" },
732
+ ...options.headers
733
+ }
734
+ });
735
+ /**
736
+ * Resend CLI signup verification code
737
+ *
738
+ * Sends a new email verification code for a pending CLI signup session.
739
+ * This endpoint does not require an API key.
740
+ *
741
+ */
742
+ const resendCliSignupVerification = (options) => (options.client ?? client).post({
743
+ url: "/cli/signup/resend",
744
+ ...options,
745
+ headers: {
746
+ ...options.body !== void 0 && { "Content-Type": "application/json" },
747
+ ...options.headers
748
+ }
749
+ });
750
+ /**
751
+ * Verify CLI signup and create API key
752
+ *
753
+ * Verifies the email code for a CLI signup session, creates the account,
754
+ * redeems the reserved signup code, mints an org-scoped CLI API key, and
755
+ * returns the raw key exactly once. This endpoint does not require an API key.
756
+ *
757
+ */
758
+ const verifyCliSignup = (options) => (options.client ?? client).post({
759
+ url: "/cli/signup/verify",
760
+ ...options,
761
+ headers: {
762
+ ...options.body !== void 0 && { "Content-Type": "application/json" },
763
+ ...options.headers
764
+ }
765
+ });
766
+ /**
714
767
  * Revoke the current CLI API key
715
768
  *
716
769
  * Revokes the API key used to authenticate the request. CLI clients use
@@ -1451,8 +1504,9 @@ const listFunctions = (options) => (options?.client ?? client).get({
1451
1504
  * than relying on external imports.
1452
1505
  *
1453
1506
  * **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.
1507
+ * (optional) is capped at 5 MiB UTF-8, stored with each deployment
1508
+ * attempt, and sent to the runtime so stack traces can resolve to
1509
+ * original source files.
1456
1510
  *
1457
1511
  * **Auto-wiring.** On successful deploy, Primitive automatically
1458
1512
  * creates a webhook endpoint that delivers inbound mail to the
@@ -1525,11 +1579,10 @@ const getFunction = (options) => (options.client ?? client).get({
1525
1579
  * passing the same `code` re-runs the deploy and refreshes the
1526
1580
  * binding set with the latest values from the secrets table.
1527
1581
  *
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.
1582
+ * On deploy failure, the previously-deployed code stays live; the
1583
+ * runtime never serves a half-built bundle. The response uses
1584
+ * `error.code` `deploy_failed`, and the function's `deploy_error`
1585
+ * field carries the latest deploy error for dashboard/API reads.
1533
1586
  *
1534
1587
  */
1535
1588
  const updateFunction = (options) => (options.client ?? client).put({
@@ -1705,7 +1758,7 @@ const openapiDocument = {
1705
1758
  "info": {
1706
1759
  "title": "Primitive API",
1707
1760
  "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",
1761
+ "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
1762
  "contact": {
1710
1763
  "name": "Primitive",
1711
1764
  "url": "https://primitive.dev"
@@ -1877,6 +1930,97 @@ const openapiDocument = {
1877
1930
  }
1878
1931
  }
1879
1932
  } },
1933
+ "/cli/signup/start": { "post": {
1934
+ "operationId": "startCliSignup",
1935
+ "summary": "Start CLI account signup",
1936
+ "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",
1937
+ "tags": ["CLI"],
1938
+ "security": [],
1939
+ "requestBody": {
1940
+ "required": true,
1941
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StartCliSignupInput" } } }
1942
+ },
1943
+ "responses": {
1944
+ "201": {
1945
+ "description": "CLI signup session created and verification email sent",
1946
+ "headers": { "Cache-Control": {
1947
+ "schema": { "type": "string" },
1948
+ "description": "Always `no-store`"
1949
+ } },
1950
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
1951
+ "type": "object",
1952
+ "properties": { "data": { "$ref": "#/components/schemas/CliSignupStartResult" } }
1953
+ }] } } }
1954
+ },
1955
+ "400": { "$ref": "#/components/responses/ValidationError" },
1956
+ "429": { "$ref": "#/components/responses/RateLimited" }
1957
+ }
1958
+ } },
1959
+ "/cli/signup/resend": { "post": {
1960
+ "operationId": "resendCliSignupVerification",
1961
+ "summary": "Resend CLI signup verification code",
1962
+ "description": "Sends a new email verification code for a pending CLI signup session.\nThis endpoint does not require an API key.\n",
1963
+ "tags": ["CLI"],
1964
+ "security": [],
1965
+ "requestBody": {
1966
+ "required": true,
1967
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResendCliSignupVerificationInput" } } }
1968
+ },
1969
+ "responses": {
1970
+ "200": {
1971
+ "description": "Verification email resent",
1972
+ "headers": { "Cache-Control": {
1973
+ "schema": { "type": "string" },
1974
+ "description": "Always `no-store`"
1975
+ } },
1976
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
1977
+ "type": "object",
1978
+ "properties": { "data": { "$ref": "#/components/schemas/CliSignupResendResult" } }
1979
+ }] } } }
1980
+ },
1981
+ "400": {
1982
+ "description": "Invalid token or expired token",
1983
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
1984
+ },
1985
+ "429": {
1986
+ "description": "Global rate limit exceeded or resend requested too quickly",
1987
+ "headers": { "Retry-After": {
1988
+ "schema": { "type": "integer" },
1989
+ "description": "Seconds to wait before retrying"
1990
+ } },
1991
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
1992
+ }
1993
+ }
1994
+ } },
1995
+ "/cli/signup/verify": { "post": {
1996
+ "operationId": "verifyCliSignup",
1997
+ "summary": "Verify CLI signup and create API key",
1998
+ "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",
1999
+ "tags": ["CLI"],
2000
+ "security": [],
2001
+ "requestBody": {
2002
+ "required": true,
2003
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifyCliSignupInput" } } }
2004
+ },
2005
+ "responses": {
2006
+ "200": {
2007
+ "description": "CLI signup verified and API key created",
2008
+ "headers": { "Cache-Control": {
2009
+ "schema": { "type": "string" },
2010
+ "description": "Always `no-store`"
2011
+ } },
2012
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
2013
+ "type": "object",
2014
+ "properties": { "data": { "$ref": "#/components/schemas/CliSignupVerifyResult" } }
2015
+ }] } } }
2016
+ },
2017
+ "400": {
2018
+ "description": "Invalid request, invalid verification code, expired token, invalid signup code, rejected password, or account creation failure",
2019
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
2020
+ },
2021
+ "429": { "$ref": "#/components/responses/RateLimited" }
2022
+ }
2023
+ } },
1880
2024
  "/cli/logout": { "post": {
1881
2025
  "operationId": "cliLogout",
1882
2026
  "summary": "Revoke the current CLI API key",
@@ -3017,7 +3161,7 @@ const openapiDocument = {
3017
3161
  "post": {
3018
3162
  "operationId": "createFunction",
3019
3163
  "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",
3164
+ "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
3165
  "tags": ["Functions"],
3022
3166
  "requestBody": {
3023
3167
  "required": true,
@@ -3031,13 +3175,18 @@ const openapiDocument = {
3031
3175
  "properties": { "data": { "$ref": "#/components/schemas/CreateFunctionResult" } }
3032
3176
  }] } } }
3033
3177
  },
3034
- "400": { "$ref": "#/components/responses/ValidationError" },
3178
+ "400": {
3179
+ "description": "Invalid request parameters or customer-correctable deploy rejection",
3180
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
3181
+ },
3035
3182
  "401": { "$ref": "#/components/responses/Unauthorized" },
3036
3183
  "409": {
3037
3184
  "description": "A function with this name already exists in the org",
3038
3185
  "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
3039
3186
  },
3040
- "502": { "$ref": "#/components/responses/BadGateway" }
3187
+ "424": { "$ref": "#/components/responses/DeployFailed" },
3188
+ "429": { "$ref": "#/components/responses/DeployFailed" },
3189
+ "503": { "$ref": "#/components/responses/DeployFailed" }
3041
3190
  }
3042
3191
  }
3043
3192
  },
@@ -3063,7 +3212,7 @@ const openapiDocument = {
3063
3212
  "put": {
3064
3213
  "operationId": "updateFunction",
3065
3214
  "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",
3215
+ "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
3216
  "tags": ["Functions"],
3068
3217
  "requestBody": {
3069
3218
  "required": true,
@@ -3077,10 +3226,15 @@ const openapiDocument = {
3077
3226
  "properties": { "data": { "$ref": "#/components/schemas/FunctionDetail" } }
3078
3227
  }] } } }
3079
3228
  },
3080
- "400": { "$ref": "#/components/responses/ValidationError" },
3229
+ "400": {
3230
+ "description": "Invalid request parameters or customer-correctable deploy rejection",
3231
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
3232
+ },
3081
3233
  "401": { "$ref": "#/components/responses/Unauthorized" },
3082
3234
  "404": { "$ref": "#/components/responses/NotFound" },
3083
- "502": { "$ref": "#/components/responses/BadGateway" }
3235
+ "424": { "$ref": "#/components/responses/DeployFailed" },
3236
+ "429": { "$ref": "#/components/responses/DeployFailed" },
3237
+ "503": { "$ref": "#/components/responses/DeployFailed" }
3084
3238
  }
3085
3239
  },
3086
3240
  "delete": {
@@ -3399,6 +3553,19 @@ const openapiDocument = {
3399
3553
  }
3400
3554
  } }
3401
3555
  },
3556
+ "DeployFailed": {
3557
+ "description": "Function deploy could not be completed; previously deployed code remains live",
3558
+ "content": { "application/json": {
3559
+ "schema": { "$ref": "#/components/schemas/ErrorResponse" },
3560
+ "example": {
3561
+ "success": false,
3562
+ "error": {
3563
+ "code": "deploy_failed",
3564
+ "message": "Function deploy failed"
3565
+ }
3566
+ }
3567
+ } }
3568
+ },
3402
3569
  "RateLimited": {
3403
3570
  "description": "Rate limit exceeded",
3404
3571
  "headers": { "Retry-After": {
@@ -3758,6 +3925,162 @@ const openapiDocument = {
3758
3925
  "org_name"
3759
3926
  ]
3760
3927
  },
3928
+ "StartCliSignupInput": {
3929
+ "type": "object",
3930
+ "additionalProperties": false,
3931
+ "properties": {
3932
+ "email": {
3933
+ "type": "string",
3934
+ "format": "email",
3935
+ "maxLength": 254
3936
+ },
3937
+ "signup_code": {
3938
+ "type": "string",
3939
+ "minLength": 1,
3940
+ "maxLength": 128
3941
+ },
3942
+ "terms_accepted": {
3943
+ "type": "boolean",
3944
+ "const": true,
3945
+ "description": "Must be true to confirm acceptance of Primitive's Terms of Service and Privacy Policy"
3946
+ },
3947
+ "device_name": {
3948
+ "type": "string",
3949
+ "minLength": 1,
3950
+ "maxLength": 80,
3951
+ "description": "Human-readable device name used for the created CLI API key"
3952
+ },
3953
+ "metadata": {
3954
+ "type": "object",
3955
+ "additionalProperties": true,
3956
+ "description": "Optional client metadata stored with the signup session; serialized JSON must be 2048 bytes or fewer"
3957
+ }
3958
+ },
3959
+ "required": [
3960
+ "email",
3961
+ "signup_code",
3962
+ "terms_accepted"
3963
+ ]
3964
+ },
3965
+ "CliSignupStartResult": {
3966
+ "type": "object",
3967
+ "properties": {
3968
+ "signup_token": {
3969
+ "type": "string",
3970
+ "description": "Opaque token used to verify or resend the pending CLI signup"
3971
+ },
3972
+ "email": {
3973
+ "type": "string",
3974
+ "format": "email"
3975
+ },
3976
+ "expires_in": {
3977
+ "type": "integer",
3978
+ "description": "Seconds until the pending signup expires"
3979
+ },
3980
+ "resend_after": {
3981
+ "type": "integer",
3982
+ "description": "Minimum seconds before requesting another verification email"
3983
+ },
3984
+ "verification_code_length": {
3985
+ "type": "integer",
3986
+ "description": "Number of digits in the emailed verification code"
3987
+ }
3988
+ },
3989
+ "required": [
3990
+ "signup_token",
3991
+ "email",
3992
+ "expires_in",
3993
+ "resend_after",
3994
+ "verification_code_length"
3995
+ ]
3996
+ },
3997
+ "ResendCliSignupVerificationInput": {
3998
+ "type": "object",
3999
+ "additionalProperties": false,
4000
+ "properties": { "signup_token": {
4001
+ "type": "string",
4002
+ "minLength": 1
4003
+ } },
4004
+ "required": ["signup_token"]
4005
+ },
4006
+ "CliSignupResendResult": {
4007
+ "type": "object",
4008
+ "properties": {
4009
+ "email": {
4010
+ "type": "string",
4011
+ "format": "email"
4012
+ },
4013
+ "expires_in": {
4014
+ "type": "integer",
4015
+ "description": "Seconds until the pending signup expires"
4016
+ },
4017
+ "resend_after": {
4018
+ "type": "integer",
4019
+ "description": "Minimum seconds before requesting another verification email"
4020
+ },
4021
+ "verification_code_length": {
4022
+ "type": "integer",
4023
+ "description": "Number of digits in the emailed verification code"
4024
+ }
4025
+ },
4026
+ "required": [
4027
+ "email",
4028
+ "expires_in",
4029
+ "resend_after",
4030
+ "verification_code_length"
4031
+ ]
4032
+ },
4033
+ "VerifyCliSignupInput": {
4034
+ "type": "object",
4035
+ "additionalProperties": false,
4036
+ "properties": {
4037
+ "signup_token": {
4038
+ "type": "string",
4039
+ "minLength": 1
4040
+ },
4041
+ "verification_code": {
4042
+ "type": "string",
4043
+ "minLength": 1,
4044
+ "maxLength": 32
4045
+ },
4046
+ "password": {
4047
+ "type": "string",
4048
+ "minLength": 1,
4049
+ "maxLength": 1024
4050
+ }
4051
+ },
4052
+ "required": [
4053
+ "signup_token",
4054
+ "verification_code",
4055
+ "password"
4056
+ ]
4057
+ },
4058
+ "CliSignupVerifyResult": {
4059
+ "type": "object",
4060
+ "properties": {
4061
+ "api_key": {
4062
+ "type": "string",
4063
+ "description": "Newly-created API key for CLI authentication"
4064
+ },
4065
+ "key_id": {
4066
+ "type": "string",
4067
+ "format": "uuid"
4068
+ },
4069
+ "key_prefix": { "type": "string" },
4070
+ "org_id": {
4071
+ "type": "string",
4072
+ "format": "uuid"
4073
+ },
4074
+ "org_name": { "type": ["string", "null"] }
4075
+ },
4076
+ "required": [
4077
+ "api_key",
4078
+ "key_id",
4079
+ "key_prefix",
4080
+ "org_id",
4081
+ "org_name"
4082
+ ]
4083
+ },
3761
4084
  "CliLogoutInput": {
3762
4085
  "type": "object",
3763
4086
  "additionalProperties": false,
@@ -5224,7 +5547,7 @@ const openapiDocument = {
5224
5547
  "type": "string",
5225
5548
  "minLength": 1,
5226
5549
  "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"
5550
+ "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
5551
  }
5229
5552
  },
5230
5553
  "required": ["name", "code"]
@@ -5756,6 +6079,58 @@ const operationManifest = [
5756
6079
  "tag": "CLI",
5757
6080
  "tagCommand": "cli"
5758
6081
  },
6082
+ {
6083
+ "binaryResponse": false,
6084
+ "bodyRequired": true,
6085
+ "command": "resend-cli-signup-verification",
6086
+ "description": "Sends a new email verification code for a pending CLI signup session.\nThis endpoint does not require an API key.\n",
6087
+ "hasJsonBody": true,
6088
+ "method": "POST",
6089
+ "operationId": "resendCliSignupVerification",
6090
+ "path": "/cli/signup/resend",
6091
+ "pathParams": [],
6092
+ "queryParams": [],
6093
+ "requestSchema": {
6094
+ "type": "object",
6095
+ "additionalProperties": false,
6096
+ "properties": { "signup_token": {
6097
+ "type": "string",
6098
+ "minLength": 1
6099
+ } },
6100
+ "required": ["signup_token"]
6101
+ },
6102
+ "responseSchema": {
6103
+ "type": "object",
6104
+ "properties": {
6105
+ "email": {
6106
+ "type": "string",
6107
+ "format": "email"
6108
+ },
6109
+ "expires_in": {
6110
+ "type": "integer",
6111
+ "description": "Seconds until the pending signup expires"
6112
+ },
6113
+ "resend_after": {
6114
+ "type": "integer",
6115
+ "description": "Minimum seconds before requesting another verification email"
6116
+ },
6117
+ "verification_code_length": {
6118
+ "type": "integer",
6119
+ "description": "Number of digits in the emailed verification code"
6120
+ }
6121
+ },
6122
+ "required": [
6123
+ "email",
6124
+ "expires_in",
6125
+ "resend_after",
6126
+ "verification_code_length"
6127
+ ]
6128
+ },
6129
+ "sdkName": "resendCliSignupVerification",
6130
+ "summary": "Resend CLI signup verification code",
6131
+ "tag": "CLI",
6132
+ "tagCommand": "cli"
6133
+ },
5759
6134
  {
5760
6135
  "binaryResponse": false,
5761
6136
  "bodyRequired": false,
@@ -5822,8 +6197,160 @@ const operationManifest = [
5822
6197
  "interval"
5823
6198
  ]
5824
6199
  },
5825
- "sdkName": "startCliLogin",
5826
- "summary": "Start CLI browser login",
6200
+ "sdkName": "startCliLogin",
6201
+ "summary": "Start CLI browser login",
6202
+ "tag": "CLI",
6203
+ "tagCommand": "cli"
6204
+ },
6205
+ {
6206
+ "binaryResponse": false,
6207
+ "bodyRequired": true,
6208
+ "command": "start-cli-signup",
6209
+ "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",
6210
+ "hasJsonBody": true,
6211
+ "method": "POST",
6212
+ "operationId": "startCliSignup",
6213
+ "path": "/cli/signup/start",
6214
+ "pathParams": [],
6215
+ "queryParams": [],
6216
+ "requestSchema": {
6217
+ "type": "object",
6218
+ "additionalProperties": false,
6219
+ "properties": {
6220
+ "email": {
6221
+ "type": "string",
6222
+ "format": "email",
6223
+ "maxLength": 254
6224
+ },
6225
+ "signup_code": {
6226
+ "type": "string",
6227
+ "minLength": 1,
6228
+ "maxLength": 128
6229
+ },
6230
+ "terms_accepted": {
6231
+ "type": "boolean",
6232
+ "const": true,
6233
+ "description": "Must be true to confirm acceptance of Primitive's Terms of Service and Privacy Policy"
6234
+ },
6235
+ "device_name": {
6236
+ "type": "string",
6237
+ "minLength": 1,
6238
+ "maxLength": 80,
6239
+ "description": "Human-readable device name used for the created CLI API key"
6240
+ },
6241
+ "metadata": {
6242
+ "type": "object",
6243
+ "additionalProperties": true,
6244
+ "description": "Optional client metadata stored with the signup session; serialized JSON must be 2048 bytes or fewer"
6245
+ }
6246
+ },
6247
+ "required": [
6248
+ "email",
6249
+ "signup_code",
6250
+ "terms_accepted"
6251
+ ]
6252
+ },
6253
+ "responseSchema": {
6254
+ "type": "object",
6255
+ "properties": {
6256
+ "signup_token": {
6257
+ "type": "string",
6258
+ "description": "Opaque token used to verify or resend the pending CLI signup"
6259
+ },
6260
+ "email": {
6261
+ "type": "string",
6262
+ "format": "email"
6263
+ },
6264
+ "expires_in": {
6265
+ "type": "integer",
6266
+ "description": "Seconds until the pending signup expires"
6267
+ },
6268
+ "resend_after": {
6269
+ "type": "integer",
6270
+ "description": "Minimum seconds before requesting another verification email"
6271
+ },
6272
+ "verification_code_length": {
6273
+ "type": "integer",
6274
+ "description": "Number of digits in the emailed verification code"
6275
+ }
6276
+ },
6277
+ "required": [
6278
+ "signup_token",
6279
+ "email",
6280
+ "expires_in",
6281
+ "resend_after",
6282
+ "verification_code_length"
6283
+ ]
6284
+ },
6285
+ "sdkName": "startCliSignup",
6286
+ "summary": "Start CLI account signup",
6287
+ "tag": "CLI",
6288
+ "tagCommand": "cli"
6289
+ },
6290
+ {
6291
+ "binaryResponse": false,
6292
+ "bodyRequired": true,
6293
+ "command": "verify-cli-signup",
6294
+ "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",
6295
+ "hasJsonBody": true,
6296
+ "method": "POST",
6297
+ "operationId": "verifyCliSignup",
6298
+ "path": "/cli/signup/verify",
6299
+ "pathParams": [],
6300
+ "queryParams": [],
6301
+ "requestSchema": {
6302
+ "type": "object",
6303
+ "additionalProperties": false,
6304
+ "properties": {
6305
+ "signup_token": {
6306
+ "type": "string",
6307
+ "minLength": 1
6308
+ },
6309
+ "verification_code": {
6310
+ "type": "string",
6311
+ "minLength": 1,
6312
+ "maxLength": 32
6313
+ },
6314
+ "password": {
6315
+ "type": "string",
6316
+ "minLength": 1,
6317
+ "maxLength": 1024
6318
+ }
6319
+ },
6320
+ "required": [
6321
+ "signup_token",
6322
+ "verification_code",
6323
+ "password"
6324
+ ]
6325
+ },
6326
+ "responseSchema": {
6327
+ "type": "object",
6328
+ "properties": {
6329
+ "api_key": {
6330
+ "type": "string",
6331
+ "description": "Newly-created API key for CLI authentication"
6332
+ },
6333
+ "key_id": {
6334
+ "type": "string",
6335
+ "format": "uuid"
6336
+ },
6337
+ "key_prefix": { "type": "string" },
6338
+ "org_id": {
6339
+ "type": "string",
6340
+ "format": "uuid"
6341
+ },
6342
+ "org_name": { "type": ["string", "null"] }
6343
+ },
6344
+ "required": [
6345
+ "api_key",
6346
+ "key_id",
6347
+ "key_prefix",
6348
+ "org_id",
6349
+ "org_name"
6350
+ ]
6351
+ },
6352
+ "sdkName": "verifyCliSignup",
6353
+ "summary": "Verify CLI signup and create API key",
5827
6354
  "tag": "CLI",
5828
6355
  "tagCommand": "cli"
5829
6356
  },
@@ -7497,7 +8024,7 @@ const operationManifest = [
7497
8024
  "binaryResponse": false,
7498
8025
  "bodyRequired": true,
7499
8026
  "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",
8027
+ "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
8028
  "hasJsonBody": true,
7502
8029
  "method": "POST",
7503
8030
  "operationId": "createFunction",
@@ -7523,7 +8050,7 @@ const operationManifest = [
7523
8050
  "type": "string",
7524
8051
  "minLength": 1,
7525
8052
  "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"
8053
+ "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
8054
  }
7528
8055
  },
7529
8056
  "required": ["name", "code"]
@@ -8127,7 +8654,7 @@ const operationManifest = [
8127
8654
  "binaryResponse": false,
8128
8655
  "bodyRequired": true,
8129
8656
  "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",
8657
+ "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
8658
  "hasJsonBody": true,
8132
8659
  "method": "PUT",
8133
8660
  "operationId": "updateFunction",
@@ -9386,7 +9913,7 @@ const CREDENTIALS_FILE = "credentials.json";
9386
9913
  const CREDENTIALS_LOCK_DIR = "credentials.lock";
9387
9914
  const CREDENTIALS_LOCK_STALE_MS = 1800 * 1e3;
9388
9915
  const MALFORMED_CREDENTIALS_HINT = "Run `primitive logout` and then `primitive login`.";
9389
- function isRecord(value) {
9916
+ function isRecord$1(value) {
9390
9917
  return value !== null && typeof value === "object" && !Array.isArray(value);
9391
9918
  }
9392
9919
  function requireString(value, key) {
@@ -9409,7 +9936,7 @@ var StaleCredentialFormatError = class extends Error {
9409
9936
  }
9410
9937
  };
9411
9938
  function parseCredentials(raw) {
9412
- if (!isRecord(raw)) throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
9939
+ if (!isRecord$1(raw)) throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
9413
9940
  if (typeof raw.api_base_url_1 !== "string" && typeof raw.base_url === "string") throw new StaleCredentialFormatError();
9414
9941
  const orgName = raw.org_name;
9415
9942
  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 +10109,7 @@ function formatFunctionEndpointRedirect(match) {
9582
10109
  return [
9583
10110
  "This is a function endpoint. Function endpoints are tested differently. Run:",
9584
10111
  "",
9585
- ` primitive functions:test-function --id ${match.functionId}`,
10112
+ ` primitive functions test --id ${match.functionId}`,
9586
10113
  "",
9587
10114
  `(pass the function id, not the endpoint id. endpoint_id=${match.endpointId} function_id=${match.functionId})`
9588
10115
  ].join("\n");
@@ -9597,6 +10124,32 @@ async function maybeWriteFunctionEndpointRedirect(inputs) {
9597
10124
  return match;
9598
10125
  }
9599
10126
  //#endregion
10127
+ //#region src/oclif/idempotent-replay-banner.ts
10128
+ /**
10129
+ * If `data` carries `idempotent_replay: true`, write a multi-line
10130
+ * stderr banner explaining the replay condition and what the caller
10131
+ * can do to bypass it. Otherwise no-op.
10132
+ */
10133
+ function writeIdempotentReplayBannerIfReplay(data, options) {
10134
+ if (!isReplay(data)) return;
10135
+ const probe = data;
10136
+ const rowId = typeof probe.id === "string" ? probe.id : null;
10137
+ const status = typeof probe.status === "string" ? probe.status : null;
10138
+ const deliveryStatus = typeof probe.delivery_status === "string" ? probe.delivery_status : null;
10139
+ const lines = ["note: idempotent replay. this exact send already happened earlier.", " no new MX traffic was generated by this call. nothing new will arrive in any inbox."];
10140
+ if (rowId) lines.push(` cached row id: ${rowId}`);
10141
+ if (status || deliveryStatus) {
10142
+ const parts = [status ? `status=${status}` : null, deliveryStatus && deliveryStatus !== status ? `delivery_status=${deliveryStatus}` : null].filter((part) => part !== null);
10143
+ if (parts.length > 0) lines.push(` original ${parts.join(", ")}`);
10144
+ }
10145
+ lines.push(" to send a fresh copy: vary any field (subject, body, etc.) or", " pass a unique Idempotency-Key on the underlying API call.");
10146
+ options.write(`${lines.join("\n")}\n`);
10147
+ }
10148
+ function isReplay(data) {
10149
+ if (data === null || typeof data !== "object") return false;
10150
+ return data.idempotent_replay === true;
10151
+ }
10152
+ //#endregion
9600
10153
  //#region src/oclif/api-command.ts
9601
10154
  const API_ERROR_CODES = {
9602
10155
  accessDenied: "access_denied",
@@ -9727,26 +10280,26 @@ function coerceParameterValue(parameter, value) {
9727
10280
  if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") return value;
9728
10281
  throw new Errors.CLIError(`Unsupported flag value for --${parameter.name}`);
9729
10282
  }
9730
- function cliError$4(message) {
10283
+ function cliError$5(message) {
9731
10284
  return new Errors.CLIError(message, { exit: 1 });
9732
10285
  }
9733
10286
  function parseJson(source, flagLabel) {
9734
10287
  try {
9735
10288
  return JSON.parse(source);
9736
10289
  } catch (error) {
9737
- throw cliError$4(`${flagLabel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
10290
+ throw cliError$5(`${flagLabel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
9738
10291
  }
9739
10292
  }
9740
10293
  function readJsonBody(flags) {
9741
10294
  const bodyFile = flags["body-file"];
9742
10295
  const rawBody = flags["raw-body"];
9743
- if (bodyFile && rawBody) throw cliError$4("Use either --raw-body or --body-file, not both");
10296
+ if (bodyFile && rawBody) throw cliError$5("Use either --raw-body or --body-file, not both");
9744
10297
  if (typeof bodyFile === "string") {
9745
10298
  let contents;
9746
10299
  try {
9747
10300
  contents = readFileSync(bodyFile, "utf8");
9748
10301
  } catch (error) {
9749
- throw cliError$4(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
10302
+ throw cliError$5(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
9750
10303
  }
9751
10304
  return parseJson(contents, `--body-file ${bodyFile}`);
9752
10305
  }
@@ -9756,7 +10309,7 @@ function readTextFileFlag(path, flagLabel) {
9756
10309
  try {
9757
10310
  return readFileSync(path, "utf8");
9758
10311
  } catch (error) {
9759
- throw cliError$4(`Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`);
10312
+ throw cliError$5(`Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`);
9760
10313
  }
9761
10314
  }
9762
10315
  function extractErrorPayload(raw) {
@@ -9928,14 +10481,14 @@ function collectValues(parameters, flags) {
9928
10481
  return values;
9929
10482
  }
9930
10483
  const OPERATION_HINTS = {
9931
- createFunction: "Tip: prefer `primitive functions:deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
9932
- updateFunction: "Tip: prefer `primitive functions:redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
9933
- 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.",
9934
- 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."
10484
+ createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
10485
+ updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
10486
+ 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.",
10487
+ 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."
9935
10488
  };
9936
10489
  function createOperationCommand(operation) {
9937
10490
  const { flags, bodyFieldFlagToProperty } = buildFlags(operation);
9938
- const baseDescription = operation.description ?? `${operation.method} ${operation.path}`;
10491
+ const baseDescription = operation.description !== null && operation.description !== void 0 ? canonicalizeCliReferences(operation.description) : `${operation.method} ${operation.path}`;
9939
10492
  const schemaSummary = operation.hasJsonBody ? renderRequestSchemaSummary(operation.requestSchema) : null;
9940
10493
  const hint = OPERATION_HINTS[operation.sdkName];
9941
10494
  const descriptionWithSchema = schemaSummary ? `${baseDescription}\n\n${schemaSummary}` : baseDescription;
@@ -10031,6 +10584,9 @@ function createOperationCommand(operation) {
10031
10584
  const hint = EMPTY_RESULT_HINTS[operation.sdkName];
10032
10585
  if (hint) process.stderr.write(`${hint}\n`);
10033
10586
  }
10587
+ writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
10588
+ process.stderr.write(chunk);
10589
+ } });
10034
10590
  this.log(JSON.stringify(envelope?.data ?? null, null, 2));
10035
10591
  });
10036
10592
  }
@@ -10038,12 +10594,15 @@ function createOperationCommand(operation) {
10038
10594
  return OperationCommand;
10039
10595
  }
10040
10596
  const EMPTY_RESULT_HINTS = {
10041
- 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.",
10042
- listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints:create-endpoint --url <your-url>`.",
10043
- 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`.",
10044
- listDomains: "(no results) No domains on this account. Add one with `primitive domains:add-domain --domain <yourdomain.example>`.",
10597
+ 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.",
10598
+ listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints create --url <your-url>`.",
10599
+ 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`.",
10600
+ listDomains: "(no results) No domains on this account. Add one with `primitive domains add --domain <yourdomain.example>`.",
10045
10601
  listFilters: "(no results) No filter rules configured."
10046
10602
  };
10603
+ function canonicalizeCliReferences(description) {
10604
+ 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'`");
10605
+ }
10047
10606
  //#endregion
10048
10607
  //#region src/oclif/commands/doctor.ts
10049
10608
  const MIN_NODE_MAJOR = 22;
@@ -10204,7 +10763,7 @@ async function checkDomains(opts) {
10204
10763
  if (result.error) return {
10205
10764
  status: "warn",
10206
10765
  message: "could not list domains",
10207
- hint: "Run `primitive domains:list-domains` for the full error envelope."
10766
+ hint: "Run `primitive domains list` for the full error envelope."
10208
10767
  };
10209
10768
  const rows = result.data?.data ?? [];
10210
10769
  const active = rows.filter((row) => row.is_active === true);
@@ -10216,7 +10775,7 @@ async function checkDomains(opts) {
10216
10775
  if (active.length === 0) return {
10217
10776
  status: "warn",
10218
10777
  message: `${rows.length} domain(s), none active`,
10219
- hint: "Run `primitive domains:verify-domain --id <id>` for any domain you intend to send / receive on."
10778
+ hint: "Run `primitive domains verify --id <id>` for any domain you intend to send / receive on."
10220
10779
  };
10221
10780
  return {
10222
10781
  status: "ok",
@@ -10345,11 +10904,11 @@ function formatHeader(idWidth) {
10345
10904
  return `${"ID".padEnd(idWidth)} ${"RECEIVED (UTC)".padEnd(RECEIVED_DISPLAY_WIDTH)} ${"FROM".padEnd(ADDRESS_DISPLAY_WIDTH)} ${"TO".padEnd(ADDRESS_DISPLAY_WIDTH)} SUBJECT`;
10346
10905
  }
10347
10906
  var EmailsLatestCommand = class EmailsLatestCommand extends Command {
10348
- 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.
10907
+ 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.
10349
10908
 
10350
10909
  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.
10351
10910
 
10352
- 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.`;
10911
+ 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.`;
10353
10912
  static summary = "Show the most recent inbound emails as a compact table";
10354
10913
  static examples = [
10355
10914
  "<%= config.bin %> emails latest",
@@ -10529,7 +11088,7 @@ function sleep$1(ms) {
10529
11088
  //#endregion
10530
11089
  //#region src/oclif/commands/emails-wait.ts
10531
11090
  const DEFAULT_WAIT_TIMEOUT_SECONDS$1 = 300;
10532
- function cliError$3(message) {
11091
+ function cliError$4(message) {
10533
11092
  return new Errors.CLIError(message, { exit: 1 });
10534
11093
  }
10535
11094
  var EmailsWaitCommand = class EmailsWaitCommand extends Command {
@@ -10609,7 +11168,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
10609
11168
  try {
10610
11169
  since = sinceFromFlags(flags);
10611
11170
  } catch (error) {
10612
- throw cliError$3(error instanceof Error ? error.message : String(error));
11171
+ throw cliError$4(error instanceof Error ? error.message : String(error));
10613
11172
  }
10614
11173
  const filters = filtersFromFlags(flags);
10615
11174
  const deadline = flags.timeout === 0 ? null : Date.now() + flags.timeout * 1e3;
@@ -10660,7 +11219,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
10660
11219
  };
10661
11220
  //#endregion
10662
11221
  //#region src/oclif/commands/emails-watch.ts
10663
- function cliError$2(message) {
11222
+ function cliError$3(message) {
10664
11223
  return new Errors.CLIError(message, { exit: 1 });
10665
11224
  }
10666
11225
  var EmailsWatchCommand = class EmailsWatchCommand extends Command {
@@ -10737,7 +11296,7 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
10737
11296
  try {
10738
11297
  since = sinceFromFlags(flags);
10739
11298
  } catch (error) {
10740
- throw cliError$2(error instanceof Error ? error.message : String(error));
11299
+ throw cliError$3(error instanceof Error ? error.message : String(error));
10741
11300
  }
10742
11301
  const filters = filtersFromFlags(flags);
10743
11302
  const deadline = flags.seconds ? Date.now() + flags.seconds * 1e3 : null;
@@ -10967,14 +11526,14 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
10967
11526
  final update-function with the same bundle so the running handler
10968
11527
  picks up the bindings. If a secret write fails after the create
10969
11528
  step the function exists with whatever secrets succeeded and the
10970
- redeploy has NOT fired; re-run \`primitive functions:set-secret\`
10971
- for the missing keys, then \`primitive functions:redeploy\` to
11529
+ redeploy has NOT fired; re-run \`primitive functions set-secret\`
11530
+ for the missing keys, then \`primitive functions redeploy\` to
10972
11531
  push them live.`;
10973
11532
  static summary = "Deploy a new function from a bundled handler file";
10974
11533
  static examples = [
10975
- "<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js",
10976
- "<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
10977
- "<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com"
11534
+ "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js",
11535
+ "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
11536
+ "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com"
10978
11537
  ];
10979
11538
  static flags = {
10980
11539
  "api-key": Flags.string({
@@ -10999,7 +11558,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
10999
11558
  description: "Path to the bundled ESM handler file (single self-contained module). Loaded as the `code` body field.",
11000
11559
  required: true
11001
11560
  }),
11002
- "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." }),
11561
+ "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." }),
11003
11562
  secret: Flags.string({
11004
11563
  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}`,
11005
11564
  multiple: true
@@ -11074,10 +11633,10 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
11074
11633
  const succeeded = outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)";
11075
11634
  const pending = outcome.pendingKeys.length > 0 ? outcome.pendingKeys.join(", ") : "(none)";
11076
11635
  const allMissing = [outcome.failedKey, ...outcome.pendingKeys].join(", ");
11077
- 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`);
11636
+ 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`);
11078
11637
  } else if (outcome.stage === "redeploy") {
11079
11638
  const succeeded = outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)";
11080
- 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`);
11639
+ 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`);
11081
11640
  }
11082
11641
  writeErrorWithHints(outcome.payload);
11083
11642
  removeStaleSavedCredentialOnUnauthorized({
@@ -11094,8 +11653,8 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
11094
11653
  };
11095
11654
  //#endregion
11096
11655
  //#region src/oclif/commands/functions-init.ts
11097
- const SDK_VERSION_RANGE = "^0.27.1";
11098
- const CLI_VERSION_RANGE = "^0.26.0";
11656
+ const SDK_VERSION_RANGE = "^0.28.0";
11657
+ const CLI_VERSION_RANGE = "^0.28.0";
11099
11658
  const ESBUILD_VERSION_RANGE = "^0.27.0";
11100
11659
  const VALID_NAME = /^[a-z0-9][a-z0-9_-]{0,62}$/;
11101
11660
  function isValidFunctionName(name) {
@@ -11240,8 +11799,8 @@ function renderPackageJson(name) {
11240
11799
  type: "module",
11241
11800
  scripts: {
11242
11801
  build: "node build.mjs",
11243
- deploy: `npm run build && primitive functions:deploy --name ${name} --file ./dist/handler.js`,
11244
- redeploy: "npm run build && primitive functions:redeploy --id $PRIMITIVE_FUNCTION_ID --file ./dist/handler.js"
11802
+ deploy: `npm run build && primitive functions deploy --name ${name} --file ./dist/handler.js`,
11803
+ redeploy: "npm run build && primitive functions redeploy --id $PRIMITIVE_FUNCTION_ID --file ./dist/handler.js"
11245
11804
  },
11246
11805
  dependencies: { "@primitivedotdev/sdk": SDK_VERSION_RANGE },
11247
11806
  devDependencies: {
@@ -11312,7 +11871,7 @@ npm run build
11312
11871
  npm run deploy
11313
11872
  \`\`\`
11314
11873
 
11315
- The deploy step calls \`primitive functions:deploy\` (provided by the
11874
+ The deploy step calls \`primitive functions deploy\` (provided by the
11316
11875
  \`@primitivedotdev/cli\` package; install with
11317
11876
  \`npm install -g @primitivedotdev/cli\` or run via
11318
11877
  \`npx @primitivedotdev/cli@latest <command>\`). It requires
@@ -11388,12 +11947,12 @@ var FunctionsInitCommand = class FunctionsInitCommand extends Command {
11388
11947
  \`@primitivedotdev/sdk/api\` and demonstrates the canonical pattern:
11389
11948
  parse the email.received event, send a reply via the SDK, return a
11390
11949
  JSON envelope. The build script uses esbuild's JS API and emits
11391
- ./dist/handler.js, ready to hand to \`primitive functions:deploy --file\`.
11950
+ ./dist/handler.js, ready to hand to \`primitive functions deploy --file\`.
11392
11951
 
11393
11952
  Refuses to overwrite an existing directory. Use --out-dir to pick a
11394
11953
  different target path than ./<name>/.`;
11395
- static summary = "Scaffold a new Primitive Function project ready for functions:deploy";
11396
- static examples = ["<%= config.bin %> functions:init my-fn", "<%= config.bin %> functions:init my-fn --out-dir ./functions/my-fn"];
11954
+ static summary = "Scaffold a new Primitive Function project ready for functions deploy";
11955
+ static examples = ["<%= config.bin %> functions init my-fn", "<%= config.bin %> functions init my-fn --out-dir ./functions/my-fn"];
11397
11956
  static args = { name: Args.string({
11398
11957
  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.",
11399
11958
  required: true
@@ -11411,7 +11970,7 @@ var FunctionsInitCommand = class FunctionsInitCommand extends Command {
11411
11970
  this.log(` cd ${outDir}`);
11412
11971
  this.log(" npm install");
11413
11972
  this.log(" npm run build");
11414
- this.log(` primitive functions:deploy --name ${args.name} --file ./dist/handler.js`);
11973
+ this.log(` primitive functions deploy --name ${args.name} --file ./dist/handler.js`);
11415
11974
  }
11416
11975
  };
11417
11976
  //#endregion
@@ -11495,9 +12054,9 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
11495
12054
  API calls (set-secret per pair, then update-function).`;
11496
12055
  static summary = "Redeploy a function from a bundled handler file";
11497
12056
  static examples = [
11498
- "<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js",
11499
- "<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js --source-map-file ./bundle.js.map",
11500
- "<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com"
12057
+ "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js",
12058
+ "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --source-map-file ./bundle.js.map",
12059
+ "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com"
11501
12060
  ];
11502
12061
  static flags = {
11503
12062
  "api-key": Flags.string({
@@ -11588,10 +12147,10 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
11588
12147
  const succeeded = outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)";
11589
12148
  const pending = outcome.pendingKeys.length > 0 ? outcome.pendingKeys.join(", ") : "(none)";
11590
12149
  const allMissing = [outcome.failedKey, ...outcome.pendingKeys].join(", ");
11591
- 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`);
12150
+ 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`);
11592
12151
  } else if (outcome.stage === "redeploy") {
11593
12152
  const succeeded = outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)";
11594
- 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`);
12153
+ 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`);
11595
12154
  }
11596
12155
  writeErrorWithHints(outcome.payload);
11597
12156
  removeStaleSavedCredentialOnUnauthorized({
@@ -11673,7 +12232,7 @@ async function runSetSecret(api, params) {
11673
12232
  };
11674
12233
  }
11675
12234
  var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command {
11676
- 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.
12235
+ 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.
11677
12236
 
11678
12237
  Without --redeploy this is a plain secret upsert: the value is
11679
12238
  encrypted at rest but is NOT visible to the running handler until
@@ -11685,7 +12244,7 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
11685
12244
  underscores; first character is a letter or underscore). System-
11686
12245
  managed keys are reserved and rejected.`;
11687
12246
  static summary = "Write a function secret (optionally redeploying to push it live)";
11688
- 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"];
12247
+ 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"];
11689
12248
  static flags = {
11690
12249
  "api-key": Flags.string({
11691
12250
  description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
@@ -11713,7 +12272,7 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
11713
12272
  description: "Secret value (up to 4096 UTF-8 bytes). Encrypted at rest.",
11714
12273
  required: true
11715
12274
  }),
11716
- 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." }),
12275
+ 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." }),
11717
12276
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
11718
12277
  };
11719
12278
  async run() {
@@ -11764,8 +12323,8 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
11764
12323
  value: flags.value
11765
12324
  });
11766
12325
  if (outcome.kind === "error") {
11767
- 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");
11768
- 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");
12326
+ 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");
12327
+ 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");
11769
12328
  writeErrorWithHints(outcome.payload);
11770
12329
  removeStaleSavedCredentialOnUnauthorized({
11771
12330
  ...authFailureContext,
@@ -11786,10 +12345,10 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
11786
12345
  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.";
11787
12346
  static summary = "Trigger a test invocation; with --wait, watch it land";
11788
12347
  static examples = [
11789
- "<%= config.bin %> functions:test-function --id <fn-id>",
11790
- "<%= config.bin %> functions:test-function --id <fn-id> --local-part summarize",
11791
- "<%= config.bin %> functions:test-function --id <fn-id> --wait --show-sends",
11792
- "<%= config.bin %> functions:test-function --id <fn-id> --local-part summarize --wait --timeout 120"
12348
+ "<%= config.bin %> functions test --id <fn-id>",
12349
+ "<%= config.bin %> functions test --id <fn-id> --local-part summarize",
12350
+ "<%= config.bin %> functions test --id <fn-id> --wait --show-sends",
12351
+ "<%= config.bin %> functions test --id <fn-id> --local-part summarize --wait --timeout 120"
11793
12352
  ];
11794
12353
  static flags = {
11795
12354
  "api-key": Flags.string({
@@ -11946,7 +12505,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
11946
12505
  //#endregion
11947
12506
  //#region src/oclif/commands/login.ts
11948
12507
  const MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS = 60;
11949
- function cliError$1(message) {
12508
+ function cliError$2(message) {
11950
12509
  return new Errors.CLIError(message, { exit: 1 });
11951
12510
  }
11952
12511
  function sleep(ms) {
@@ -11965,10 +12524,10 @@ function openBrowser(url) {
11965
12524
  child.on("error", () => void 0);
11966
12525
  child.unref();
11967
12526
  }
11968
- function unwrapData$1(value) {
12527
+ function unwrapData$2(value) {
11969
12528
  return value?.data ?? null;
11970
12529
  }
11971
- function retryAfterSeconds(result) {
12530
+ function retryAfterSeconds$1(result) {
11972
12531
  const raw = result.response?.headers.get("retry-after");
11973
12532
  if (!raw) return null;
11974
12533
  const parsed = Number.parseInt(raw, 10);
@@ -12032,7 +12591,7 @@ var LoginCommand = class LoginCommand extends Command {
12032
12591
  try {
12033
12592
  releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
12034
12593
  } catch (error) {
12035
- throw cliError$1(error instanceof Error ? error.message : String(error));
12594
+ throw cliError$2(error instanceof Error ? error.message : String(error));
12036
12595
  }
12037
12596
  try {
12038
12597
  await this.runWithCredentialLock(flags);
@@ -12061,8 +12620,8 @@ var LoginCommand = class LoginCommand extends Command {
12061
12620
  if (existingStatus.status === "removed_stale") process.stderr.write("Continuing with a new Primitive CLI login...\n");
12062
12621
  else if (existingStatus.status === "blocked") {
12063
12622
  writeErrorWithHints(existingStatus.payload);
12064
- throw cliError$1(existingStatus.message);
12065
- } else throw cliError$1(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before logging in again.`);
12623
+ throw cliError$2(existingStatus.message);
12624
+ } else throw cliError$2(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before logging in again.`);
12066
12625
  }
12067
12626
  const apiClient = new PrimitiveApiClient({ apiBaseUrl1 });
12068
12627
  const started = await startCliLogin({
@@ -12072,10 +12631,10 @@ var LoginCommand = class LoginCommand extends Command {
12072
12631
  });
12073
12632
  if (started.error) {
12074
12633
  writeErrorWithHints(extractErrorPayload(started.error));
12075
- throw cliError$1("Could not start Primitive CLI login.");
12634
+ throw cliError$2("Could not start Primitive CLI login.");
12076
12635
  }
12077
- const start = unwrapData$1(started.data);
12078
- if (!start) throw cliError$1("Primitive API returned an empty CLI login response.");
12636
+ const start = unwrapData$2(started.data);
12637
+ if (!start) throw cliError$2("Primitive API returned an empty CLI login response.");
12079
12638
  process.stderr.write(`Your login code is: ${start.user_code}\n`);
12080
12639
  if (!flags["no-browser"]) {
12081
12640
  openBrowser(start.verification_uri_complete);
@@ -12095,8 +12654,8 @@ var LoginCommand = class LoginCommand extends Command {
12095
12654
  responseStyle: "fields"
12096
12655
  });
12097
12656
  if (polled.data) {
12098
- const login = unwrapData$1(polled.data);
12099
- if (!login) throw cliError$1("Primitive API returned an empty CLI poll response.");
12657
+ const login = unwrapData$2(polled.data);
12658
+ if (!login) throw cliError$2("Primitive API returned an empty CLI poll response.");
12100
12659
  saveCliCredentials(this.config.configDir, {
12101
12660
  api_key: login.api_key,
12102
12661
  api_base_url_1: apiBaseUrl1,
@@ -12118,25 +12677,25 @@ var LoginCommand = class LoginCommand extends Command {
12118
12677
  continue;
12119
12678
  }
12120
12679
  if (code === API_ERROR_CODES.slowDown) {
12121
- interval = Math.min(retryAfterSeconds(polled) ?? interval + 5, MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
12680
+ interval = Math.min(retryAfterSeconds$1(polled) ?? interval + 5, MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
12122
12681
  nextPollDelay = interval;
12123
12682
  continue;
12124
12683
  }
12125
- if (code === API_ERROR_CODES.accessDenied) throw cliError$1("Primitive CLI login was denied in the browser.");
12126
- if (code === API_ERROR_CODES.expiredToken) throw cliError$1("Primitive CLI login expired. Run `primitive login` again.");
12127
- if (code === API_ERROR_CODES.invalidDeviceCode) throw cliError$1("Primitive CLI login device code is invalid. Run `primitive login` again.");
12684
+ if (code === API_ERROR_CODES.accessDenied) throw cliError$2("Primitive CLI login was denied in the browser.");
12685
+ if (code === API_ERROR_CODES.expiredToken) throw cliError$2("Primitive CLI login expired. Run `primitive login` again.");
12686
+ if (code === API_ERROR_CODES.invalidDeviceCode) throw cliError$2("Primitive CLI login device code is invalid. Run `primitive login` again.");
12128
12687
  writeErrorWithHints(payload);
12129
- throw cliError$1("Primitive CLI login failed while polling for approval.");
12688
+ throw cliError$2("Primitive CLI login failed while polling for approval.");
12130
12689
  }
12131
- throw cliError$1("Primitive CLI login expired. Run `primitive login` again.");
12690
+ throw cliError$2("Primitive CLI login expired. Run `primitive login` again.");
12132
12691
  }
12133
12692
  };
12134
12693
  //#endregion
12135
12694
  //#region src/oclif/commands/logout.ts
12136
- function cliError(message) {
12695
+ function cliError$1(message) {
12137
12696
  return new Errors.CLIError(message, { exit: 1 });
12138
12697
  }
12139
- function unwrapData(value) {
12698
+ function unwrapData$1(value) {
12140
12699
  return value?.data ?? null;
12141
12700
  }
12142
12701
  var LogoutCommand = class LogoutCommand extends Command {
@@ -12154,7 +12713,7 @@ var LogoutCommand = class LogoutCommand extends Command {
12154
12713
  try {
12155
12714
  releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
12156
12715
  } catch (error) {
12157
- throw cliError(error instanceof Error ? error.message : String(error));
12716
+ throw cliError$1(error instanceof Error ? error.message : String(error));
12158
12717
  }
12159
12718
  try {
12160
12719
  await this.runWithCredentialLock(flags);
@@ -12173,7 +12732,7 @@ var LogoutCommand = class LogoutCommand extends Command {
12173
12732
  process.exitCode = 1;
12174
12733
  return;
12175
12734
  }
12176
- if (!credentials) throw cliError("Not logged in. Run `primitive login` to create saved CLI credentials.");
12735
+ if (!credentials) throw cliError$1("Not logged in. Run `primitive login` to create saved CLI credentials.");
12177
12736
  const apiBaseUrl1 = flags["api-base-url-1"] ? normalizeApiBaseUrl1(flags["api-base-url-1"]) : credentials.api_base_url_1;
12178
12737
  const apiClient = new PrimitiveApiClient({
12179
12738
  apiKey: credentials.api_key,
@@ -12195,15 +12754,101 @@ var LogoutCommand = class LogoutCommand extends Command {
12195
12754
  return;
12196
12755
  }
12197
12756
  writeErrorWithHints(payload);
12198
- throw cliError("Could not revoke the saved Primitive CLI API key.");
12757
+ throw cliError$1("Could not revoke the saved Primitive CLI API key.");
12199
12758
  }
12200
- const logout = unwrapData(result.data);
12759
+ const logout = unwrapData$1(result.data);
12201
12760
  deleteCliCredentials(this.config.configDir);
12202
12761
  const keyId = logout?.key_id ?? credentials.key_id;
12203
12762
  process.stderr.write(`Logged out and revoked API key ${keyId}.\n`);
12204
12763
  }
12205
12764
  };
12206
12765
  //#endregion
12766
+ //#region src/oclif/commands/reply.ts
12767
+ var ReplyCommand = class ReplyCommand extends Command {
12768
+ static description = `Reply to an inbound email.
12769
+
12770
+ 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.`;
12771
+ static summary = "Reply to an inbound email";
12772
+ static examples = [
12773
+ "<%= config.bin %> reply --id <inbound-email-id> --body 'Thanks, got it.'",
12774
+ "<%= config.bin %> reply --id <inbound-email-id> --html '<p>Thanks, got it.</p>' --wait",
12775
+ "<%= config.bin %> reply --id <inbound-email-id> --from 'Support <support@example.com>' --body 'Thanks!'"
12776
+ ];
12777
+ static flags = {
12778
+ "api-key": Flags.string({
12779
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
12780
+ env: "PRIMITIVE_API_KEY"
12781
+ }),
12782
+ "api-base-url-1": Flags.string({
12783
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
12784
+ env: "PRIMITIVE_API_BASE_URL_1",
12785
+ hidden: true
12786
+ }),
12787
+ "api-base-url-2": Flags.string({
12788
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
12789
+ env: "PRIMITIVE_API_BASE_URL_2",
12790
+ hidden: true
12791
+ }),
12792
+ id: Flags.string({
12793
+ description: "Inbound email id to reply to.",
12794
+ required: true
12795
+ }),
12796
+ body: Flags.string({ description: "Plain-text reply body. Either --body or --html (or both) is required." }),
12797
+ html: Flags.string({ description: "HTML reply body. Either --body or --html (or both) is required." }),
12798
+ from: Flags.string({ description: "Optional From header override. Defaults to the inbound recipient." }),
12799
+ 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." }),
12800
+ "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
12801
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
12802
+ };
12803
+ async run() {
12804
+ const { flags } = await this.parse(ReplyCommand);
12805
+ if (!flags.body && !flags.html) throw new Errors.CLIError("Either --body or --html (or both) is required.");
12806
+ await runWithTiming(flags.time, async () => {
12807
+ const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
12808
+ const auth = resolveCliAuth({
12809
+ apiKey: flags["api-key"],
12810
+ apiBaseUrl1: flags["api-base-url-1"],
12811
+ apiBaseUrl2: flags["api-base-url-2"],
12812
+ configDir: this.config.configDir
12813
+ });
12814
+ const apiClient = new PrimitiveApiClient({
12815
+ apiKey: auth.apiKey,
12816
+ apiBaseUrl1: auth.apiBaseUrl1,
12817
+ apiBaseUrl2: auth.apiBaseUrl2
12818
+ });
12819
+ const result = await replyToEmail({
12820
+ body: {
12821
+ ...flags.body !== void 0 ? { body_text: flags.body } : {},
12822
+ ...flags.html !== void 0 ? { body_html: flags.html } : {},
12823
+ ...flags.from !== void 0 ? { from: flags.from } : {},
12824
+ ...flags.wait !== void 0 ? { wait: flags.wait } : {},
12825
+ ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
12826
+ },
12827
+ client: apiClient.client,
12828
+ path: { id: flags.id },
12829
+ responseStyle: "fields"
12830
+ });
12831
+ if (result.error) {
12832
+ const errorPayload = extractErrorPayload(result.error);
12833
+ writeErrorWithHints(errorPayload);
12834
+ removeStaleSavedCredentialOnUnauthorized({
12835
+ auth,
12836
+ baseUrlOverridden,
12837
+ configDir: this.config.configDir,
12838
+ payload: errorPayload
12839
+ });
12840
+ process.exitCode = 1;
12841
+ return;
12842
+ }
12843
+ const envelope = result.data;
12844
+ writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
12845
+ process.stderr.write(chunk);
12846
+ } });
12847
+ this.log(JSON.stringify(envelope?.data ?? null, null, 2));
12848
+ });
12849
+ }
12850
+ };
12851
+ //#endregion
12207
12852
  //#region src/oclif/commands/send.ts
12208
12853
  const SUBJECT_MAX_LENGTH = 200;
12209
12854
  function deriveSubject(body) {
@@ -12235,17 +12880,17 @@ async function pickDefaultFromAddress(apiClient, authFailureContext) {
12235
12880
  throw new Errors.CLIError(`Could not look up your verified domains to default --from. Pass --from explicitly. Underlying error: ${formatErrorPayload(errorPayload)}`);
12236
12881
  }
12237
12882
  const first = result.data?.data?.find(isVerifiedDomain);
12238
- 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.");
12883
+ 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.");
12239
12884
  return `agent@${first.domain}`;
12240
12885
  }
12241
12886
  var SendCommand = class SendCommand extends Command {
12242
- static description = `Send an outbound email. Agent-grade shortcut for sending:send-email with sensible defaults.
12887
+ static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
12243
12888
 
12244
12889
  --from defaults to agent@<your-first-verified-outbound-domain> when omitted.
12245
12890
  --subject defaults to the first line of the body when omitted.
12246
12891
 
12247
12892
  For the full flag set (custom message-id threading on the wire,
12248
- references arrays, etc.), use \`primitive sending:send-email\`.`;
12893
+ references arrays, etc.), use \`primitive sending send\`.`;
12249
12894
  static summary = "Send an email (simplified, agent-friendly)";
12250
12895
  static examples = [
12251
12896
  "<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
@@ -12277,7 +12922,7 @@ var SendCommand = class SendCommand extends Command {
12277
12922
  subject: Flags.string({ description: "Subject line. Defaults to the first line of --body / --html when omitted." }),
12278
12923
  body: Flags.string({ description: "Plain-text message body. Either --body or --html (or both) is required." }),
12279
12924
  html: Flags.string({ description: "HTML message body. Either --body or --html (or both) is required." }),
12280
- "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>`." }),
12925
+ "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>`." }),
12281
12926
  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." }),
12282
12927
  "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
12283
12928
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
@@ -12330,11 +12975,402 @@ var SendCommand = class SendCommand extends Command {
12330
12975
  return;
12331
12976
  }
12332
12977
  const envelope = result.data;
12978
+ writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
12979
+ process.stderr.write(chunk);
12980
+ } });
12333
12981
  this.log(JSON.stringify(envelope?.data ?? null, null, 2));
12334
12982
  });
12335
12983
  }
12336
12984
  };
12337
12985
  //#endregion
12986
+ //#region src/oclif/commands/signup.ts
12987
+ const INVALID_VERIFICATION_CODE = "invalid_verification_code";
12988
+ const CLERK_PASSWORD_REJECTED = "clerk_password_rejected";
12989
+ const EXPIRED_TOKEN = "expired_token";
12990
+ const INVALID_SIGNUP_TOKEN = "invalid_signup_token";
12991
+ const SLOW_DOWN = "slow_down";
12992
+ const PENDING_SIGNUP_FILE = "signup.json";
12993
+ function cliError(message) {
12994
+ return new Errors.CLIError(message, { exit: 1 });
12995
+ }
12996
+ function unwrapData(value) {
12997
+ return value?.data ?? null;
12998
+ }
12999
+ function isRecord(value) {
13000
+ return value !== null && typeof value === "object" && !Array.isArray(value);
13001
+ }
13002
+ function pendingSignupFromJson(value) {
13003
+ if (!isRecord(value)) return null;
13004
+ 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;
13005
+ return {
13006
+ api_base_url_1: value.api_base_url_1,
13007
+ created_at: value.created_at,
13008
+ email: value.email,
13009
+ expires_at: value.expires_at,
13010
+ expires_in: value.expires_in,
13011
+ resend_after: value.resend_after,
13012
+ signup_token: value.signup_token,
13013
+ verification_code_length: value.verification_code_length
13014
+ };
13015
+ }
13016
+ function pendingSignupPath(configDir) {
13017
+ return join(configDir, PENDING_SIGNUP_FILE);
13018
+ }
13019
+ function deletePendingCliSignup(configDir) {
13020
+ rmSync(pendingSignupPath(configDir), { force: true });
13021
+ }
13022
+ function pendingSignupFromStart(start, apiBaseUrl1) {
13023
+ return {
13024
+ ...start,
13025
+ api_base_url_1: apiBaseUrl1,
13026
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
13027
+ expires_at: new Date(Date.now() + start.expires_in * 1e3).toISOString()
13028
+ };
13029
+ }
13030
+ function savePendingCliSignup(configDir, start, apiBaseUrl1) {
13031
+ mkdirSync(configDir, {
13032
+ mode: 448,
13033
+ recursive: true
13034
+ });
13035
+ const pending = pendingSignupFromStart(start, apiBaseUrl1);
13036
+ const path = pendingSignupPath(configDir);
13037
+ const tempPath = join(configDir, `${PENDING_SIGNUP_FILE}.${process$1.pid}.${randomUUID()}.tmp`);
13038
+ try {
13039
+ writeFileSync(tempPath, `${JSON.stringify(pending, null, 2)}\n`, { mode: 384 });
13040
+ chmodSync(tempPath, 384);
13041
+ renameSync(tempPath, path);
13042
+ chmodSync(path, 384);
13043
+ return pending;
13044
+ } catch (error) {
13045
+ rmSync(tempPath, { force: true });
13046
+ throw error;
13047
+ }
13048
+ }
13049
+ function loadPendingCliSignup(configDir, apiBaseUrl1) {
13050
+ const path = pendingSignupPath(configDir);
13051
+ let contents;
13052
+ try {
13053
+ contents = readFileSync(path, "utf8");
13054
+ } catch (error) {
13055
+ if (error && typeof error === "object" && error.code === "ENOENT") return null;
13056
+ throw error;
13057
+ }
13058
+ let pending;
13059
+ try {
13060
+ pending = pendingSignupFromJson(JSON.parse(contents));
13061
+ } catch {
13062
+ pending = null;
13063
+ }
13064
+ if (!pending) {
13065
+ deletePendingCliSignup(configDir);
13066
+ return null;
13067
+ }
13068
+ if (pending.api_base_url_1 !== apiBaseUrl1) return null;
13069
+ if (new Date(pending.expires_at).getTime() <= Date.now()) {
13070
+ deletePendingCliSignup(configDir);
13071
+ return null;
13072
+ }
13073
+ return {
13074
+ ...pending,
13075
+ expires_in: Math.max(0, Math.ceil((new Date(pending.expires_at).getTime() - Date.now()) / 1e3))
13076
+ };
13077
+ }
13078
+ function retryAfterSeconds(result) {
13079
+ const raw = result.response?.headers.get("retry-after");
13080
+ if (!raw) return null;
13081
+ const parsed = Number.parseInt(raw, 10);
13082
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
13083
+ }
13084
+ function normalizeAnswer(value) {
13085
+ return value.trim();
13086
+ }
13087
+ async function promptLine(question) {
13088
+ const rl = createInterface({
13089
+ input: process$1.stdin,
13090
+ output: process$1.stderr
13091
+ });
13092
+ try {
13093
+ return normalizeAnswer(await rl.question(question));
13094
+ } finally {
13095
+ rl.close();
13096
+ }
13097
+ }
13098
+ async function promptHidden(question) {
13099
+ 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.");
13100
+ return new Promise((resolve, reject) => {
13101
+ const input = process$1.stdin;
13102
+ let value = "";
13103
+ const cleanup = () => {
13104
+ input.setRawMode(false);
13105
+ input.pause();
13106
+ input.off("data", onData);
13107
+ };
13108
+ const finish = () => {
13109
+ cleanup();
13110
+ process$1.stderr.write("\n");
13111
+ resolve(value);
13112
+ };
13113
+ const onData = (chunk) => {
13114
+ const text = chunk.toString("utf8");
13115
+ for (const char of text) {
13116
+ if (char === "") {
13117
+ cleanup();
13118
+ process$1.stderr.write("\n");
13119
+ reject(cliError("Signup cancelled."));
13120
+ return;
13121
+ }
13122
+ if (char === "\r" || char === "\n") {
13123
+ finish();
13124
+ return;
13125
+ }
13126
+ if (char === "\b" || char === "") {
13127
+ value = value.slice(0, -1);
13128
+ continue;
13129
+ }
13130
+ value += char;
13131
+ }
13132
+ };
13133
+ process$1.stderr.write(question);
13134
+ input.setRawMode(true);
13135
+ input.resume();
13136
+ input.on("data", onData);
13137
+ });
13138
+ }
13139
+ function formatSignupSeconds(seconds) {
13140
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) return "soon";
13141
+ if (seconds < 60) return `${Math.ceil(seconds)} seconds`;
13142
+ const minutes = Math.ceil(seconds / 60);
13143
+ return `${minutes} minute${minutes === 1 ? "" : "s"}`;
13144
+ }
13145
+ function shouldRetrySignupPassword(errorCode) {
13146
+ return errorCode === CLERK_PASSWORD_REJECTED;
13147
+ }
13148
+ function signupErrorMessage(payload) {
13149
+ if (!isRecord(payload)) return null;
13150
+ const message = (isRecord(payload.error) ? payload.error : payload).message;
13151
+ return typeof message === "string" && message.trim() ? message : null;
13152
+ }
13153
+ async function promptRequired(question) {
13154
+ while (true) {
13155
+ const value = await promptLine(question);
13156
+ if (value) return value;
13157
+ process$1.stderr.write("Please enter a value.\n");
13158
+ }
13159
+ }
13160
+ async function promptRequiredPassword(question) {
13161
+ while (true) {
13162
+ const value = await promptHidden(question);
13163
+ if (value) return value;
13164
+ process$1.stderr.write("Please enter a password.\n");
13165
+ }
13166
+ }
13167
+ async function promptNewPassword() {
13168
+ while (true) {
13169
+ const password = await promptRequiredPassword("Password: ");
13170
+ if (password === await promptRequiredPassword("Confirm password: ")) return password;
13171
+ process$1.stderr.write("Passwords did not match. Try again.\n");
13172
+ }
13173
+ }
13174
+ async function confirmTerms() {
13175
+ process$1.stderr.write("By creating an account, you agree to Primitive's Terms of Service and Privacy Policy:\n");
13176
+ process$1.stderr.write(" https://primitive.dev/terms\n");
13177
+ process$1.stderr.write(" https://primitive.dev/privacy\n");
13178
+ const answer = (await promptRequired("Type 'yes' to continue: ")).toLowerCase();
13179
+ if (answer !== "yes" && answer !== "y") throw cliError("You must accept the terms to create an account.");
13180
+ }
13181
+ async function resendVerificationCode(params) {
13182
+ const resent = await (params.deps.resendCliSignupVerification ?? resendCliSignupVerification)({
13183
+ body: { signup_token: params.start.signup_token },
13184
+ client: params.apiClient.client,
13185
+ responseStyle: "fields"
13186
+ });
13187
+ if (resent.data) {
13188
+ const resend = unwrapData(resent.data);
13189
+ const next = resend ? {
13190
+ email: resend.email,
13191
+ expires_in: resend.expires_in,
13192
+ resend_after: resend.resend_after,
13193
+ signup_token: params.start.signup_token,
13194
+ verification_code_length: resend.verification_code_length
13195
+ } : params.start;
13196
+ savePendingCliSignup(params.configDir, next, params.apiBaseUrl1);
13197
+ process$1.stderr.write(`Sent a new ${next.verification_code_length}-digit verification code. It expires in ${formatSignupSeconds(next.expires_in)}.\n`);
13198
+ return next;
13199
+ }
13200
+ const payload = extractErrorPayload(resent.error);
13201
+ if (extractErrorCode(payload) === SLOW_DOWN) {
13202
+ const suffix = ` Wait ${formatSignupSeconds(retryAfterSeconds(resent) ?? params.start.resend_after)} before trying again.`;
13203
+ process$1.stderr.write(`Verification email was sent recently.${suffix}\n`);
13204
+ return params.start;
13205
+ }
13206
+ writeErrorWithHints(payload);
13207
+ throw cliError("Could not resend Primitive CLI signup verification email.");
13208
+ }
13209
+ async function runSignupWithCredentialLock(params) {
13210
+ const { configDir, flags } = params;
13211
+ const deps = params.deps ?? {};
13212
+ const promptRequiredFn = deps.promptRequired ?? promptRequired;
13213
+ const promptNewPasswordFn = deps.promptNewPassword ?? promptNewPassword;
13214
+ const confirmTermsFn = deps.confirmTerms ?? confirmTerms;
13215
+ const startFn = deps.startCliSignup ?? startCliSignup;
13216
+ const verifyFn = deps.verifyCliSignup ?? verifyCliSignup;
13217
+ const checkExistingLoginFn = deps.checkExistingLogin ?? checkExistingLogin;
13218
+ const apiBaseUrl1 = normalizeApiBaseUrl1(flags["api-base-url-1"]);
13219
+ let existing;
13220
+ try {
13221
+ existing = loadCliCredentials(configDir);
13222
+ } catch (error) {
13223
+ if (!flags.force) throw error;
13224
+ const detail = error instanceof Error ? error.message : String(error);
13225
+ process$1.stderr.write(`Replacing unreadable Primitive CLI credentials because --force was set: ${detail}\n`);
13226
+ existing = null;
13227
+ }
13228
+ if (existing && flags.force) process$1.stderr.write("Replacing saved Primitive CLI credentials after signup because --force was set.\n");
13229
+ else if (existing) {
13230
+ const existingStatus = await checkExistingLoginFn({
13231
+ apiBaseUrl1: flags["api-base-url-1"],
13232
+ configDir,
13233
+ credentials: existing
13234
+ });
13235
+ if (existingStatus.status === "removed_stale") process$1.stderr.write("Continuing with Primitive CLI signup...\n");
13236
+ else if (existingStatus.status === "blocked") {
13237
+ writeErrorWithHints(existingStatus.payload);
13238
+ throw cliError(existingStatus.message);
13239
+ } else throw cliError(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before creating a new account.`);
13240
+ }
13241
+ if (flags.force) deletePendingCliSignup(configDir);
13242
+ const apiClient = new PrimitiveApiClient({ apiBaseUrl1 });
13243
+ let start = flags.force ? null : loadPendingCliSignup(configDir, apiBaseUrl1);
13244
+ const resumed = Boolean(start);
13245
+ if (start) process$1.stderr.write(`Continuing pending Primitive CLI signup for ${start.email}.\n`);
13246
+ else {
13247
+ const signupCode = await promptRequiredFn("Signup code: ");
13248
+ await confirmTermsFn();
13249
+ const email = await promptRequiredFn("Email: ");
13250
+ const started = await startFn({
13251
+ body: {
13252
+ device_name: flags["device-name"] ?? hostname(),
13253
+ email,
13254
+ signup_code: signupCode,
13255
+ terms_accepted: true
13256
+ },
13257
+ client: apiClient.client,
13258
+ responseStyle: "fields"
13259
+ });
13260
+ if (started.error) {
13261
+ writeErrorWithHints(extractErrorPayload(started.error));
13262
+ throw cliError("Could not start Primitive CLI signup.");
13263
+ }
13264
+ const startResult = unwrapData(started.data);
13265
+ if (!startResult) throw cliError("Primitive API returned an empty CLI signup response.");
13266
+ start = savePendingCliSignup(configDir, startResult, apiBaseUrl1);
13267
+ }
13268
+ if (resumed) process$1.stderr.write(`Check your email for the ${start.verification_code_length}-digit verification code sent to ${start.email}.\n`);
13269
+ else process$1.stderr.write(`Sent a ${start.verification_code_length}-digit verification code to ${start.email}.\n`);
13270
+ process$1.stderr.write(`The code expires in ${formatSignupSeconds(start.expires_in)}.\n`);
13271
+ process$1.stderr.write(`Enter the code from the email, or type \`resend\` to send a new code after ${formatSignupSeconds(start.resend_after)}.\n`);
13272
+ while (true) {
13273
+ const verificationCode = await promptRequiredFn(`Verification code (${start.verification_code_length} digits): `);
13274
+ if (verificationCode.toLowerCase() === "resend") {
13275
+ start = await resendVerificationCode({
13276
+ apiBaseUrl1,
13277
+ apiClient,
13278
+ configDir,
13279
+ deps,
13280
+ start
13281
+ });
13282
+ continue;
13283
+ }
13284
+ let password = await promptNewPasswordFn();
13285
+ while (true) {
13286
+ const verified = await verifyFn({
13287
+ body: {
13288
+ password,
13289
+ signup_token: start.signup_token,
13290
+ verification_code: verificationCode
13291
+ },
13292
+ client: apiClient.client,
13293
+ responseStyle: "fields"
13294
+ });
13295
+ if (verified.data) {
13296
+ const signup = unwrapData(verified.data);
13297
+ if (!signup) throw cliError("Primitive API returned an empty CLI signup verification response.");
13298
+ saveCliCredentials(configDir, {
13299
+ api_key: signup.api_key,
13300
+ api_base_url_1: apiBaseUrl1,
13301
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
13302
+ key_id: signup.key_id,
13303
+ key_prefix: signup.key_prefix,
13304
+ org_id: signup.org_id,
13305
+ org_name: signup.org_name
13306
+ });
13307
+ deletePendingCliSignup(configDir);
13308
+ const org = signup.org_name ? ` (${signup.org_name})` : "";
13309
+ process$1.stderr.write(`Created account and logged in to org ${signup.org_id}${org}.\n`);
13310
+ process$1.stderr.write(`Saved credentials to ${credentialsPath(configDir)}.\n`);
13311
+ return;
13312
+ }
13313
+ const payload = extractErrorPayload(verified.error);
13314
+ const code = extractErrorCode(payload);
13315
+ if (code === INVALID_VERIFICATION_CODE) {
13316
+ process$1.stderr.write("Invalid verification code. Try again or type `resend`.\n");
13317
+ break;
13318
+ }
13319
+ if (shouldRetrySignupPassword(code)) {
13320
+ const message = signupErrorMessage(payload);
13321
+ if (message) process$1.stderr.write(`Password rejected: ${message}\n`);
13322
+ process$1.stderr.write("Choose a different password and try again.\n");
13323
+ password = await promptNewPasswordFn();
13324
+ continue;
13325
+ }
13326
+ if (code === EXPIRED_TOKEN || code === INVALID_SIGNUP_TOKEN) deletePendingCliSignup(configDir);
13327
+ writeErrorWithHints(payload);
13328
+ throw cliError("Primitive CLI signup failed while verifying the account.");
13329
+ }
13330
+ }
13331
+ }
13332
+ var SignupCommand = class SignupCommand extends Command {
13333
+ static description = "Create a Primitive account from the terminal, verify your email, and save an org-scoped CLI API key locally.";
13334
+ static summary = "Create an account and log in";
13335
+ static examples = [
13336
+ "<%= config.bin %> signup",
13337
+ "<%= config.bin %> signup --device-name work-laptop",
13338
+ "<%= config.bin %> signup --force"
13339
+ ];
13340
+ static flags = {
13341
+ "api-base-url-1": Flags.string({
13342
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
13343
+ env: "PRIMITIVE_API_BASE_URL_1",
13344
+ hidden: true
13345
+ }),
13346
+ "device-name": Flags.string({ description: "Device name used for the created CLI API key" }),
13347
+ force: Flags.boolean({
13348
+ char: "f",
13349
+ description: "Replace saved credentials without first verifying the existing login"
13350
+ })
13351
+ };
13352
+ async run() {
13353
+ const { flags } = await this.parse(SignupCommand);
13354
+ let releaseCredentialsLock;
13355
+ try {
13356
+ releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
13357
+ } catch (error) {
13358
+ throw cliError(error instanceof Error ? error.message : String(error));
13359
+ }
13360
+ try {
13361
+ await this.runWithCredentialLock(flags);
13362
+ } finally {
13363
+ releaseCredentialsLock();
13364
+ }
13365
+ }
13366
+ async runWithCredentialLock(flags) {
13367
+ await runSignupWithCredentialLock({
13368
+ configDir: this.config.configDir,
13369
+ flags
13370
+ });
13371
+ }
13372
+ };
13373
+ //#endregion
12338
13374
  //#region src/oclif/commands/whoami.ts
12339
13375
  var WhoamiCommand = class WhoamiCommand extends Command {
12340
13376
  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.`;
@@ -12477,7 +13513,7 @@ var ListOperationsCommand = class extends Command {
12477
13513
  }
12478
13514
  };
12479
13515
  function lookupOperation(id) {
12480
- const trimmed = id.trim();
13516
+ const trimmed = resolveOperationAlias(id.trim());
12481
13517
  const sep = trimmed.indexOf(":");
12482
13518
  const tag = sep === -1 ? "" : trimmed.slice(0, sep);
12483
13519
  const cmd = sep === -1 ? trimmed : trimmed.slice(sep + 1);
@@ -12493,7 +13529,7 @@ function lookupOperation(id) {
12493
13529
  }
12494
13530
  var DescribeCommand = class DescribeCommand extends Command {
12495
13531
  static args = { command: Args.string({
12496
- description: "Command id to describe, in `<topic>:<command>` form (e.g. `emails:get-email`). Run `primitive list-operations | jq -r '.[] | \"\\(.tagCommand):\\(.command)\"'` to enumerate.",
13532
+ 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.",
12497
13533
  required: true
12498
13534
  }) };
12499
13535
  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.
@@ -12501,15 +13537,15 @@ var DescribeCommand = class DescribeCommand extends Command {
12501
13537
  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:
12502
13538
 
12503
13539
  # Which of EmailDetail's sender-shaped fields is canonical?
12504
- primitive describe emails:get-email | jq '.responseSchema.properties | keys'
12505
- primitive describe emails:get-email | jq -r '.responseSchema.properties.from_email.description'
13540
+ primitive describe emails:get | jq '.responseSchema.properties | keys'
13541
+ primitive describe emails:get | jq -r '.responseSchema.properties.from_email.description'
12506
13542
 
12507
13543
  # What does each value of SentEmailStatus mean?
12508
- primitive describe sending:get-sent-email | jq -r '.responseSchema.properties.status.description'
13544
+ primitive describe sent:get | jq -r '.responseSchema.properties.status.description'
12509
13545
 
12510
13546
  \`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.`;
12511
13547
  static summary = "Describe a single API operation in detail";
12512
- static examples = ["<%= config.bin %> describe emails:get-email", "<%= config.bin %> describe sending:send-email"];
13548
+ static examples = ["<%= config.bin %> describe emails:get", "<%= config.bin %> describe sent:get"];
12513
13549
  async run() {
12514
13550
  const { args } = await this.parse(DescribeCommand);
12515
13551
  const { match, candidates } = lookupOperation(args.command);
@@ -12545,13 +13581,66 @@ var CompletionCommand = class CompletionCommand extends Command {
12545
13581
  function commandId(operation) {
12546
13582
  return `${operation.tagCommand}:${operation.command}`;
12547
13583
  }
13584
+ const CANONICAL_OPERATION_ALIASES = {
13585
+ "account:show": "account:get-account",
13586
+ "account:storage": "account:get-storage-stats",
13587
+ "account:webhook-secret": "account:get-webhook-secret",
13588
+ "deliveries:list": "webhook-deliveries:list-deliveries",
13589
+ "deliveries:replay": "webhook-deliveries:replay-delivery",
13590
+ "domains:add": "domains:add-domain",
13591
+ "domains:delete": "domains:delete-domain",
13592
+ "domains:list": "domains:list-domains",
13593
+ "domains:update": "domains:update-domain",
13594
+ "domains:verify": "domains:verify-domain",
13595
+ "emails:delete": "emails:delete-email",
13596
+ "emails:discard-content": "emails:discard-email-content",
13597
+ "emails:download-raw": "emails:download-raw-email",
13598
+ "emails:get": "emails:get-email",
13599
+ "emails:list": "emails:list-emails",
13600
+ "emails:replay-webhooks": "emails:replay-email-webhooks",
13601
+ "emails:search": "emails:search-emails",
13602
+ "endpoints:create": "endpoints:create-endpoint",
13603
+ "endpoints:delete": "endpoints:delete-endpoint",
13604
+ "endpoints:list": "endpoints:list-endpoints",
13605
+ "endpoints:test": "endpoints:test-endpoint",
13606
+ "endpoints:update": "endpoints:update-endpoint",
13607
+ "filters:create": "filters:create-filter",
13608
+ "filters:delete": "filters:delete-filter",
13609
+ "filters:list": "filters:list-filters",
13610
+ "filters:update": "filters:update-filter",
13611
+ "functions:delete": "functions:delete-function",
13612
+ "functions:delete-secret": "functions:delete-function-secret",
13613
+ "functions:get": "functions:get-function",
13614
+ "functions:list": "functions:list-functions",
13615
+ "functions:list-secrets": "functions:list-function-secrets",
13616
+ "functions:logs": "functions:list-function-logs",
13617
+ "sending:get": "sending:get-sent-email",
13618
+ "sending:list": "sending:list-sent-emails",
13619
+ "sending:permissions": "sending:get-send-permissions",
13620
+ "sending:reply": "sending:reply-to-email",
13621
+ "sending:send": "sending:send-email",
13622
+ "sent:get": "sending:get-sent-email",
13623
+ "sent:list": "sending:list-sent-emails",
13624
+ "webhook-deliveries:list": "webhook-deliveries:list-deliveries",
13625
+ "webhook-deliveries:replay": "webhook-deliveries:replay-delivery"
13626
+ };
13627
+ const DESCRIBE_OPERATION_ALIASES = {
13628
+ ...CANONICAL_OPERATION_ALIASES,
13629
+ reply: "sending:reply-to-email"
13630
+ };
13631
+ function resolveOperationAlias(id) {
13632
+ return DESCRIBE_OPERATION_ALIASES[id] ?? id;
13633
+ }
12548
13634
  const OVERRIDDEN_OPERATION_IDS = new Set(["functions:test-function"]);
13635
+ const generatedCommands = Object.fromEntries(operationManifest.filter((operation) => !OVERRIDDEN_OPERATION_IDS.has(commandId(operation))).map((operation) => [commandId(operation), createOperationCommand(operation)]));
12549
13636
  const COMMANDS = {
12550
13637
  completion: CompletionCommand,
12551
13638
  "list-operations": ListOperationsCommand,
12552
13639
  describe: DescribeCommand,
12553
13640
  send: SendCommand,
13641
+ reply: ReplyCommand,
12554
13642
  login: LoginCommand,
13643
+ signup: SignupCommand,
12555
13644
  logout: LogoutCommand,
12556
13645
  whoami: WhoamiCommand,
12557
13646
  doctor: DoctorCommand,
@@ -12562,8 +13651,14 @@ const COMMANDS = {
12562
13651
  "functions:deploy": FunctionsDeployCommand,
12563
13652
  "functions:redeploy": FunctionsRedeployCommand,
12564
13653
  "functions:set-secret": FunctionsSetSecretCommand,
13654
+ "functions:test": FunctionsTestFunctionCommand,
12565
13655
  "functions:test-function": FunctionsTestFunctionCommand,
12566
- ...Object.fromEntries(operationManifest.filter((operation) => !OVERRIDDEN_OPERATION_IDS.has(commandId(operation))).map((operation) => [commandId(operation), createOperationCommand(operation)]))
13656
+ ...Object.fromEntries(Object.entries(CANONICAL_OPERATION_ALIASES).map(([alias, target]) => {
13657
+ const command = generatedCommands[target];
13658
+ if (!command) throw new Error(`Missing generated command target for alias ${alias}`);
13659
+ return [alias, command];
13660
+ })),
13661
+ ...generatedCommands
12567
13662
  };
12568
13663
  //#endregion
12569
- export { COMMANDS, lookupOperation };
13664
+ export { CANONICAL_OPERATION_ALIASES, COMMANDS, lookupOperation };