atmn 2.0.35 → 2.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -52,6 +52,8 @@ var sandboxKeyName = ({
52
52
  sandboxId
53
53
  }) => `AUTUMN_SANDBOX_${sandboxId.toUpperCase().replace(NON_ALPHANUMERIC_RUN, "_")}_SECRET_KEY`;
54
54
  var SANDBOX_PIN_NAME = "AUTUMN_SANDBOX_ID";
55
+ var SANDBOX_KEY_NAME = /^AUTUMN_SANDBOX_[A-Z0-9_]+_SECRET_KEY$/;
56
+ var isSandboxKeyName = (name) => SANDBOX_KEY_NAME.test(name);
55
57
 
56
58
  // src/env/resolveTarget.ts
57
59
  var LOCAL_HOST = "http://localhost";
@@ -2742,6 +2744,12 @@ var API_ROUTES = [
2742
2744
  type: "json",
2743
2745
  required: false,
2744
2746
  description: "Filter events by time range"
2747
+ },
2748
+ {
2749
+ name: "range",
2750
+ type: "string",
2751
+ required: false,
2752
+ description: "Preset time range to filter events by, resolved the same way as in events.aggregate. Cannot be combined with custom_range"
2745
2753
  }
2746
2754
  ]
2747
2755
  },
@@ -2794,6 +2802,12 @@ var API_ROUTES = [
2794
2802
  required: false,
2795
2803
  description: "Custom time range to aggregate events for. If provided, range must not be provided"
2796
2804
  },
2805
+ {
2806
+ name: "timezone",
2807
+ type: "string",
2808
+ required: false,
2809
+ description: 'IANA timezone (e.g. "America/New_York") that day, week and month bins are aligned to. Hour bins are always UTC. Defaults to UTC'
2810
+ },
2797
2811
  {
2798
2812
  name: "filter_by",
2799
2813
  type: "json",
@@ -7283,7 +7297,7 @@ var createClient = (options) => ({
7283
7297
  });
7284
7298
 
7285
7299
  // src/version.ts
7286
- var version = "2.0.35";
7300
+ var version = "2.0.37";
7287
7301
 
7288
7302
  // src/http/autumnFetch.ts
7289
7303
  var runtime = typeof Bun !== "undefined" ? `bun ${Bun.version}` : `node ${process.versions.node}`;
@@ -9538,16 +9552,16 @@ import { dirname as dirname4, join as join7 } from "node:path";
9538
9552
  import chalk3 from "chalk";
9539
9553
 
9540
9554
  // src/generated/skills.ts
9541
- var SKILLS_VERSION = "2.0.35";
9555
+ var SKILLS_VERSION = "2.0.37";
9542
9556
  var SKILLS = [
9543
9557
  {
9544
9558
  name: "autumn-setup",
9545
9559
  description: "First-time Autumn setup — install the atmn CLI, connect to an org (sign in, or keyless with no account), turn the user's pricing into autumn.config.ts, and push it to a sandbox org. Use when the user is new to Autumn, pastes an Autumn setup prompt, or asks to set up Autumn, add billing, monetization, subscriptions, usage-based billing, pricing, or plans. If Autumn is already set up, use autumn-catalog instead.",
9546
- version: "2.0.35",
9560
+ version: "2.0.37",
9547
9561
  markdown: `---
9548
9562
  name: autumn-setup
9549
9563
  description: First-time Autumn setup — install the atmn CLI, connect to an org (sign in, or keyless with no account), turn the user's pricing into autumn.config.ts, and push it to a sandbox org. Use when the user is new to Autumn, pastes an Autumn setup prompt, or asks to set up Autumn, add billing, monetization, subscriptions, usage-based billing, pricing, or plans. If Autumn is already set up, use autumn-catalog instead.
9550
- version: 2.0.35
9564
+ version: 2.0.37
9551
9565
  ---
9552
9566
 
9553
9567
  # Setup
@@ -9705,11 +9719,11 @@ Then stop. Don't keep building, don't tour the dashboard, don't deploy anything.
9705
9719
  {
9706
9720
  name: "autumn-catalog",
9707
9721
  description: "Modeling a user's pricing into an Autumn catalog — deciding the structure (plans, variants, add-ons, licenses, credit systems, pooled balances) before writing config, then filling in the numbers. Use when the user describes their pricing or asks to model, change, or push a catalog.",
9708
- version: "2.0.35",
9722
+ version: "2.0.37",
9709
9723
  markdown: `---
9710
9724
  name: autumn-catalog
9711
9725
  description: Modeling a user's pricing into an Autumn catalog — deciding the structure (plans, variants, add-ons, licenses, credit systems, pooled balances) before writing config, then filling in the numbers. Use when the user describes their pricing or asks to model, change, or push a catalog.
9712
- version: 2.0.35
9726
+ version: 2.0.37
9713
9727
  ---
9714
9728
 
9715
9729
  # Catalog
@@ -13576,7 +13590,7 @@ Pitch: "Pro $20/mo or $200/yr — 1,000 messages per month either way."
13576
13590
  },
13577
13591
  {
13578
13592
  path: "references/atmn.md",
13579
- contents: "# atmn catalog flows\n\nContents: commands · when to use it · the config is the catalog's state (versions, renames, drafts) · variants and licenses · config shapes · splitting the config · update loop · pull · webhooks · sandboxes and keys · what to show the user.\n\nUse `atmn` when a project has or should have an `autumn.config.ts` source of truth.\n\nCommands — these two, not `atmn preview` (that does not exist):\n\n```sh\natmn push # lints, then previews the diff; applies nothing\natmn push --yes # apply exactly what the preview showed\n```\n\n`push` without `--yes` is always a dry run, on a clean org too, and it never asks anything: `--yes` is the only gate. The commands that do ask (`init`, `login --keyless`, `sandbox use`, `skills install`) print the flag to pass instead whenever there is no TTY, which is every agent run; `--headless` forces that in a terminal.\n\n## When to use it\n\n- New project: run `atmn init`. It connects (sign in, or keyless), places the config, pulls whatever the org already holds, and installs these skills beside it.\n- Existing project: if `autumn.config.ts` exists, run `atmn pull` first so every row carries its server ids, then edit and push. Never rewrite a config that predates you from scratch.\n- Use MCP/API directly when the user wants dashboard/API-first changes or there is no local config workflow.\n\n## The config is the catalog's state\n\nThe document is the whole desired catalog for every collection it states. A plan or feature missing from a stated `plans` / `features` is a deletion (archived when customers depend on it). A collection left out entirely is not managed. That rule reaches down to versions: **a version row missing from `plans` is deleted too**.\n\nEvery version of a plan is a row in `plans`. Rows of one plan share a `planId`; each names its version with `versionSlug`; exactly one is `active: true` and the rest `active: false`. There is no history collection and no timeline — array order means nothing. What a version *is* (a group of customers, not a step in time) is the `autumn-concepts` skill's: read `references/plan.md` in the `autumn-concepts` skill. This file owns how rows express it.\n\nTwo fields make a row addressable:\n\n- `planId` + `versionSlug` — what you write. The pair names exactly one version. `versionSlug` is required on every plan row and every variant row, including a plan's only version; an omitted slug is a lint error, never an implicit `v1`.\n- `internalId` — the server's stable id. It is minted on the first `push --yes` and written back into the fixture; `pull` writes it for every row that lacks one. A fixture carrying it can be renamed (`planId`, `featureId`, `versionSlug`) and the server treats that as a rename, not a delete plus create.\n\n**This is where atmn differs from the API and the dashboard.** Those send one plan entry plus flags — `versioning`, `active`, `propagate` — as `references/catalog-update.md` describes. A config has none of those flags. The intent is read off the rows — which rows exist, which one is active, which carry an `internalId` — and the preview reports what the server derived. If the preview says something other than what you meant, change the rows and preview again; never look for a flag.\n\n| You want | Row edit | Preview shows |\n|---|---|---|\n| Change a version in place (active or history) | edit that row; keep its `versionSlug` and `internalId` | `~ pro@v1`, a migration for its customers |\n| The same change on every version | make the edit on every row of the plan | one `~` per row |\n| A new version; customers stay on the old one | add a row: same `planId`, new `versionSlug`, `active: true`, **no `internalId`**; flip the old row to `active: false` | `+ pro@v2`, and `active: true -> false` on the old row |\n| A draft nobody can buy yet | add the row with `active: false` beside the active one | `+` with no pointer move |\n| Rename the plan id | change `planId` on **every** row of the plan (all carry `internalId`) | a rename, not `-` + `+` |\n| Rename a version | change `versionSlug` on a row that carries `internalId` | `Version slug` rename |\n| Retire a version | remove its row | `-`; refused while customers are on it (it names both exits: migrate them, or archive the whole plan) |\n\nGuards the lint applies before anything is sent: a plan with no active row or two active rows (`mark the one customers can buy active: true and the rest active: false`), a missing `versionSlug`, the same `planId` + `versionSlug` twice. A plan whose only row is `active: false` is refused — a draft needs an active sibling.\n\nNumbers vs slugs: the server numbers versions in creation order and the preview labels rows with that number (`pro@v2`). The config never states numbers, only slugs. Push a `v1` row after `v2` already exists and the server numbers it higher; the slug still says `v1`. Never read a preview number back as the slug.\n\n## Variants and licenses\n\nWhat a variant's customize can change, when a base edit reaches a variant, and how a license link anchors to a child version are catalog-wide: `references/catalog-update.md`. What is atmn's:\n\n- A variant is an entry in its base row's `variants` array and a license link an entry in the parent row's `licenses` array: `variant({...})` and `license({...})` fixtures, inline or imported from their own files, edited in place there. Pull writes new ones in that form. Every entry the config lists is a declared overlay; there is no `propagate` in a config, so a base edit reaches a variant through the entry you write, not a follow flag.\n- Minting a base version means listing the variant entries again under the new base row, each with the new `versionSlug` and no `internalId`. The old entries stay under the old base row. One variant version cannot serve two base rows; the lint names both rows and says to version and relink the variant.\n- To retire a variant, set `archived: true` on its entry. A variant left out of the array is a deletion, refused while customers hold it.\n- Every `license({...})` states `versionSlug` — the lint refuses one without it — because the link is pinned to that child version and a config that names it links the same version in every environment. Pull writes it back. Minting a child version moves no parent; relinking a parent is editing that slug.\n\n## Config shapes\n\n`autumn.config.ts` uses the atmn package types, not raw API JSON. Field names are camelCase: `featureId`, `planId`, `billingMethod`, `billingUnits`, `freeTrial`, `intervalCount`, `versionSlug`. Follow the exported types from the package when editing config. Amounts are plain dollars: $20 is `20`, never `2000`.\n\nBuilders — `feature`, `plan`, `variant`, `license`. Items are plain objects on the plan; there is no `item()` builder and fixtures expose `featureId` / `planId`, never `.id`. A plan with an active `v2`, its `v1` kept for the customers still on it, each with an annual variant:\n\n```ts\nimport { atmn, feature, plan, variant } from \"atmn\";\n\nexport const messages = feature({\n featureId: \"messages\",\n name: \"Messages\",\n type: \"metered\",\n consumable: true,\n});\n\nexport const proAnnualV2 = variant({\n variantPlanId: \"pro_annual\",\n versionSlug: \"v2\",\n name: \"Pro Annual\",\n customize: { price: { amount: 250, interval: \"year\" } },\n});\n\nexport const proAnnualV1 = variant({\n variantPlanId: \"pro_annual\",\n versionSlug: \"v1\",\n name: \"Pro Annual\",\n customize: { price: { amount: 200, interval: \"year\" } },\n});\n\nexport const proV2 = plan({\n planId: \"pro\",\n versionSlug: \"v2\",\n active: true,\n name: \"Pro\",\n price: { amount: 25, interval: \"month\" },\n items: [{ featureId: messages.featureId, included: 10000, reset: { interval: \"month\" } }],\n variants: [proAnnualV2],\n});\n\nexport const proV1 = plan({\n internalId: \"prod_…\", // written back by push --yes / pull\n planId: \"pro\",\n versionSlug: \"v1\",\n active: false,\n name: \"Pro\",\n price: { amount: 20, interval: \"month\" },\n items: [{ featureId: messages.featureId, included: 5000, reset: { interval: \"month\" } }],\n variants: [proAnnualV1],\n});\n\nexport default atmn({ features: [messages], plans: [proV2, proV1] });\n```\n\nBefore `proV2` existed, `proV1` was the active row; minting `v2` was adding `proV2` without an `internalId` and flipping `proV1` to `active: false`.\n\nUsage-priced item:\n\n```ts\n{\n featureId: messages.featureId,\n included: 10000,\n reset: { interval: \"month\" },\n price: {\n amount: 0.9,\n billingMethod: \"usage_based\",\n billingUnits: 1000,\n interval: \"month\",\n },\n}\n```\n\n## Splitting the config\n\n`atmn init` scaffolds four files: `autumn.config.ts` (the root, `export default atmn({...})`), `features.ts`, `plans.ts`, `rewards.ts`. Every version of every plan lives in the `plans` array — active and history rows side by side. The config is ordinary TypeScript, so a row can be lifted into its own file under any export name and referenced from the array; `pull` follows imports and edits each fixture where it lives, and appends rows it has to add to the imported array. What it cannot do is edit a fixture that is not a plain literal — a spread, a helper call, a `.map()` — and it says so and writes nothing rather than guess.\n\n## Update loop\n\n1. Inspect or create `autumn.config.ts`.\n2. Edit the config to represent the desired catalog.\n3. Run `atmn push` to preview changes.\n4. Show the user the plan diffs, the customer impact, which plans mint a new version, and the draft migrations it would create. If the versioning is not what they meant, change the rows (table above) and preview again.\n5. Rerun `atmn push --yes` to apply the same preview.\n6. Report created/updated/deleted/archived features and plans, and the draft migrations the output lists. `push --yes` also writes `internalId` (and any `versionSlug` the server assigned) back into the fixtures — say so; those edits are expected.\n\nTwo notes push prints that are worth relaying: a plan removed while an id-less plan appears looks like a rename — pull first so the fixture carries its id; and a config still stating a deprecated field (`entityFeatureId`) gets a note naming the replacement.\n\n## Pull\n\n`atmn pull` writes the server's catalog back into the config in place: it flips `active` where the dashboard promoted a version, appends versions the config never mentioned, and backfills `internalId` and `versionSlug`. Run it after anyone touches the dashboard, and before editing a config you did not write. With no config yet, `pull` asks which folder to create it in; headless, it prints the `-c <dir>` hint and stops, so run `atmn init` or pass `-c` instead.\n\n`atmn pull --overwrite` is different: it rewrites `autumn.config.ts` and the `features.ts`, `plans.ts` and `rewards.ts` beside it from the server. It never deletes a file, and it leaves alone any file that does not import the package. It needs `--yes`, and it is the right move only when the config describes a different org than the key — the tell is `Your config no longer matches this org's catalog`. Anywhere else, a plain `pull` is what you want.\n\n## Webhooks\n\n`webhooks` in the config lists the Autumn webhooks it manages, keyed by `id`. Unlike the catalog it is never a deletion list: `push` creates or updates the webhooks it states and leaves every other webhook alone (the preview shows them as `unmanaged`).\n\n`url` is a map keyed by environment, never one string. A missing key means \"don't register this webhook there\", which is not an error:\n\n```ts\nimport { atmn, webhook } from \"atmn\";\n\nexport default atmn({\n webhooks: [\n webhook({\n id: \"billing\",\n events: [\"billing.updated\", \"balances.limit_reached\"],\n url: {\n live: \"https://myapp.com/api/autumn\", // atmn push -p\n sandbox: \"https://staging.myapp.com/api/autumn\", // the default sandbox\n \"qa-team\": \"https://qa.myapp.com/api/autumn\", // atmn sandbox use qa-team\n },\n }),\n ],\n});\n```\n\n- Keys: `live` for `-p`, `sandbox` for the default sandbox, or a named sandbox's slug (its name lowercased; no spaces). Never copy a sandbox URL into `live`: ask the user for the production URL, or leave `live` out.\n- Every URL must be https and publicly reachable; localhost and private networks are refused, tunnels such as ngrok work. `events` needs at least one entry.\n- `id` is permanent: changing it registers a new webhook. Two ids that differ only by case or `-`/`_` are refused.\n- The preview gives a changed URL its own line (`billing url: old → new`). Read it out to the user before `--yes`.\n- `push --yes` writes each new webhook's signing secret once and prints exactly where: `AUTUMN_WEBHOOK_<ID>_SECRET` in `.env.prod` for `-p`, `AUTUMN_WEBHOOK_<ID>_<ORG4>_SECRET` in `.env.local` (or `.env`) for a sandbox. Relay the variable name and file; never print or read the secret.\n- `pull` edits only the target env's key: it adds webhooks it finds, sets a changed URL, removes the env's key when the server no longer has that webhook, and deletes a webhook whose map ends up empty. A URL written as code (`process.env.X`) is never rewritten; pull warns when it differs from the server. Webhooks already in the dashboard will be pulled and be editable in the config once you pull as usual. Since the dashboard doesn't let you set an ID, a permanent ID will be assigned to this for future reference.\n\n## Sandboxes and keys\n\n- `atmn sandbox use <name>` pins a named sandbox; every command after it targets that sandbox until `atmn sandbox use --clear`.\n- `atmn reset --yes` empties the pinned sandbox; `atmn push --yes` rebuilds it from the config.\n- `atmn env --json` says which org, sandbox and key a command would hit, with `notes` on what to do when something is off.\n- A missing key fails fast and names the fix: `AUTUMN_SECRET_KEY is not set. Run atmn login, or atmn login --keyless if you don't have an account.` — hand that to the setup flow, don't ask the user to paste a key.\n\n## What to show the user\n\n- Which plans mint a new version, and the draft migrations that come with them.\n- Feature/plan/version deletions that will archive instead because dependencies or customers exist.\n- Which sandbox is pinned before applying anything."
13593
+ contents: "# atmn catalog flows\n\nContents: commands · when to use it · the config is the catalog's state (versions, renames, drafts) · variants and licenses · config shapes · splitting the config · update loop · pull · webhooks · sandboxes and keys · what to show the user.\n\nUse `atmn` when a project has or should have an `autumn.config.ts` source of truth.\n\nCommands — these two, not `atmn preview` (that does not exist):\n\n```sh\natmn push # lints, then previews the diff; applies nothing\natmn push --yes # apply exactly what the preview showed\n```\n\n`push` without `--yes` is always a dry run, on a clean org too, and it never asks anything: `--yes` is the only gate. The commands that do ask (`init`, `login --keyless`, `sandbox use`, `skills install`) print the flag to pass instead whenever there is no TTY, which is every agent run; `--headless` forces that in a terminal.\n\n## When to use it\n\n- New project: run `atmn init`. It connects (sign in, or keyless), places the config, pulls whatever the org already holds, and installs these skills beside it.\n- Existing project: if `autumn.config.ts` exists, run `atmn pull` first so every row carries its server ids, then edit and push. Never rewrite a config that predates you from scratch.\n- Use MCP/API directly when the user wants dashboard/API-first changes or there is no local config workflow.\n\n## The config is the catalog's state\n\nThe document is the whole desired catalog for every collection it states. A plan or feature missing from a stated `plans` / `features` is a deletion (archived when customers depend on it). A collection left out entirely is not managed. That rule reaches down to versions: **a version row missing from `plans` is deleted too**.\n\nEvery version of a plan is a row in `plans`. Rows of one plan share a `planId`; each names its version with `versionSlug`; exactly one is `active: true` and the rest `active: false`. There is no history collection and no timeline — array order means nothing. What a version *is* (a group of customers, not a step in time) is the `autumn-concepts` skill's: read `references/plan.md` in the `autumn-concepts` skill. This file owns how rows express it.\n\nTwo fields make a row addressable:\n\n- `planId` + `versionSlug` — what you write. The pair names exactly one version. `versionSlug` is required on every plan row and every variant row, including a plan's only version; an omitted slug is a lint error, never an implicit `v1`.\n- `internalId` — the server's stable id. It is minted on the first `push --yes` and written back into the fixture; `pull` writes it for every row that lacks one. A fixture carrying it can be renamed (`planId`, `featureId`, `versionSlug`) and the server treats that as a rename, not a delete plus create.\n\n**This is where atmn differs from the API and the dashboard.** Those send one plan entry plus flags — `versioning`, `active`, `propagate` — as `references/catalog-update.md` describes. A config has none of those flags. The intent is read off the rows — which rows exist, which one is active, which carry an `internalId` — and the preview reports what the server derived. If the preview says something other than what you meant, change the rows and preview again; never look for a flag.\n\n| You want | Row edit | Preview shows |\n|---|---|---|\n| Change a version in place (active or history) | edit that row; keep its `versionSlug` and `internalId` | `~ pro@v1`, a migration for its customers |\n| The same change on every version | make the edit on every row of the plan | one `~` per row |\n| A new version; customers stay on the old one | add a row: same `planId`, new `versionSlug`, `active: true`, **no `internalId`**; flip the old row to `active: false` | `+ pro@v2`, and `active: true -> false` on the old row |\n| A draft nobody can buy yet | add the row with `active: false` beside the active one | `+` with no pointer move |\n| Rename the plan id | change `planId` on **every** row of the plan (all carry `internalId`) | a rename, not `-` + `+` |\n| Rename a version | change `versionSlug` on a row that carries `internalId` | `Version slug` rename |\n| Retire a version | remove its row | `-`; refused while customers are on it (it names both exits: migrate them, or archive the whole plan) |\n\nGuards the lint applies before anything is sent: a plan with no active row or two active rows (`mark the one customers can buy active: true and the rest active: false`), a missing `versionSlug`, the same `planId` + `versionSlug` twice. A plan whose only row is `active: false` is refused — a draft needs an active sibling.\n\nNumbers vs slugs: the server numbers versions in creation order and the preview labels rows with that number (`pro@v2`). The config never states numbers, only slugs. Push a `v1` row after `v2` already exists and the server numbers it higher; the slug still says `v1`. Never read a preview number back as the slug.\n\n## Variants and licenses\n\nWhat a variant's customize can change, when a base edit reaches a variant, and how a license link anchors to a child version are catalog-wide: `references/catalog-update.md`. What is atmn's:\n\n- A variant is an entry in its base row's `variants` array and a license link an entry in the parent row's `licenses` array: `variant({...})` and `license({...})` fixtures, inline or imported from their own files, edited in place there. Pull writes new ones in that form. Every entry the config lists is a declared overlay; there is no `propagate` in a config, so a base edit reaches a variant through the entry you write, not a follow flag.\n- Minting a base version means listing the variant entries again under the new base row, each with the new `versionSlug` and no `internalId`. The old entries stay under the old base row. One variant version cannot serve two base rows; the lint names both rows and says to version and relink the variant.\n- To retire a variant, set `archived: true` on its entry. A variant left out of the array is a deletion, refused while customers hold it.\n- Every `license({...})` states `versionSlug` — the lint refuses one without it — because the link is pinned to that child version and a config that names it links the same version in every environment. Pull writes it back. Minting a child version moves no parent; relinking a parent is editing that slug.\n\n## Config shapes\n\n`autumn.config.ts` uses the atmn package types, not raw API JSON. Field names are camelCase: `featureId`, `planId`, `billingMethod`, `billingUnits`, `freeTrial`, `intervalCount`, `versionSlug`. Follow the exported types from the package when editing config. Amounts are plain dollars: $20 is `20`, never `2000`.\n\nBuilders — `feature`, `plan`, `variant`, `license`. Items are plain objects on the plan; there is no `item()` builder and fixtures expose `featureId` / `planId`, never `.id`. A plan with an active `v2`, its `v1` kept for the customers still on it, each with an annual variant:\n\n```ts\nimport { atmn, feature, plan, variant } from \"atmn\";\n\nexport const messages = feature({\n featureId: \"messages\",\n name: \"Messages\",\n type: \"metered\",\n consumable: true,\n});\n\nexport const proAnnualV2 = variant({\n variantPlanId: \"pro_annual\",\n versionSlug: \"v2\",\n name: \"Pro Annual\",\n customize: { price: { amount: 250, interval: \"year\" } },\n});\n\nexport const proAnnualV1 = variant({\n variantPlanId: \"pro_annual\",\n versionSlug: \"v1\",\n name: \"Pro Annual\",\n customize: { price: { amount: 200, interval: \"year\" } },\n});\n\nexport const proV2 = plan({\n planId: \"pro\",\n versionSlug: \"v2\",\n active: true,\n name: \"Pro\",\n price: { amount: 25, interval: \"month\" },\n items: [{ featureId: messages.featureId, included: 10000, reset: { interval: \"month\" } }],\n variants: [proAnnualV2],\n});\n\nexport const proV1 = plan({\n internalId: \"prod_…\", // written back by push --yes / pull\n planId: \"pro\",\n versionSlug: \"v1\",\n active: false,\n name: \"Pro\",\n price: { amount: 20, interval: \"month\" },\n items: [{ featureId: messages.featureId, included: 5000, reset: { interval: \"month\" } }],\n variants: [proAnnualV1],\n});\n\nexport default atmn({ features: [messages], plans: [proV2, proV1] });\n```\n\nBefore `proV2` existed, `proV1` was the active row; minting `v2` was adding `proV2` without an `internalId` and flipping `proV1` to `active: false`.\n\nUsage-priced item:\n\n```ts\n{\n featureId: messages.featureId,\n included: 10000,\n reset: { interval: \"month\" },\n price: {\n amount: 0.9,\n billingMethod: \"usage_based\",\n billingUnits: 1000,\n interval: \"month\",\n },\n}\n```\n\n## Splitting the config\n\n`atmn init` scaffolds four files: `autumn.config.ts` (the root, `export default atmn({...})`), `features.ts`, `plans.ts`, `rewards.ts`. Every version of every plan lives in the `plans` array — active and history rows side by side. The config is ordinary TypeScript, so a row can be lifted into its own file under any export name and referenced from the array; `pull` follows imports and edits each fixture where it lives, and appends rows it has to add to the imported array. What it cannot do is edit a fixture that is not a plain literal — a spread, a helper call, a `.map()` — and it says so and writes nothing rather than guess.\n\n## Update loop\n\n1. Inspect or create `autumn.config.ts`.\n2. Edit the config to represent the desired catalog.\n3. Run `atmn push` to preview changes.\n4. Show the user the plan diffs, the customer impact, which plans mint a new version, and the draft migrations it would create. If the versioning is not what they meant, change the rows (table above) and preview again.\n5. Rerun `atmn push --yes` to apply the same preview.\n6. Report created/updated/deleted/archived features and plans, and the draft migrations the output lists. `push --yes` also writes `internalId` (and any `versionSlug` the server assigned) back into the fixtures — say so; those edits are expected.\n\nTwo notes push prints that are worth relaying: a plan removed while an id-less plan appears looks like a rename — pull first so the fixture carries its id; and a config still stating a deprecated field (`entityFeatureId`) gets a note naming the replacement.\n\n## Pull\n\n`atmn pull` writes the server's catalog back into the config in place: it flips `active` where the dashboard promoted a version, appends versions the config never mentioned, and backfills `internalId` and `versionSlug`. Run it after anyone touches the dashboard, and before editing a config you did not write. With no config yet, `pull` asks which folder to create it in; headless, it prints the `-c <dir>` hint and stops, so run `atmn init` or pass `-c` instead.\n\n`atmn pull --overwrite` is different: it rewrites `autumn.config.ts` and the `features.ts`, `plans.ts` and `rewards.ts` beside it from the server. It never deletes a file, and it leaves alone any file that does not import the package. It needs `--yes`, and it is the right move only when the config describes a different org than the key — the tell is `Your config no longer matches this org's catalog`. Anywhere else, a plain `pull` is what you want.\n\n## Webhooks\n\n`webhooks` in the config lists the Autumn webhooks it manages, keyed by `id`. Unlike the catalog it is never a deletion list: `push` creates or updates the webhooks it states and leaves every other webhook alone (the preview shows them as `unmanaged`).\n\n`url` is a map keyed by environment, never one string. A missing key means \"don't register this webhook there\", which is not an error:\n\n```ts\nimport { atmn, webhook } from \"atmn\";\n\nexport default atmn({\n webhooks: [\n webhook({\n id: \"billing\",\n events: [\"billing.updated\", \"balances.limit_reached\"],\n url: {\n live: \"https://myapp.com/api/autumn\", // atmn push -p\n sandbox: \"https://staging.myapp.com/api/autumn\", // the default sandbox\n \"qa-team\": \"https://qa.myapp.com/api/autumn\", // atmn sandbox use qa-team\n },\n }),\n ],\n});\n```\n\n- Keys: `live` for `-p`, `sandbox` for the default sandbox, or a named sandbox's slug (its name lowercased; no spaces). Never copy a sandbox URL into `live`: ask the user for the production URL, or leave `live` out.\n- Every URL must be https and publicly reachable; localhost and private networks are refused, tunnels such as ngrok work. `events` needs at least one entry.\n- `id` is permanent: changing it registers a new webhook. Two ids that differ only by case or `-`/`_` are refused.\n- The preview gives a changed URL its own line (`billing url: old → new`). Read it out to the user before `--yes`.\n- `push --yes` writes each new webhook's signing secret once and prints exactly where: `AUTUMN_WEBHOOK_<ID>_SECRET` in `.env.prod` for `-p`, `AUTUMN_WEBHOOK_<ID>_<ORG4>_SECRET` in `.env.local` (or `.env`) for a sandbox. Relay the variable name and file; never print or read the secret.\n- `pull` reads webhooks from every env with a key in your env files (`AUTUMN_SECRET_KEY` → `sandbox`, `AUTUMN_PROD_SECRET_KEY` → `live`, each `AUTUMN_SANDBOX_<id>_SECRET_KEY` → its slug), whatever the target; a key that's rejected or belongs to another org skips its env with a warning and leaves that env's keys alone. Per env it adds webhooks it finds, sets a changed URL, removes the env's key when the server no longer has that webhook, and deletes a webhook whose map ends up empty. A URL written as code (`process.env.X`) is never rewritten; pull warns when it differs from the server. Webhooks already in the dashboard will be pulled and be editable in the config once you pull as usual. Since the dashboard doesn't let you set an ID, a permanent ID will be assigned to this for future reference.\n\n## Sandboxes and keys\n\n- `atmn sandbox use <name>` pins a named sandbox; every command after it targets that sandbox until `atmn sandbox use --clear`.\n- `atmn reset --yes` empties the pinned sandbox; `atmn push --yes` rebuilds it from the config.\n- `atmn env --json` says which org, sandbox and key a command would hit, with `notes` on what to do when something is off.\n- A missing key fails fast and names the fix: `AUTUMN_SECRET_KEY is not set. Run atmn login, or atmn login --keyless if you don't have an account.` — hand that to the setup flow, don't ask the user to paste a key.\n\n## What to show the user\n\n- Which plans mint a new version, and the draft migrations that come with them.\n- Feature/plan/version deletions that will archive instead because dependencies or customers exist.\n- Which sandbox is pinned before applying anything."
13580
13594
  },
13581
13595
  {
13582
13596
  path: "references/catalog-update.md",
@@ -13591,11 +13605,11 @@ Pitch: "Pro $20/mo or $200/yr — 1,000 messages per month either way."
13591
13605
  {
13592
13606
  name: "autumn-integrate",
13593
13607
  description: "Integrating Autumn into an app's backend — creating customers, billing flows (attach, checkout, upgrade, cancel), gating features with check, recording usage with track, and exposing billing data to the frontend. Use when the user asks to integrate Autumn, add billing/payments to their app, gate features, meter usage, or build checkout/upgrade flows. Requires a pushed catalog; use autumn-catalog first if plans don't exist yet.",
13594
- version: "2.0.35",
13608
+ version: "2.0.37",
13595
13609
  markdown: `---
13596
13610
  name: autumn-integrate
13597
13611
  description: Integrating Autumn into an app's backend — creating customers, billing flows (attach, checkout, upgrade, cancel), gating features with check, recording usage with track, and exposing billing data to the frontend. Use when the user asks to integrate Autumn, add billing/payments to their app, gate features, meter usage, or build checkout/upgrade flows. Requires a pushed catalog; use autumn-catalog first if plans don't exist yet.
13598
- version: 2.0.35
13612
+ version: 2.0.37
13599
13613
  ---
13600
13614
 
13601
13615
  # Integrate
@@ -18197,11 +18211,11 @@ Monitor your logs for this message. If you see it, check [status.useautumn.com](
18197
18211
  {
18198
18212
  name: "autumn-concepts",
18199
18213
  description: "Understanding Autumn's billing objects before acting on them — what a plan version is and what active means, when a trial starts or ends billing, how entity- and seat-scoped subscriptions differ from the customer's own, what a customize diff actually changes, and how caps, overage, and top-ups behave. Load before versioning, editing, or drafting a plan, or when a customer's state does not match the plain reading of their plan.",
18200
- version: "2.0.35",
18214
+ version: "2.0.37",
18201
18215
  markdown: `---
18202
18216
  name: autumn-concepts
18203
18217
  description: Understanding Autumn's billing objects before acting on them — what a plan version is and what active means, when a trial starts or ends billing, how entity- and seat-scoped subscriptions differ from the customer's own, what a customize diff actually changes, and how caps, overage, and top-ups behave. Load before versioning, editing, or drafting a plan, or when a customer's state does not match the plain reading of their plan.
18204
- version: 2.0.35
18218
+ version: 2.0.37
18205
18219
  ---
18206
18220
 
18207
18221
  # Concepts
@@ -22778,6 +22792,18 @@ var removeWebhookEnv = ({
22778
22792
 
22779
22793
  // src/actions/pull/webhooks/applyWebhooksPull.ts
22780
22794
  var isVercelWebhook = (events) => Array.isArray(events) && events.length > 0 && events.every((event) => typeof event === "string" && event.startsWith("vercel."));
22795
+ var statedFrom = ({
22796
+ row,
22797
+ webhook,
22798
+ envKey
22799
+ }) => ({
22800
+ ...row,
22801
+ id: webhook.id,
22802
+ url: { ...row?.url, [envKey]: webhook.url },
22803
+ events: webhook.events.length > 0 ? webhook.events : row?.events,
22804
+ description: webhook.description,
22805
+ disabled: webhook.disabled
22806
+ });
22781
22807
  var merge = (target, source) => {
22782
22808
  target.lines.push(...source.lines);
22783
22809
  target.warnings.push(...source.warnings);
@@ -22792,13 +22818,20 @@ var applyWebhooksPull = ({
22792
22818
  }) => {
22793
22819
  const result = { lines: [], warnings: [], unlocated: [] };
22794
22820
  const statedById = new Map((stated2 ?? []).map((row) => [row.id, row]));
22821
+ const next = new Map(statedById);
22795
22822
  const fromDashboard = (webhook) => !statedById.has(webhook.id) && isDashboardWebhook(webhook);
22796
22823
  const present = new Set;
22797
22824
  for (const webhook of remote) {
22798
22825
  if (fromDashboard(webhook)) {
22799
- const represented = (stated2 ?? []).find((row2) => row2.url?.[envKey] === webhook.url && isVercelWebhook(row2.events) === isVercelWebhook(webhook.events));
22826
+ const sameApp = (row2) => isVercelWebhook(row2.events) === isVercelWebhook(webhook.events);
22827
+ const represented = (stated2 ?? []).find((row2) => row2.url?.[envKey] === webhook.url && sameApp(row2)) ?? (stated2 ?? []).find((row2) => row2.url?.[envKey] === undefined && Object.values(row2.url ?? {}).includes(webhook.url) && sameApp(row2));
22800
22828
  if (represented !== undefined) {
22801
22829
  present.add(represented.id);
22830
+ next.set(represented.id, statedFrom({
22831
+ row: represented,
22832
+ webhook: { ...webhook, id: represented.id },
22833
+ envKey
22834
+ }));
22802
22835
  merge(result, updateWebhook({
22803
22836
  pull,
22804
22837
  webhook: { ...webhook, id: represented.id },
@@ -22817,14 +22850,16 @@ var applyWebhooksPull = ({
22817
22850
  webhook: { ...webhook, id },
22818
22851
  envKey
22819
22852
  });
22820
- if (failure2 === null)
22853
+ if (failure2 === null) {
22854
+ next.set(id, statedFrom({ row: undefined, webhook: { ...webhook, id }, envKey }));
22821
22855
  result.lines.push(`+ webhook ${id} (made in the dashboard; push adopts it by URL)`);
22822
- else
22856
+ } else
22823
22857
  result.unlocated.push({ id, action: failure2 });
22824
22858
  continue;
22825
22859
  }
22826
22860
  present.add(webhook.id);
22827
22861
  const row = statedById.get(webhook.id);
22862
+ next.set(webhook.id, statedFrom({ row, webhook, envKey }));
22828
22863
  if (row !== undefined) {
22829
22864
  merge(result, updateWebhook({ pull, webhook, stated: row, envKey }));
22830
22865
  continue;
@@ -22838,9 +22873,114 @@ var applyWebhooksPull = ({
22838
22873
  for (const row of stated2 ?? []) {
22839
22874
  if (present.has(row.id) || row.url?.[envKey] === undefined)
22840
22875
  continue;
22841
- merge(result, removeWebhookEnv({ pull, stated: row, envKey }));
22876
+ const removed = removeWebhookEnv({ pull, stated: row, envKey });
22877
+ merge(result, removed);
22878
+ if (removed.lines.length === 0)
22879
+ continue;
22880
+ const { [envKey]: _removed, ...url } = row.url;
22881
+ if (Object.keys(url).length === 0)
22882
+ next.delete(row.id);
22883
+ else
22884
+ next.set(row.id, { ...row, url });
22842
22885
  }
22843
- return result;
22886
+ return { ...result, stated: [...next.values()] };
22887
+ };
22888
+
22889
+ // src/generated/sandboxName.ts
22890
+ var RESERVED_SANDBOX_SLUGS = new Set([
22891
+ "products",
22892
+ "customers",
22893
+ "features",
22894
+ "migrations",
22895
+ "dev",
22896
+ "analytics",
22897
+ "settings",
22898
+ "admin",
22899
+ "chat",
22900
+ "quickstart",
22901
+ "onboarding",
22902
+ "impersonate-redirect",
22903
+ "trmnl"
22904
+ ]);
22905
+ var ENV_SANDBOX_SLUGS = new Set(["live", "sandbox"]);
22906
+ var sandboxSlug = (name) => name.toLowerCase().replace(/ /g, "-").replace(/[^\w\s-]/g, "");
22907
+
22908
+ // src/actions/pull/webhooks/webhookPullEnvs.ts
22909
+ class ForeignOrgKeyError extends Error {
22910
+ }
22911
+ var mainOrgIdOf = (info) => info.is_sandbox && info.created_by ? info.created_by : info.id;
22912
+ var webhookPullEnvs = ({
22913
+ env = process.env,
22914
+ targetKeyName,
22915
+ listWebhooks,
22916
+ fetchOrgInfo: fetchOrgInfo2
22917
+ }) => {
22918
+ const infos = new Map;
22919
+ const infoOf = (secretKey) => {
22920
+ if (!infos.has(secretKey))
22921
+ infos.set(secretKey, fetchOrgInfo2({ secretKey }));
22922
+ return infos.get(secretKey);
22923
+ };
22924
+ const targetKey = env[targetKeyName];
22925
+ const targetOrg = async () => targetKey === undefined ? undefined : mainOrgIdOf(await infoOf(targetKey));
22926
+ const named = Object.keys(env).filter(isSandboxKeyName).sort();
22927
+ const keyNames = [
22928
+ "AUTUMN_SECRET_KEY",
22929
+ ...named,
22930
+ "AUTUMN_PROD_SECRET_KEY"
22931
+ ];
22932
+ return keyNames.flatMap((keyName) => {
22933
+ const secretKey = env[keyName];
22934
+ if (!secretKey)
22935
+ return [];
22936
+ const envKey = async () => {
22937
+ if (keyName !== targetKeyName) {
22938
+ const [info, org] = await Promise.all([infoOf(secretKey), targetOrg()]);
22939
+ if (mainOrgIdOf(info) !== org)
22940
+ throw new ForeignOrgKeyError;
22941
+ }
22942
+ if (keyName === "AUTUMN_SECRET_KEY")
22943
+ return "sandbox";
22944
+ if (keyName === "AUTUMN_PROD_SECRET_KEY")
22945
+ return "live";
22946
+ return sandboxSlug((await infoOf(secretKey)).name);
22947
+ };
22948
+ const label = keyName === "AUTUMN_SECRET_KEY" ? "sandbox" : keyName === "AUTUMN_PROD_SECRET_KEY" ? "live" : `sandbox ${keyName.slice("AUTUMN_SANDBOX_".length, -"_SECRET_KEY".length).toLowerCase()}`;
22949
+ return [
22950
+ {
22951
+ keyName,
22952
+ label,
22953
+ envKey,
22954
+ listWebhooks: () => listWebhooks({ secretKey })
22955
+ }
22956
+ ];
22957
+ });
22958
+ };
22959
+
22960
+ // src/actions/pull/webhooks/readWebhookEnvs.ts
22961
+ var isRejectedKey = (error) => error instanceof AutumnApiError && (error.status === 401 || error.status === 403);
22962
+ var readWebhookEnvs = async ({
22963
+ envs
22964
+ }) => {
22965
+ const results = await Promise.all(envs.map(async (env) => {
22966
+ try {
22967
+ const [envKey, { list: list2 }] = await Promise.all([
22968
+ env.envKey(),
22969
+ env.listWebhooks()
22970
+ ]);
22971
+ return { envKey, list: list2 };
22972
+ } catch (error) {
22973
+ if (error instanceof ForeignOrgKeyError)
22974
+ return `⚠ webhooks: skipped ${env.label} (${env.keyName} belongs to another org)`;
22975
+ if (!isRejectedKey(error))
22976
+ throw error;
22977
+ return `⚠ webhooks: skipped ${env.label} (${env.keyName} was rejected)`;
22978
+ }
22979
+ }));
22980
+ return {
22981
+ read: results.filter((result) => typeof result !== "string"),
22982
+ skipped: results.filter((result) => typeof result === "string")
22983
+ };
22844
22984
  };
22845
22985
 
22846
22986
  // src/render/renderPreview.ts
@@ -24381,7 +24521,7 @@ var runPull = async ({
24381
24521
  overwrite = false,
24382
24522
  yes = false,
24383
24523
  prompter = createPrompter({ interactive: false, write }),
24384
- webhookEnv
24524
+ webhookEnvs
24385
24525
  }) => {
24386
24526
  const project = resolveProject({ cwd, configFlag });
24387
24527
  if (overwrite && !yes) {
@@ -24420,7 +24560,7 @@ var runPull = async ({
24420
24560
  diffOrExplain({ client, wire }),
24421
24561
  client.get({ include_versions: true }),
24422
24562
  client.previewUpdateOrganization(singletons.settings ?? { config: {} }),
24423
- webhookEnv === undefined ? undefined : Promise.all([webhookEnv(), client.listWebhooks({})])
24563
+ readWebhookEnvs({ envs: webhookEnvs?.() ?? [] })
24424
24564
  ]);
24425
24565
  const files = new Map;
24426
24566
  files.set(configPath, readFileSync14(configPath, "utf8"));
@@ -24491,15 +24631,16 @@ var runPull = async ({
24491
24631
  action: `set by hand: \`${singleton}\` is not an object literal`
24492
24632
  })));
24493
24633
  }
24494
- const webhookWarnings = [];
24495
- if (webhooks !== undefined) {
24496
- const [env, { list: list3 }] = webhooks;
24634
+ const webhookWarnings = [...webhooks.skipped];
24635
+ let stated2 = lists.webhooks;
24636
+ for (const { envKey, list: list3 } of webhooks.read) {
24497
24637
  const applied = applyWebhooksPull({
24498
24638
  pull: { configPath, files },
24499
24639
  remote: list3,
24500
- stated: lists.webhooks,
24501
- envKey: env.key
24640
+ stated: stated2,
24641
+ envKey
24502
24642
  });
24643
+ stated2 = applied.stated;
24503
24644
  lines.push(...applied.lines);
24504
24645
  webhookWarnings.push(...applied.warnings);
24505
24646
  unlocated.push(...applied.unlocated.map((entry) => ({
@@ -24998,25 +25139,6 @@ var runSandboxUse = async ({
24998
25139
  return result;
24999
25140
  };
25000
25141
 
25001
- // src/generated/sandboxName.ts
25002
- var RESERVED_SANDBOX_SLUGS = new Set([
25003
- "products",
25004
- "customers",
25005
- "features",
25006
- "migrations",
25007
- "dev",
25008
- "analytics",
25009
- "settings",
25010
- "admin",
25011
- "chat",
25012
- "quickstart",
25013
- "onboarding",
25014
- "impersonate-redirect",
25015
- "trmnl"
25016
- ]);
25017
- var ENV_SANDBOX_SLUGS = new Set(["live", "sandbox"]);
25018
- var sandboxSlug = (name) => name.toLowerCase().replace(/ /g, "-").replace(/[^\w\s-]/g, "");
25019
-
25020
25142
  // src/actions/webhooks/resolveWebhookEnv.ts
25021
25143
  var memoized = (fetchOrgInfo2) => {
25022
25144
  let pending;
@@ -25286,7 +25408,15 @@ Nothing applied. Re-run with --yes to apply these changes.
25286
25408
  overwrite: options.overwrite === true,
25287
25409
  yes: options.yes === true,
25288
25410
  prompter: prompterFor({ command }),
25289
- webhookEnv: webhookEnvFor({ target, command })
25411
+ webhookEnvs: () => webhookPullEnvs({
25412
+ targetKeyName: target.secretKeyName,
25413
+ listWebhooks: ({ secretKey }) => createClient({
25414
+ secretKey,
25415
+ fetch: autumnFetch,
25416
+ ...target.baseUrl ? { baseUrl: target.baseUrl } : {}
25417
+ }).listWebhooks({}),
25418
+ fetchOrgInfo: ({ secretKey }) => fetchOrgInfo({ baseUrl: targetBaseUrl({ target }), secretKey })
25419
+ })
25290
25420
  });
25291
25421
  writeStaleSkillsHint({ command });
25292
25422
  });
@@ -1 +1 @@
1
- {"version":"7.0.0-dev.20260511.1","root":["../src/bin.ts","../src/cli.ts","../src/index.ts","../src/version.ts","../src/actions/env.ts","../src/actions/login.ts","../src/actions/pull.ts","../src/actions/push.ts","../src/actions/api/callApi.ts","../src/actions/api/registerApiCommands.ts","../src/actions/env/fetchOrgInfo.ts","../src/actions/env/types/orgInfo.ts","../src/actions/init/runInit.ts","../src/actions/login/keyless.ts","../src/actions/pull/appendPlanVersionFixture.ts","../src/actions/pull/applyMappings.ts","../src/actions/pull/applyPreview.ts","../src/actions/pull/applySettingsPreview.ts","../src/actions/pull/changedFixtureKeys.ts","../src/actions/pull/elementStatesPlanId.ts","../src/actions/pull/listSourceFiles.ts","../src/actions/pull/locateFixture.ts","../src/actions/pull/pruneUnpulledPlanIds.ts","../src/actions/pull/resolveCollectionTarget.ts","../src/actions/pull/rewriteConfig.ts","../src/actions/pull/scaffoldConfig.ts","../src/actions/pull/webhooks/appendWebhook.ts","../src/actions/pull/webhooks/applyWebhooksPull.ts","../src/actions/pull/webhooks/locateWebhook.ts","../src/actions/pull/webhooks/removeWebhookEnv.ts","../src/actions/pull/webhooks/types.ts","../src/actions/pull/webhooks/updateWebhook.ts","../src/actions/push/backfillInternalIds.ts","../src/actions/push/deprecatedFields.ts","../src/actions/push/pushLanesError.ts","../src/actions/reset/runReset.ts","../src/actions/sandbox/createSandbox.ts","../src/actions/sandbox/deleteSandbox.ts","../src/actions/sandbox/listSandboxes.ts","../src/actions/sandbox/useSandbox.ts","../src/actions/sandbox/withSandboxScopeHint.ts","../src/actions/sandbox/types/sandboxClient.ts","../src/actions/skills/ignoreBundledSkills.ts","../src/actions/skills/skills.ts","../src/actions/webhooks/applyWebhooks.ts","../src/actions/webhooks/isDashboardWebhook.ts","../src/actions/webhooks/newWebhookId.ts","../src/actions/webhooks/previewWebhooks.ts","../src/actions/webhooks/resolveWebhookEnv.ts","../src/actions/webhooks/resolveWebhooksForEnv.ts","../src/actions/webhooks/webhookSecretName.ts","../src/actions/webhooks/writeWebhookSecrets.ts","../src/actions/webhooks/types/webhookEnv.ts","../src/auth/announceAuthorizationUrl.ts","../src/auth/buildAuthorizationUrl.ts","../src/auth/callbackPages.ts","../src/auth/createOrgApiKeys.ts","../src/auth/keyless.ts","../src/auth/oauthConfig.ts","../src/auth/runOAuthFlow.ts","../src/auth/browser/openSystemBrowser.ts","../src/auth/browser/tryOpenBrowser.ts","../src/auth/browser/watchLauncher.ts","../src/auth/types/browserOpener.ts","../src/auth/types/impersonationTokens.ts","../src/auth/types/oauthTokens.ts","../src/auth/types/orgApiKeys.ts","../src/config/configPackageName.ts","../src/config/legacyConfig.ts","../src/config/loadConfig.ts","../src/env/assertSandboxTarget.ts","../src/env/loadEnv.ts","../src/env/resolveTarget.ts","../src/env/sandboxKeyName.ts","../src/generated/apiRoutes.ts","../src/generated/client.ts","../src/generated/emit.ts","../src/generated/emitRuntime.ts","../src/generated/features.ts","../src/generated/isLocalWebhookUrl.ts","../src/generated/labels.ts","../src/generated/licenses.ts","../src/generated/lintRules.ts","../src/generated/lintRuntime.ts","../src/generated/mappingAssignments.ts","../src/generated/plans.ts","../src/generated/referralPrograms.ts","../src/generated/rewards.ts","../src/generated/sandboxName.ts","../src/generated/settings.ts","../src/generated/skills.ts","../src/generated/variants.ts","../src/generated/webhooks.ts","../src/generated/wire.ts","../src/http/autumnFetch.ts","../src/project/chooseConfigDir.ts","../src/project/resolveProject.ts","../src/project/rootMarker.ts","../src/prompt/prompt.ts","../src/prompt/select.ts","../src/render/renderEnv.ts","../src/render/renderPreview.ts","../src/render/renderSandboxes.ts","../src/render/renderWebhooks.ts","../src/render/stripTerminalControls.ts","../src/repo/findRepoRoot.ts","../src/surgery/appendPropertyEdit.ts","../src/surgery/appendToArray.ts","../src/surgery/appendToBinding.ts","../src/surgery/appendToCollection.ts","../src/surgery/appendToFixtureArray.ts","../src/surgery/arrayBinding.ts","../src/surgery/deleteFixtureLiteral.ts","../src/surgery/deleteReference.ts","../src/surgery/ensureBuilderImport.ts","../src/surgery/findFixture.ts","../src/surgery/fixtureEdit.ts","../src/surgery/fixtureLocation.ts","../src/surgery/insertCollection.ts","../src/surgery/insertFirstProperty.ts","../src/surgery/patchFixturePaths.ts","../src/surgery/patchFixtureProperty.ts","../src/surgery/patchSingletonProperty.ts","../src/surgery/readFixtureValue.ts","../src/surgery/replaceFixture.ts","../src/surgery/setFixtureProperty.ts","../src/surgery/staticFixtureRule.ts","../test/apiCommand.test.ts","../test/authorizationUrl.test.ts","../test/autumnFetch.test.ts","../test/backfill.test.ts","../test/backfillInlineVariants.test.ts","../test/backfillSlug.test.ts","../test/backfillVariants.test.ts","../test/chooseConfigDir.test.ts","../test/cliTargetFlags.test.ts","../test/client.test.ts","../test/configPackageName.test.ts","../test/emitFixture.test.ts","../test/env.test.ts","../test/envWriter.test.ts","../test/ignoreBundledSkills.test.ts","../test/init.test.ts","../test/keyless.test.ts","../test/legacyConfig.test.ts","../test/lintDocument.test.ts","../test/loadConfigNode.test.ts","../test/loadConfigV2.test.ts","../test/locateFixtureUnique.test.ts","../test/login.test.ts","../test/nodeNextTypes.test.ts","../test/patchFixturePaths.test.ts","../test/plans.test.ts","../test/previewIsEmpty.test.ts","../test/prompt.test.ts","../test/pull.test.ts","../test/pullByReference.test.ts","../test/pullFieldLevel.test.ts","../test/pullMappings.test.ts","../test/pullOmitDefaults.test.ts","../test/pullPlans.test.ts","../test/pullStatesSlugs.test.ts","../test/pullTwoCollections.test.ts","../test/readFixtureValue.test.ts","../test/renameHint.test.ts","../test/renderPreview.test.ts","../test/renderSandboxes.test.ts","../test/renderSettings.test.ts","../test/reset.test.ts","../test/resolveCollectionTarget.test.ts","../test/resolveProject.test.ts","../test/resolveTarget.test.ts","../test/sandboxActions.test.ts","../test/sandboxKeyName.test.ts","../test/sandboxUse.test.ts","../test/settingsActions.test.ts","../test/settingsWire.test.ts","../test/skills.test.ts","../test/watchLauncher.test.ts","../test/wireDocument.test.ts","../test/helpers/targetFor.ts","../test/surgery/appendToCollection.test.ts","../test/surgery/deleteFixtureLiteral.test.ts","../test/surgery/deleteReference.test.ts","../test/surgery/findFixture.test.ts","../test/surgery/fixtureLocation.test.ts","../test/surgery/insertCollection.test.ts","../test/surgery/insertFirstProperty.test.ts","../test/surgery/patchFixtureProperty.test.ts","../test/surgery/patchSingletonProperty.test.ts","../test/surgery/replaceFixture.test.ts","../test/surgery/staticFixtureRule.test.ts","../test/webhooks/webhooksEnv.test.ts","../test/webhooks/webhooksLint.test.ts","../test/webhooks/webhooksPull.test.ts","../test/webhooks/webhooksPush.test.ts"]}
1
+ {"version":"7.0.0-dev.20260511.1","root":["../src/bin.ts","../src/cli.ts","../src/index.ts","../src/version.ts","../src/actions/env.ts","../src/actions/login.ts","../src/actions/pull.ts","../src/actions/push.ts","../src/actions/api/callApi.ts","../src/actions/api/registerApiCommands.ts","../src/actions/env/fetchOrgInfo.ts","../src/actions/env/types/orgInfo.ts","../src/actions/init/runInit.ts","../src/actions/login/keyless.ts","../src/actions/pull/appendPlanVersionFixture.ts","../src/actions/pull/applyMappings.ts","../src/actions/pull/applyPreview.ts","../src/actions/pull/applySettingsPreview.ts","../src/actions/pull/changedFixtureKeys.ts","../src/actions/pull/elementStatesPlanId.ts","../src/actions/pull/listSourceFiles.ts","../src/actions/pull/locateFixture.ts","../src/actions/pull/pruneUnpulledPlanIds.ts","../src/actions/pull/resolveCollectionTarget.ts","../src/actions/pull/rewriteConfig.ts","../src/actions/pull/scaffoldConfig.ts","../src/actions/pull/webhooks/appendWebhook.ts","../src/actions/pull/webhooks/applyWebhooksPull.ts","../src/actions/pull/webhooks/locateWebhook.ts","../src/actions/pull/webhooks/readWebhookEnvs.ts","../src/actions/pull/webhooks/removeWebhookEnv.ts","../src/actions/pull/webhooks/types.ts","../src/actions/pull/webhooks/updateWebhook.ts","../src/actions/pull/webhooks/webhookPullEnvs.ts","../src/actions/push/backfillInternalIds.ts","../src/actions/push/deprecatedFields.ts","../src/actions/push/pushLanesError.ts","../src/actions/reset/runReset.ts","../src/actions/sandbox/createSandbox.ts","../src/actions/sandbox/deleteSandbox.ts","../src/actions/sandbox/listSandboxes.ts","../src/actions/sandbox/useSandbox.ts","../src/actions/sandbox/withSandboxScopeHint.ts","../src/actions/sandbox/types/sandboxClient.ts","../src/actions/skills/ignoreBundledSkills.ts","../src/actions/skills/skills.ts","../src/actions/webhooks/applyWebhooks.ts","../src/actions/webhooks/isDashboardWebhook.ts","../src/actions/webhooks/newWebhookId.ts","../src/actions/webhooks/previewWebhooks.ts","../src/actions/webhooks/resolveWebhookEnv.ts","../src/actions/webhooks/resolveWebhooksForEnv.ts","../src/actions/webhooks/webhookSecretName.ts","../src/actions/webhooks/writeWebhookSecrets.ts","../src/actions/webhooks/types/webhookEnv.ts","../src/auth/announceAuthorizationUrl.ts","../src/auth/buildAuthorizationUrl.ts","../src/auth/callbackPages.ts","../src/auth/createOrgApiKeys.ts","../src/auth/keyless.ts","../src/auth/oauthConfig.ts","../src/auth/runOAuthFlow.ts","../src/auth/browser/openSystemBrowser.ts","../src/auth/browser/tryOpenBrowser.ts","../src/auth/browser/watchLauncher.ts","../src/auth/types/browserOpener.ts","../src/auth/types/impersonationTokens.ts","../src/auth/types/oauthTokens.ts","../src/auth/types/orgApiKeys.ts","../src/config/configPackageName.ts","../src/config/legacyConfig.ts","../src/config/loadConfig.ts","../src/env/assertSandboxTarget.ts","../src/env/loadEnv.ts","../src/env/resolveTarget.ts","../src/env/sandboxKeyName.ts","../src/generated/apiRoutes.ts","../src/generated/client.ts","../src/generated/emit.ts","../src/generated/emitRuntime.ts","../src/generated/features.ts","../src/generated/isLocalWebhookUrl.ts","../src/generated/labels.ts","../src/generated/licenses.ts","../src/generated/lintRules.ts","../src/generated/lintRuntime.ts","../src/generated/mappingAssignments.ts","../src/generated/plans.ts","../src/generated/referralPrograms.ts","../src/generated/rewards.ts","../src/generated/sandboxName.ts","../src/generated/settings.ts","../src/generated/skills.ts","../src/generated/variants.ts","../src/generated/webhooks.ts","../src/generated/wire.ts","../src/http/autumnFetch.ts","../src/project/chooseConfigDir.ts","../src/project/resolveProject.ts","../src/project/rootMarker.ts","../src/prompt/prompt.ts","../src/prompt/select.ts","../src/render/renderEnv.ts","../src/render/renderPreview.ts","../src/render/renderSandboxes.ts","../src/render/renderWebhooks.ts","../src/render/stripTerminalControls.ts","../src/repo/findRepoRoot.ts","../src/surgery/appendPropertyEdit.ts","../src/surgery/appendToArray.ts","../src/surgery/appendToBinding.ts","../src/surgery/appendToCollection.ts","../src/surgery/appendToFixtureArray.ts","../src/surgery/arrayBinding.ts","../src/surgery/deleteFixtureLiteral.ts","../src/surgery/deleteReference.ts","../src/surgery/ensureBuilderImport.ts","../src/surgery/findFixture.ts","../src/surgery/fixtureEdit.ts","../src/surgery/fixtureLocation.ts","../src/surgery/insertCollection.ts","../src/surgery/insertFirstProperty.ts","../src/surgery/patchFixturePaths.ts","../src/surgery/patchFixtureProperty.ts","../src/surgery/patchSingletonProperty.ts","../src/surgery/readFixtureValue.ts","../src/surgery/replaceFixture.ts","../src/surgery/setFixtureProperty.ts","../src/surgery/staticFixtureRule.ts","../test/apiCommand.test.ts","../test/authorizationUrl.test.ts","../test/autumnFetch.test.ts","../test/backfill.test.ts","../test/backfillInlineVariants.test.ts","../test/backfillSlug.test.ts","../test/backfillVariants.test.ts","../test/chooseConfigDir.test.ts","../test/cliTargetFlags.test.ts","../test/client.test.ts","../test/configPackageName.test.ts","../test/emitFixture.test.ts","../test/env.test.ts","../test/envWriter.test.ts","../test/ignoreBundledSkills.test.ts","../test/init.test.ts","../test/keyless.test.ts","../test/legacyConfig.test.ts","../test/lintDocument.test.ts","../test/loadConfigNode.test.ts","../test/loadConfigV2.test.ts","../test/locateFixtureUnique.test.ts","../test/login.test.ts","../test/nodeNextTypes.test.ts","../test/patchFixturePaths.test.ts","../test/plans.test.ts","../test/previewIsEmpty.test.ts","../test/prompt.test.ts","../test/pull.test.ts","../test/pullByReference.test.ts","../test/pullFieldLevel.test.ts","../test/pullMappings.test.ts","../test/pullOmitDefaults.test.ts","../test/pullPlans.test.ts","../test/pullStatesSlugs.test.ts","../test/pullTwoCollections.test.ts","../test/readFixtureValue.test.ts","../test/renameHint.test.ts","../test/renderPreview.test.ts","../test/renderSandboxes.test.ts","../test/renderSettings.test.ts","../test/reset.test.ts","../test/resolveCollectionTarget.test.ts","../test/resolveProject.test.ts","../test/resolveTarget.test.ts","../test/sandboxActions.test.ts","../test/sandboxKeyName.test.ts","../test/sandboxUse.test.ts","../test/settingsActions.test.ts","../test/settingsWire.test.ts","../test/skills.test.ts","../test/watchLauncher.test.ts","../test/wireDocument.test.ts","../test/helpers/targetFor.ts","../test/surgery/appendToCollection.test.ts","../test/surgery/deleteFixtureLiteral.test.ts","../test/surgery/deleteReference.test.ts","../test/surgery/findFixture.test.ts","../test/surgery/fixtureLocation.test.ts","../test/surgery/insertCollection.test.ts","../test/surgery/insertFirstProperty.test.ts","../test/surgery/patchFixtureProperty.test.ts","../test/surgery/patchSingletonProperty.test.ts","../test/surgery/replaceFixture.test.ts","../test/surgery/staticFixtureRule.test.ts","../test/webhooks/webhooksEnv.test.ts","../test/webhooks/webhooksLint.test.ts","../test/webhooks/webhooksPull.test.ts","../test/webhooks/webhooksPullEnvs.test.ts","../test/webhooks/webhooksPush.test.ts"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atmn",
3
- "version": "2.0.35",
3
+ "version": "2.0.37",
4
4
  "license": "MIT",
5
5
  "description": "The CLI for Autumn — define your pricing in code, sync it to Autumn, and keep everything in version control.",
6
6
  "bin": {
@@ -49,5 +49,5 @@
49
49
  "open": "10.2.0",
50
50
  "yaml": "2.9.0"
51
51
  },
52
- "gitHead": "2cb5e886c7daf693e8236afa7998fa9a5d48757b"
52
+ "gitHead": "d9c26387dbdb3e644a5ff4720196850c94fb7845"
53
53
  }
@@ -17,6 +17,24 @@ const isVercelWebhook = (events: unknown): boolean =>
17
17
  (event) => typeof event === "string" && event.startsWith("vercel."),
18
18
  );
19
19
 
20
+ /** The config as it reads once this env's remote webhook is written into it. */
21
+ const statedFrom = ({
22
+ row,
23
+ webhook,
24
+ envKey,
25
+ }: {
26
+ row: StatedWebhook | undefined;
27
+ webhook: RemoteWebhook;
28
+ envKey: string;
29
+ }): StatedWebhook => ({
30
+ ...row,
31
+ id: webhook.id,
32
+ url: { ...row?.url, [envKey]: webhook.url },
33
+ events: webhook.events.length > 0 ? webhook.events : row?.events,
34
+ description: webhook.description,
35
+ disabled: webhook.disabled,
36
+ });
37
+
20
38
  const merge = (target: WebhookEditResult, source: WebhookEditResult): void => {
21
39
  target.lines.push(...source.lines);
22
40
  target.warnings.push(...source.warnings);
@@ -43,9 +61,11 @@ export const applyWebhooksPull = ({
43
61
  envKey: string;
44
62
  /** Mints the id a dashboard webhook is written under. */
45
63
  newId?: () => string;
46
- }): WebhookEditResult => {
64
+ }): WebhookEditResult & { stated: StatedWebhook[] } => {
47
65
  const result: WebhookEditResult = { lines: [], warnings: [], unlocated: [] };
48
66
  const statedById = new Map((stated ?? []).map((row) => [row.id, row]));
67
+ // What the next env pulled into the same files sees as the config.
68
+ const next = new Map(statedById);
49
69
  // An id the config states is the config's, whatever its shape.
50
70
  const fromDashboard = (webhook: RemoteWebhook): boolean =>
51
71
  !statedById.has(webhook.id) && isDashboardWebhook(webhook);
@@ -55,13 +75,30 @@ export const applyWebhooksPull = ({
55
75
  if (fromDashboard(webhook)) {
56
76
  // Vercel and other webhooks live in separate apps: a URL only
57
77
  // matches within one.
58
- const represented = (stated ?? []).find(
59
- (row) =>
60
- row.url?.[envKey] === webhook.url &&
61
- isVercelWebhook(row.events) === isVercelWebhook(webhook.events),
62
- );
78
+ const sameApp = (row: StatedWebhook) =>
79
+ isVercelWebhook(row.events) === isVercelWebhook(webhook.events);
80
+ // Another env's copy of this URL fills in this env's key rather than
81
+ // becoming a second webhook.
82
+ const represented =
83
+ (stated ?? []).find(
84
+ (row) => row.url?.[envKey] === webhook.url && sameApp(row),
85
+ ) ??
86
+ (stated ?? []).find(
87
+ (row) =>
88
+ row.url?.[envKey] === undefined &&
89
+ Object.values(row.url ?? {}).includes(webhook.url) &&
90
+ sameApp(row),
91
+ );
63
92
  if (represented !== undefined) {
64
93
  present.add(represented.id);
94
+ next.set(
95
+ represented.id,
96
+ statedFrom({
97
+ row: represented,
98
+ webhook: { ...webhook, id: represented.id },
99
+ envKey,
100
+ }),
101
+ );
65
102
  merge(
66
103
  result,
67
104
  updateWebhook({
@@ -85,15 +122,20 @@ export const applyWebhooksPull = ({
85
122
  webhook: { ...webhook, id },
86
123
  envKey,
87
124
  });
88
- if (failure === null)
125
+ if (failure === null) {
126
+ next.set(
127
+ id,
128
+ statedFrom({ row: undefined, webhook: { ...webhook, id }, envKey }),
129
+ );
89
130
  result.lines.push(
90
131
  `+ webhook ${id} (made in the dashboard; push adopts it by URL)`,
91
132
  );
92
- else result.unlocated.push({ id, action: failure });
133
+ } else result.unlocated.push({ id, action: failure });
93
134
  continue;
94
135
  }
95
136
  present.add(webhook.id);
96
137
  const row = statedById.get(webhook.id);
138
+ next.set(webhook.id, statedFrom({ row, webhook, envKey }));
97
139
  if (row !== undefined) {
98
140
  merge(result, updateWebhook({ pull, webhook, stated: row, envKey }));
99
141
  continue;
@@ -105,7 +147,13 @@ export const applyWebhooksPull = ({
105
147
 
106
148
  for (const row of stated ?? []) {
107
149
  if (present.has(row.id) || row.url?.[envKey] === undefined) continue;
108
- merge(result, removeWebhookEnv({ pull, stated: row, envKey }));
150
+ const removed = removeWebhookEnv({ pull, stated: row, envKey });
151
+ merge(result, removed);
152
+ // A key left in the source (code, or unlocated) is still the config's.
153
+ if (removed.lines.length === 0) continue;
154
+ const { [envKey]: _removed, ...url } = row.url;
155
+ if (Object.keys(url).length === 0) next.delete(row.id);
156
+ else next.set(row.id, { ...row, url });
109
157
  }
110
- return result;
158
+ return { ...result, stated: [...next.values()] };
111
159
  };