atmn 2.0.42 → 2.0.44
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 +360 -368
- package/dist/index.js +21 -19
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/actions/pull/webhooks/appendWebhook.ts +4 -3
- package/src/actions/pull/webhooks/applyWebhooksPull.ts +27 -98
- package/src/actions/pull/webhooks/deleteWebhook.ts +59 -0
- package/src/actions/pull/webhooks/locateWebhook.ts +9 -1
- package/src/actions/pull/webhooks/readWebhookEnvs.ts +7 -12
- package/src/actions/pull/webhooks/types.ts +3 -2
- package/src/actions/pull/webhooks/updateWebhook.ts +32 -36
- package/src/actions/pull/webhooks/webhookEnvSkipWarning.ts +29 -0
- package/src/actions/pull/webhooks/webhookPullEnvs.ts +5 -5
- package/src/actions/pull.ts +1 -1
- package/src/actions/push.ts +30 -12
- package/src/actions/webhooks/applyWebhooks.ts +41 -10
- package/src/actions/webhooks/previewWebhookEnvs.ts +213 -0
- package/src/actions/webhooks/resolveWebhooksForEnv.ts +5 -16
- package/src/actions/webhooks/types/webhookEnv.ts +1 -3
- package/src/actions/webhooks/webhookEnvFailures.ts +27 -0
- package/src/actions/webhooks/webhookSecretName.ts +10 -9
- package/src/actions/webhooks/writeWebhookSecrets.ts +5 -2
- package/src/auth/runOAuthFlow.ts +1 -1
- package/src/cli.ts +23 -25
- package/src/generated/emit.ts +4 -4
- package/src/generated/emitRuntime.ts +3 -3
- package/src/generated/lintRules.ts +17 -18
- package/src/generated/lintRuntime.ts +10 -0
- package/src/generated/skills.ts +10 -10
- package/src/generated/webhooks.ts +5 -7
- package/src/generated/wire.ts +0 -1
- package/src/render/renderPreview.ts +25 -9
- package/src/render/renderWebhooks.ts +8 -2
- package/src/actions/pull/webhooks/removeWebhookEnv.ts +0 -141
- package/src/actions/webhooks/isDashboardWebhook.ts +0 -9
- package/src/actions/webhooks/newWebhookId.ts +0 -4
- package/src/actions/webhooks/previewWebhooks.ts +0 -40
- package/src/actions/webhooks/resolveWebhookEnv.ts +0 -34
package/dist/bin.js
CHANGED
|
@@ -6245,25 +6245,38 @@ var LINT_RULES = {
|
|
|
6245
6245
|
webhooks: {
|
|
6246
6246
|
label: "webhook",
|
|
6247
6247
|
idField: "id",
|
|
6248
|
-
required: ["id", "url"],
|
|
6248
|
+
required: ["env", "id", "url"],
|
|
6249
6249
|
fields: {
|
|
6250
6250
|
id: {
|
|
6251
6251
|
minLength: 1,
|
|
6252
6252
|
maxLength: 256,
|
|
6253
6253
|
pattern: "^[a-zA-Z0-9_-]+$"
|
|
6254
|
+
},
|
|
6255
|
+
env: {
|
|
6256
|
+
pattern: "^[a-z0-9_-]+$"
|
|
6257
|
+
},
|
|
6258
|
+
url: {
|
|
6259
|
+
pattern: "^[Hh][Tt][Tt][Pp][Ss]:\\/\\/"
|
|
6254
6260
|
}
|
|
6255
6261
|
},
|
|
6256
6262
|
rules: [
|
|
6263
|
+
{
|
|
6264
|
+
kind: "notMap",
|
|
6265
|
+
field: "url",
|
|
6266
|
+
because: 'Each webhook() is one endpoint in one env: split into one webhook() per env, e.g. webhook({ id, env: "live", url }).'
|
|
6267
|
+
},
|
|
6257
6268
|
{
|
|
6258
6269
|
kind: "unique",
|
|
6259
6270
|
field: "id",
|
|
6260
|
-
|
|
6271
|
+
alongside: "env",
|
|
6272
|
+
because: "Two webhooks claiming one id in one env race to define the same endpoint."
|
|
6261
6273
|
},
|
|
6262
6274
|
{
|
|
6263
6275
|
kind: "unique",
|
|
6264
6276
|
field: "id",
|
|
6277
|
+
alongside: "env",
|
|
6265
6278
|
asEnvName: true,
|
|
6266
|
-
because: "Each webhook's signing secret is saved
|
|
6279
|
+
because: "Each webhook's signing secret is saved under its id in an env var name, so these two would overwrite each other's secret. Rename one."
|
|
6267
6280
|
},
|
|
6268
6281
|
{
|
|
6269
6282
|
kind: "rejects",
|
|
@@ -6277,12 +6290,6 @@ var LINT_RULES = {
|
|
|
6277
6290
|
prefix: "vercel.",
|
|
6278
6291
|
because: "vercel.* events can't be mixed with other events in one webhook: they're delivered from a separate app. Make one webhook for each."
|
|
6279
6292
|
},
|
|
6280
|
-
{
|
|
6281
|
-
kind: "nonEmpty",
|
|
6282
|
-
field: "url",
|
|
6283
|
-
warning: true,
|
|
6284
|
-
because: "With no environment key the webhook is registered nowhere. Add `live`, `sandbox` or a sandbox's slug."
|
|
6285
|
-
},
|
|
6286
6293
|
{
|
|
6287
6294
|
kind: "knownValues",
|
|
6288
6295
|
field: "events",
|
|
@@ -6304,14 +6311,6 @@ var LINT_RULES = {
|
|
|
6304
6311
|
because: "isn't known to this atmn version; the server will check it."
|
|
6305
6312
|
}
|
|
6306
6313
|
]
|
|
6307
|
-
},
|
|
6308
|
-
"webhooks.url": {
|
|
6309
|
-
keys: {
|
|
6310
|
-
pattern: "^[a-z0-9_-]+$"
|
|
6311
|
-
},
|
|
6312
|
-
values: {
|
|
6313
|
-
pattern: "^[Hh][Tt][Tt][Pp][Ss]:\\/\\/"
|
|
6314
|
-
}
|
|
6315
6314
|
}
|
|
6316
6315
|
};
|
|
6317
6316
|
|
|
@@ -6532,6 +6531,10 @@ var entryRuleFailures = ({
|
|
|
6532
6531
|
return [];
|
|
6533
6532
|
return value.filter((item) => typeof item === "string" && !rule.values.includes(item)).map((item) => `\`${item}\` ${rule.because}`);
|
|
6534
6533
|
}
|
|
6534
|
+
case "notMap": {
|
|
6535
|
+
const value = entry[rule.field];
|
|
6536
|
+
return isEntry(value) ? [`${rule.field} is a map. ${rule.because}`] : [];
|
|
6537
|
+
}
|
|
6535
6538
|
case "uniformPrefix": {
|
|
6536
6539
|
const value = entry[rule.field];
|
|
6537
6540
|
if (!Array.isArray(value))
|
|
@@ -7022,8 +7025,7 @@ var CATALOG_HINTS = hintsOf({
|
|
|
7022
7025
|
"plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.creditSchema.multipliers",
|
|
7023
7026
|
"plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.creditSchema.multipliers.*.match",
|
|
7024
7027
|
"plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.modelMarkups",
|
|
7025
|
-
"plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.providerMarkups"
|
|
7026
|
-
"webhooks.url"
|
|
7028
|
+
"plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.providerMarkups"
|
|
7027
7029
|
],
|
|
7028
7030
|
frozenPaths: [
|
|
7029
7031
|
"plans.licenses.metadata",
|
|
@@ -7321,7 +7323,7 @@ var createClient = (options) => ({
|
|
|
7321
7323
|
});
|
|
7322
7324
|
|
|
7323
7325
|
// src/version.ts
|
|
7324
|
-
var version = "2.0.
|
|
7326
|
+
var version = "2.0.44";
|
|
7325
7327
|
|
|
7326
7328
|
// src/http/autumnFetch.ts
|
|
7327
7329
|
var runtime = typeof Bun !== "undefined" ? `bun ${Bun.version}` : `node ${process.versions.node}`;
|
|
@@ -9459,11 +9461,11 @@ var SYNCED_LISTS = {
|
|
|
9459
9461
|
builder: "webhook",
|
|
9460
9462
|
idField: "id",
|
|
9461
9463
|
responseIdField: "id",
|
|
9462
|
-
keys: ["id", "url", "events", "description", "disabled"],
|
|
9463
|
-
required: ["id", "url"],
|
|
9464
|
-
paths: ["description", "disabled", "events", "id", "url"],
|
|
9464
|
+
keys: ["id", "env", "url", "events", "description", "disabled"],
|
|
9465
|
+
required: ["env", "id", "url"],
|
|
9466
|
+
paths: ["description", "disabled", "env", "events", "id", "url"],
|
|
9465
9467
|
pull: true,
|
|
9466
|
-
|
|
9468
|
+
envField: "env"
|
|
9467
9469
|
}
|
|
9468
9470
|
};
|
|
9469
9471
|
var NESTED_FIXTURES = {
|
|
@@ -9578,16 +9580,16 @@ import { dirname as dirname4, join as join7 } from "node:path";
|
|
|
9578
9580
|
import chalk3 from "chalk";
|
|
9579
9581
|
|
|
9580
9582
|
// src/generated/skills.ts
|
|
9581
|
-
var SKILLS_VERSION = "2.0.
|
|
9583
|
+
var SKILLS_VERSION = "2.0.44";
|
|
9582
9584
|
var SKILLS = [
|
|
9583
9585
|
{
|
|
9584
9586
|
name: "autumn-setup",
|
|
9585
9587
|
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.",
|
|
9586
|
-
version: "2.0.
|
|
9588
|
+
version: "2.0.44",
|
|
9587
9589
|
markdown: `---
|
|
9588
9590
|
name: autumn-setup
|
|
9589
9591
|
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.
|
|
9590
|
-
version: 2.0.
|
|
9592
|
+
version: 2.0.44
|
|
9591
9593
|
---
|
|
9592
9594
|
|
|
9593
9595
|
# Setup
|
|
@@ -9745,11 +9747,11 @@ Then stop. Don't keep building, don't tour the dashboard, don't deploy anything.
|
|
|
9745
9747
|
{
|
|
9746
9748
|
name: "autumn-catalog",
|
|
9747
9749
|
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.",
|
|
9748
|
-
version: "2.0.
|
|
9750
|
+
version: "2.0.44",
|
|
9749
9751
|
markdown: `---
|
|
9750
9752
|
name: autumn-catalog
|
|
9751
9753
|
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.
|
|
9752
|
-
version: 2.0.
|
|
9754
|
+
version: 2.0.44
|
|
9753
9755
|
---
|
|
9754
9756
|
|
|
9755
9757
|
# Catalog
|
|
@@ -13616,7 +13618,7 @@ Pitch: "Pro $20/mo or $200/yr — 1,000 messages per month either way."
|
|
|
13616
13618
|
},
|
|
13617
13619
|
{
|
|
13618
13620
|
path: "references/atmn.md",
|
|
13619
|
-
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.\n- Leave `events` out to receive every event. Don't write `events: []`.\n- Vercel webhooks require their own distinct webhook and are a mutually exclusive variant to the rest. It can only listen to Vercel events and cannot later be changed to listen to non-Vercel events.\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."
|
|
13621
|
+
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. Each `webhook()` is one endpoint in one environment, keyed by `env` and `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\nTo register a webhook in several environments, write one `webhook()` per environment. The same `id` may repeat across environments:\n\n```ts\nimport { atmn, webhook } from \"atmn\";\n\nconst events = [\"billing.updated\", \"balances.limit_reached\"];\n\nexport default atmn({\n webhooks: [\n webhook({ id: \"billing\", env: \"live\", url: \"https://myapp.com/api/autumn\", events }), // atmn push -p\n webhook({ id: \"billing\", env: \"sandbox\", url: \"https://staging.myapp.com/api/autumn\", events }), // the default sandbox\n webhook({ id: \"billing\", env: \"qa-team\", url: \"https://qa.myapp.com/api/autumn\", events }), // the sandbox named qa-team\n ],\n});\n```\n\n- `env`: `live` for `-p`, `sandbox` for the default sandbox, or a named sandbox's slug (its name lowercased; no spaces). An environment with no entry has nothing registered. Never copy a sandbox URL into a `live` entry: ask the user for the production URL, or leave `live` out.\n- One `id` may appear once per `env`; a repeat in the same env is refused.\n- Configs from atmn 2.0.36–2.0.42 stated `url` as a map (`url: { live, sandbox }`). That shape is refused: split it into one `webhook()` per env.\n- Every URL must be https and publicly reachable; localhost and private networks are refused, tunnels such as ngrok work.\n- Leave `events` out to receive every event. Don't write `events: []`.\n- Vercel webhooks require their own distinct webhook and are a mutually exclusive variant to the rest. It can only listen to Vercel events and cannot later be changed to listen to non-Vercel events.\n- `id` is permanent: changing it registers a new webhook. In one env, two ids that differ only by case or `-`/`_` are refused.\n- A webhook made in the dashboard has no id of its own, so its entry uses the `ep_…` id `pull` writes. Keep that id: push updates that endpoint by it.\n- `push` syncs webhooks to every sandbox with a key in your env files (`AUTUMN_SECRET_KEY` → `sandbox`, each `AUTUMN_SANDBOX_<id>_SECRET_KEY` → its slug), each through its own key, whichever sandbox is pinned. The preview shows one `Webhooks · <env>` block per env with changes. A key that's rejected or belongs to another org skips its env with a warning. The catalog and settings still go to the targeted env only.\n- Production webhooks change only with `-p`, which syncs `live` and no sandbox. When `AUTUMN_PROD_SECRET_KEY` is set, a plain push also previews `live` read-only, and if production would change it prints `Production webhooks differ from your config (<ids>). Run atmn push -p to update production.` Relay that line, and run `-p` only when the user asks.\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>_<SANDBOX_SLUG>_SECRET` in `.env.local` (or `.env`) for each sandbox (`AUTUMN_WEBHOOK_BILLING_SANDBOX_SECRET` for the default sandbox, `AUTUMN_WEBHOOK_BILLING_QA_TEAM_SECRET` for `qa-team`). Secrets saved earlier keep their existing names. If two sandboxes' names differ only by punctuation (`qa-team`, `qa_team`), they would share a variable, so push refuses before writing; ask the user to rename one. 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, belongs to another org, or whose sandbox is gone skips its env with a warning, and that env's entries stay. For each env it read, pull writes one entry per endpoint: it adds new ones (dashboard ones under their `ep_…` id), updates changed fields, and deletes an entry the server no longer has in that env. A value written as code (`process.env.X`) is never rewritten; pull warns when it differs from the server.\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."
|
|
13620
13622
|
},
|
|
13621
13623
|
{
|
|
13622
13624
|
path: "references/catalog-update.md",
|
|
@@ -13631,11 +13633,11 @@ Pitch: "Pro $20/mo or $200/yr — 1,000 messages per month either way."
|
|
|
13631
13633
|
{
|
|
13632
13634
|
name: "autumn-integrate",
|
|
13633
13635
|
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.",
|
|
13634
|
-
version: "2.0.
|
|
13636
|
+
version: "2.0.44",
|
|
13635
13637
|
markdown: `---
|
|
13636
13638
|
name: autumn-integrate
|
|
13637
13639
|
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.
|
|
13638
|
-
version: 2.0.
|
|
13640
|
+
version: 2.0.44
|
|
13639
13641
|
---
|
|
13640
13642
|
|
|
13641
13643
|
# Integrate
|
|
@@ -18237,11 +18239,11 @@ Monitor your logs for this message. If you see it, check [status.useautumn.com](
|
|
|
18237
18239
|
{
|
|
18238
18240
|
name: "autumn-concepts",
|
|
18239
18241
|
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.",
|
|
18240
|
-
version: "2.0.
|
|
18242
|
+
version: "2.0.44",
|
|
18241
18243
|
markdown: `---
|
|
18242
18244
|
name: autumn-concepts
|
|
18243
18245
|
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.
|
|
18244
|
-
version: 2.0.
|
|
18246
|
+
version: 2.0.44
|
|
18245
18247
|
---
|
|
18246
18248
|
|
|
18247
18249
|
# Concepts
|
|
@@ -19429,7 +19431,7 @@ var runOAuthFlow = async ({
|
|
|
19429
19431
|
if (outcome)
|
|
19430
19432
|
return outcome;
|
|
19431
19433
|
}
|
|
19432
|
-
throw new Error(`All OAuth callback ports (${OAUTH_PORTS
|
|
19434
|
+
throw new Error(`All OAuth callback ports (${OAUTH_PORTS[0]}-${OAUTH_PORTS[OAUTH_PORTS.length - 1]}) are in use.`);
|
|
19433
19435
|
};
|
|
19434
19436
|
|
|
19435
19437
|
// src/actions/login.ts
|
|
@@ -22493,65 +22495,6 @@ var rewriteConfig = ({
|
|
|
22493
22495
|
return { rewritten: [configPath, ...rewritten], kept };
|
|
22494
22496
|
};
|
|
22495
22497
|
|
|
22496
|
-
// src/actions/webhooks/isDashboardWebhook.ts
|
|
22497
|
-
var SVIX_ENDPOINT_ID = /^ep_[0-9A-Za-z]{27}$/;
|
|
22498
|
-
var isDashboardWebhook = ({ id }) => SVIX_ENDPOINT_ID.test(id);
|
|
22499
|
-
|
|
22500
|
-
// ../ksuid/src/base62.ts
|
|
22501
|
-
var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
22502
|
-
var base62Encode = ({
|
|
22503
|
-
bytes,
|
|
22504
|
-
padTo
|
|
22505
|
-
}) => {
|
|
22506
|
-
const digits = [];
|
|
22507
|
-
const working = new Uint8Array(bytes);
|
|
22508
|
-
let allZero = false;
|
|
22509
|
-
while (!allZero) {
|
|
22510
|
-
allZero = true;
|
|
22511
|
-
let remainder = 0;
|
|
22512
|
-
for (let i = 0;i < working.length; i++) {
|
|
22513
|
-
const accumulator = remainder * 256 + working[i];
|
|
22514
|
-
working[i] = Math.floor(accumulator / 62);
|
|
22515
|
-
remainder = accumulator % 62;
|
|
22516
|
-
if (working[i] !== 0)
|
|
22517
|
-
allZero = false;
|
|
22518
|
-
}
|
|
22519
|
-
digits.push(remainder);
|
|
22520
|
-
}
|
|
22521
|
-
digits.reverse();
|
|
22522
|
-
while (digits.length < padTo) {
|
|
22523
|
-
digits.unshift(0);
|
|
22524
|
-
}
|
|
22525
|
-
return digits.map((d) => ALPHABET[d]).join("");
|
|
22526
|
-
};
|
|
22527
|
-
|
|
22528
|
-
// ../ksuid/src/random.ts
|
|
22529
|
-
var getRandomBytes = ({ size }) => {
|
|
22530
|
-
const buffer = new Uint8Array(size);
|
|
22531
|
-
globalThis.crypto.getRandomValues(buffer);
|
|
22532
|
-
return buffer;
|
|
22533
|
-
};
|
|
22534
|
-
|
|
22535
|
-
// ../ksuid/src/index.ts
|
|
22536
|
-
var KSUID_EPOCH = 1400000000;
|
|
22537
|
-
var PAYLOAD_BYTES = 20;
|
|
22538
|
-
var ENCODED_LENGTH = 27;
|
|
22539
|
-
var generateKsuid = ({ prefix = "" } = {}) => {
|
|
22540
|
-
const payload = new Uint8Array(PAYLOAD_BYTES);
|
|
22541
|
-
const timestamp = Math.floor(Date.now() / 1000) - KSUID_EPOCH;
|
|
22542
|
-
payload[0] = timestamp >>> 24 & 255;
|
|
22543
|
-
payload[1] = timestamp >>> 16 & 255;
|
|
22544
|
-
payload[2] = timestamp >>> 8 & 255;
|
|
22545
|
-
payload[3] = timestamp & 255;
|
|
22546
|
-
const randomBytes = getRandomBytes({ size: 16 });
|
|
22547
|
-
payload.set(randomBytes, 4);
|
|
22548
|
-
const encoded = base62Encode({ bytes: payload, padTo: ENCODED_LENGTH });
|
|
22549
|
-
return `${prefix}${encoded}`;
|
|
22550
|
-
};
|
|
22551
|
-
|
|
22552
|
-
// src/actions/webhooks/newWebhookId.ts
|
|
22553
|
-
var newWebhookId = () => generateKsuid({ prefix: "wh_" });
|
|
22554
|
-
|
|
22555
22498
|
// src/actions/pull/webhooks/appendWebhook.ts
|
|
22556
22499
|
var SPEC = SYNCED_LISTS.webhooks;
|
|
22557
22500
|
var COLLECTION = "webhooks";
|
|
@@ -22560,7 +22503,8 @@ var fixtureRowOf = ({
|
|
|
22560
22503
|
envKey
|
|
22561
22504
|
}) => ({
|
|
22562
22505
|
id: webhook.id,
|
|
22563
|
-
|
|
22506
|
+
[SPEC.envField]: envKey,
|
|
22507
|
+
url: webhook.url,
|
|
22564
22508
|
...webhook.events.length > 0 ? { events: webhook.events } : {},
|
|
22565
22509
|
...webhook.description ? { description: webhook.description } : {},
|
|
22566
22510
|
...webhook.disabled ? { disabled: true } : {}
|
|
@@ -22611,19 +22555,67 @@ var appendWebhook = ({
|
|
|
22611
22555
|
var SPEC2 = SYNCED_LISTS.webhooks;
|
|
22612
22556
|
var locateWebhook = ({
|
|
22613
22557
|
pull,
|
|
22614
|
-
id
|
|
22558
|
+
id,
|
|
22559
|
+
envKey
|
|
22615
22560
|
}) => locateFixture({
|
|
22616
22561
|
configPath: pull.configPath,
|
|
22617
22562
|
files: pull.files,
|
|
22618
22563
|
builder: SPEC2.builder,
|
|
22619
22564
|
idField: SPEC2.idField,
|
|
22620
22565
|
id,
|
|
22566
|
+
where: webhookWhere({ envKey }),
|
|
22621
22567
|
allowDynamic: true
|
|
22622
22568
|
});
|
|
22569
|
+
var webhookWhere = ({ envKey }) => [
|
|
22570
|
+
{ field: SPEC2.envField, equals: envKey }
|
|
22571
|
+
];
|
|
22572
|
+
|
|
22573
|
+
// src/actions/pull/webhooks/deleteWebhook.ts
|
|
22574
|
+
var SPEC3 = SYNCED_LISTS.webhooks;
|
|
22575
|
+
var deleteWebhook = ({
|
|
22576
|
+
pull,
|
|
22577
|
+
stated: stated2,
|
|
22578
|
+
envKey
|
|
22579
|
+
}) => {
|
|
22580
|
+
const result = { lines: [], warnings: [], unlocated: [] };
|
|
22581
|
+
const located = locateWebhook({ pull, id: stated2.id, envKey });
|
|
22582
|
+
const removed = located === null ? null : deleteFixtureLiteral({
|
|
22583
|
+
source: located.source,
|
|
22584
|
+
builder: SPEC3.builder,
|
|
22585
|
+
idField: SPEC3.idField,
|
|
22586
|
+
id: stated2.id,
|
|
22587
|
+
where: webhookWhere({ envKey })
|
|
22588
|
+
});
|
|
22589
|
+
if (located === null || removed === null) {
|
|
22590
|
+
result.unlocated.push({
|
|
22591
|
+
id: stated2.id,
|
|
22592
|
+
action: `delete the ${envKey} webhook by hand`
|
|
22593
|
+
});
|
|
22594
|
+
return result;
|
|
22595
|
+
}
|
|
22596
|
+
pull.files.set(located.file, removed.source);
|
|
22597
|
+
if (removed.exportedName !== undefined) {
|
|
22598
|
+
const referenceFiles = new Set([pull.configPath]);
|
|
22599
|
+
const target = resolveCollectionTarget({
|
|
22600
|
+
configPath: pull.configPath,
|
|
22601
|
+
files: pull.files,
|
|
22602
|
+
collection: "webhooks"
|
|
22603
|
+
});
|
|
22604
|
+
if (target !== null)
|
|
22605
|
+
referenceFiles.add(target.file);
|
|
22606
|
+
for (const file of referenceFiles)
|
|
22607
|
+
pull.files.set(file, deleteReference({
|
|
22608
|
+
source: pull.files.get(file) ?? "",
|
|
22609
|
+
name: removed.exportedName
|
|
22610
|
+
}));
|
|
22611
|
+
}
|
|
22612
|
+
result.lines.push(`- webhook ${stated2.id} (${envKey})`);
|
|
22613
|
+
return result;
|
|
22614
|
+
};
|
|
22623
22615
|
|
|
22624
22616
|
// src/actions/pull/webhooks/updateWebhook.ts
|
|
22625
22617
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
|
|
22626
|
-
var
|
|
22618
|
+
var SPEC4 = SYNCED_LISTS.webhooks;
|
|
22627
22619
|
var sameEvents = (left, right) => Array.isArray(left) && isDeepStrictEqual2([...left].sort(), [...right].sort());
|
|
22628
22620
|
var fieldAssignments = ({
|
|
22629
22621
|
webhook,
|
|
@@ -22658,8 +22650,8 @@ var nonLiteralUrlWarning = ({
|
|
|
22658
22650
|
}) => {
|
|
22659
22651
|
const pad = " ".repeat(id.length + 4);
|
|
22660
22652
|
return [
|
|
22661
|
-
`⚠ ${id}
|
|
22662
|
-
`${pad}server: ${server
|
|
22653
|
+
`⚠ ${id} (${envKey}) url isn't a plain string, so pull left it untouched`,
|
|
22654
|
+
`${pad}server: ${server}`,
|
|
22663
22655
|
`${pad}your config: ${typeof config === "string" ? config : "(no value)"}`,
|
|
22664
22656
|
`${pad}Update it by hand, or make it a string and pull will manage it.`
|
|
22665
22657
|
].join(`
|
|
@@ -22672,161 +22664,57 @@ var updateWebhook = ({
|
|
|
22672
22664
|
envKey
|
|
22673
22665
|
}) => {
|
|
22674
22666
|
const result = { lines: [], warnings: [], unlocated: [] };
|
|
22675
|
-
const
|
|
22676
|
-
|
|
22677
|
-
|
|
22678
|
-
id: webhook.id,
|
|
22679
|
-
action: "update from the server by hand"
|
|
22680
|
-
});
|
|
22681
|
-
return result;
|
|
22682
|
-
}
|
|
22683
|
-
const patched = patchFixturePaths({
|
|
22667
|
+
const label = `${webhook.id} (${envKey})`;
|
|
22668
|
+
const located = locateWebhook({ pull, id: webhook.id, envKey });
|
|
22669
|
+
const patched = located === null ? null : patchFixturePaths({
|
|
22684
22670
|
source: located.source,
|
|
22685
|
-
builder:
|
|
22686
|
-
idField:
|
|
22671
|
+
builder: SPEC4.builder,
|
|
22672
|
+
idField: SPEC4.idField,
|
|
22687
22673
|
id: webhook.id,
|
|
22674
|
+
where: webhookWhere({ envKey }),
|
|
22688
22675
|
assignments: [
|
|
22689
|
-
{ path: ["url"
|
|
22676
|
+
{ path: ["url"], text: JSON.stringify(webhook.url) },
|
|
22690
22677
|
...fieldAssignments({ webhook, stated: stated2 })
|
|
22691
22678
|
],
|
|
22692
22679
|
bareKeys: true
|
|
22693
22680
|
});
|
|
22694
|
-
if (patched === null) {
|
|
22681
|
+
if (located === null || patched === null) {
|
|
22695
22682
|
result.unlocated.push({
|
|
22696
22683
|
id: webhook.id,
|
|
22697
|
-
action:
|
|
22684
|
+
action: `update the ${envKey} webhook from the server by hand`
|
|
22698
22685
|
});
|
|
22699
22686
|
return result;
|
|
22700
22687
|
}
|
|
22701
22688
|
for (const { path } of patched.skipped) {
|
|
22702
22689
|
const field = path.join(".");
|
|
22703
|
-
if (field !==
|
|
22704
|
-
result.warnings.push(`⚠ ${
|
|
22690
|
+
if (field !== "url") {
|
|
22691
|
+
result.warnings.push(`⚠ ${label} ${field} isn't a plain literal, so pull left it untouched`);
|
|
22705
22692
|
continue;
|
|
22706
22693
|
}
|
|
22707
|
-
|
|
22708
|
-
if (config === webhook.url)
|
|
22694
|
+
if (stated2.url === webhook.url)
|
|
22709
22695
|
continue;
|
|
22710
22696
|
result.warnings.push(nonLiteralUrlWarning({
|
|
22711
22697
|
id: webhook.id,
|
|
22712
22698
|
envKey,
|
|
22713
22699
|
server: webhook.url,
|
|
22714
|
-
config
|
|
22715
|
-
}));
|
|
22716
|
-
}
|
|
22717
|
-
if (patched.source === located.source)
|
|
22718
|
-
return result;
|
|
22719
|
-
pull.files.set(located.file, patched.source);
|
|
22720
|
-
result.lines.push(`~ webhook ${webhook.id}`);
|
|
22721
|
-
return result;
|
|
22722
|
-
};
|
|
22723
|
-
|
|
22724
|
-
// src/actions/pull/webhooks/removeWebhookEnv.ts
|
|
22725
|
-
var SPEC4 = SYNCED_LISTS.webhooks;
|
|
22726
|
-
var urlMapIsEmpty = ({
|
|
22727
|
-
pull,
|
|
22728
|
-
id
|
|
22729
|
-
}) => {
|
|
22730
|
-
const located = locateWebhook({ pull, id });
|
|
22731
|
-
const object = located === null ? null : fixtureObjectOf(located.node);
|
|
22732
|
-
const member = object?.namedChildren().find((child) => child.kind() === "pair" && child.field("key") !== null && fixtureKeyOf({ node: child.field("key") }) === "url");
|
|
22733
|
-
const value = member?.field("value");
|
|
22734
|
-
if (value === null || value === undefined)
|
|
22735
|
-
return false;
|
|
22736
|
-
const read = readFixtureValue({ node: value });
|
|
22737
|
-
return typeof read === "object" && read !== null && Object.keys(read).length === 0;
|
|
22738
|
-
};
|
|
22739
|
-
var deleteWebhook = ({
|
|
22740
|
-
pull,
|
|
22741
|
-
id
|
|
22742
|
-
}) => {
|
|
22743
|
-
const located = locateWebhook({ pull, id });
|
|
22744
|
-
if (located === null)
|
|
22745
|
-
return false;
|
|
22746
|
-
const removed = deleteFixtureLiteral({
|
|
22747
|
-
source: located.source,
|
|
22748
|
-
builder: SPEC4.builder,
|
|
22749
|
-
idField: SPEC4.idField,
|
|
22750
|
-
id
|
|
22751
|
-
});
|
|
22752
|
-
if (removed === null)
|
|
22753
|
-
return false;
|
|
22754
|
-
pull.files.set(located.file, removed.source);
|
|
22755
|
-
if (removed.exportedName !== undefined) {
|
|
22756
|
-
const referenceFiles = new Set([pull.configPath]);
|
|
22757
|
-
const target = resolveCollectionTarget({
|
|
22758
|
-
configPath: pull.configPath,
|
|
22759
|
-
files: pull.files,
|
|
22760
|
-
collection: "webhooks"
|
|
22761
|
-
});
|
|
22762
|
-
if (target !== null)
|
|
22763
|
-
referenceFiles.add(target.file);
|
|
22764
|
-
for (const file of referenceFiles)
|
|
22765
|
-
pull.files.set(file, deleteReference({
|
|
22766
|
-
source: pull.files.get(file) ?? "",
|
|
22767
|
-
name: removed.exportedName
|
|
22768
|
-
}));
|
|
22769
|
-
}
|
|
22770
|
-
return true;
|
|
22771
|
-
};
|
|
22772
|
-
var removeWebhookEnv = ({
|
|
22773
|
-
pull,
|
|
22774
|
-
stated: stated2,
|
|
22775
|
-
envKey
|
|
22776
|
-
}) => {
|
|
22777
|
-
const result = { lines: [], warnings: [], unlocated: [] };
|
|
22778
|
-
const located = locateWebhook({ pull, id: stated2.id });
|
|
22779
|
-
const patched = located === null ? null : patchFixturePaths({
|
|
22780
|
-
source: located.source,
|
|
22781
|
-
builder: SPEC4.builder,
|
|
22782
|
-
idField: SPEC4.idField,
|
|
22783
|
-
id: stated2.id,
|
|
22784
|
-
assignments: [{ path: ["url", envKey], text: null }]
|
|
22785
|
-
});
|
|
22786
|
-
if (located === null || patched === null) {
|
|
22787
|
-
result.unlocated.push({
|
|
22788
|
-
id: stated2.id,
|
|
22789
|
-
action: `remove url.${envKey} by hand`
|
|
22790
|
-
});
|
|
22791
|
-
return result;
|
|
22792
|
-
}
|
|
22793
|
-
if (patched.skipped.length > 0) {
|
|
22794
|
-
result.warnings.push(nonLiteralUrlWarning({
|
|
22795
|
-
id: stated2.id,
|
|
22796
|
-
envKey,
|
|
22797
|
-
server: undefined,
|
|
22798
|
-
config: stated2.url?.[envKey]
|
|
22700
|
+
config: stated2.url
|
|
22799
22701
|
}));
|
|
22800
|
-
return result;
|
|
22801
22702
|
}
|
|
22802
22703
|
if (patched.source === located.source)
|
|
22803
22704
|
return result;
|
|
22804
22705
|
pull.files.set(located.file, patched.source);
|
|
22805
|
-
|
|
22806
|
-
if (deleteWebhook({ pull, id: stated2.id })) {
|
|
22807
|
-
result.lines.push(`- webhook ${stated2.id}`);
|
|
22808
|
-
return result;
|
|
22809
|
-
}
|
|
22810
|
-
result.unlocated.push({
|
|
22811
|
-
id: stated2.id,
|
|
22812
|
-
action: "delete the webhook by hand: its url map is now empty"
|
|
22813
|
-
});
|
|
22814
|
-
return result;
|
|
22815
|
-
}
|
|
22816
|
-
result.lines.push(`- webhook ${stated2.id} url.${envKey}`);
|
|
22706
|
+
result.lines.push(`~ webhook ${label}`);
|
|
22817
22707
|
return result;
|
|
22818
22708
|
};
|
|
22819
22709
|
|
|
22820
22710
|
// src/actions/pull/webhooks/applyWebhooksPull.ts
|
|
22821
|
-
var isVercelWebhook = (events) => Array.isArray(events) && events.length > 0 && events.every((event) => typeof event === "string" && event.startsWith("vercel."));
|
|
22822
22711
|
var statedFrom = ({
|
|
22823
|
-
row,
|
|
22824
22712
|
webhook,
|
|
22825
22713
|
envKey
|
|
22826
22714
|
}) => ({
|
|
22827
|
-
...row,
|
|
22828
22715
|
id: webhook.id,
|
|
22829
|
-
|
|
22716
|
+
env: envKey,
|
|
22717
|
+
url: webhook.url,
|
|
22830
22718
|
events: webhook.events.length > 0 ? webhook.events : undefined,
|
|
22831
22719
|
description: webhook.description,
|
|
22832
22720
|
disabled: webhook.disabled
|
|
@@ -22840,73 +22728,37 @@ var applyWebhooksPull = ({
|
|
|
22840
22728
|
pull,
|
|
22841
22729
|
remote,
|
|
22842
22730
|
stated: stated2,
|
|
22843
|
-
envKey
|
|
22844
|
-
newId = newWebhookId
|
|
22731
|
+
envKey
|
|
22845
22732
|
}) => {
|
|
22846
22733
|
const result = { lines: [], warnings: [], unlocated: [] };
|
|
22847
|
-
const
|
|
22848
|
-
const
|
|
22849
|
-
const
|
|
22850
|
-
const present = new Set;
|
|
22734
|
+
const rows = stated2 ?? [];
|
|
22735
|
+
const inEnv = new Map(rows.filter((row) => row.env === envKey).map((row) => [row.id, row]));
|
|
22736
|
+
const next = rows.filter((row) => row.env !== envKey);
|
|
22851
22737
|
for (const webhook of remote) {
|
|
22852
|
-
|
|
22853
|
-
const sameApp = (row2) => isVercelWebhook(row2.events) === isVercelWebhook(webhook.events);
|
|
22854
|
-
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));
|
|
22855
|
-
if (represented !== undefined) {
|
|
22856
|
-
present.add(represented.id);
|
|
22857
|
-
next.set(represented.id, statedFrom({
|
|
22858
|
-
row: represented,
|
|
22859
|
-
webhook: { ...webhook, id: represented.id },
|
|
22860
|
-
envKey
|
|
22861
|
-
}));
|
|
22862
|
-
merge(result, updateWebhook({
|
|
22863
|
-
pull,
|
|
22864
|
-
webhook: { ...webhook, id: represented.id },
|
|
22865
|
-
stated: represented,
|
|
22866
|
-
envKey
|
|
22867
|
-
}));
|
|
22868
|
-
continue;
|
|
22869
|
-
}
|
|
22870
|
-
const id = newId();
|
|
22871
|
-
const failure2 = appendWebhook({
|
|
22872
|
-
pull,
|
|
22873
|
-
webhook: { ...webhook, id },
|
|
22874
|
-
envKey
|
|
22875
|
-
});
|
|
22876
|
-
if (failure2 === null) {
|
|
22877
|
-
next.set(id, statedFrom({ row: undefined, webhook: { ...webhook, id }, envKey }));
|
|
22878
|
-
result.lines.push(`+ webhook ${id} (made in the dashboard; push adopts it by URL)`);
|
|
22879
|
-
} else
|
|
22880
|
-
result.unlocated.push({ id, action: failure2 });
|
|
22881
|
-
continue;
|
|
22882
|
-
}
|
|
22883
|
-
present.add(webhook.id);
|
|
22884
|
-
const row = statedById.get(webhook.id);
|
|
22885
|
-
next.set(webhook.id, statedFrom({ row, webhook, envKey }));
|
|
22738
|
+
const row = inEnv.get(webhook.id);
|
|
22886
22739
|
if (row !== undefined) {
|
|
22887
22740
|
merge(result, updateWebhook({ pull, webhook, stated: row, envKey }));
|
|
22741
|
+
next.push(statedFrom({ webhook, envKey }));
|
|
22888
22742
|
continue;
|
|
22889
22743
|
}
|
|
22890
22744
|
const failure = appendWebhook({ pull, webhook, envKey });
|
|
22891
|
-
if (failure
|
|
22892
|
-
result.lines.push(`+ webhook ${webhook.id}`);
|
|
22893
|
-
else
|
|
22745
|
+
if (failure !== null) {
|
|
22894
22746
|
result.unlocated.push({ id: webhook.id, action: failure });
|
|
22747
|
+
continue;
|
|
22748
|
+
}
|
|
22749
|
+
result.lines.push(`+ webhook ${webhook.id} (${envKey})`);
|
|
22750
|
+
next.push(statedFrom({ webhook, envKey }));
|
|
22895
22751
|
}
|
|
22896
|
-
|
|
22897
|
-
|
|
22752
|
+
const remoteIds = new Set(remote.map(({ id }) => id));
|
|
22753
|
+
for (const row of inEnv.values()) {
|
|
22754
|
+
if (remoteIds.has(row.id))
|
|
22898
22755
|
continue;
|
|
22899
|
-
const removed =
|
|
22756
|
+
const removed = deleteWebhook({ pull, stated: row, envKey });
|
|
22900
22757
|
merge(result, removed);
|
|
22901
22758
|
if (removed.lines.length === 0)
|
|
22902
|
-
|
|
22903
|
-
const { [envKey]: _removed, ...url } = row.url;
|
|
22904
|
-
if (Object.keys(url).length === 0)
|
|
22905
|
-
next.delete(row.id);
|
|
22906
|
-
else
|
|
22907
|
-
next.set(row.id, { ...row, url });
|
|
22759
|
+
next.push(row);
|
|
22908
22760
|
}
|
|
22909
|
-
return { ...result, stated:
|
|
22761
|
+
return { ...result, stated: next };
|
|
22910
22762
|
};
|
|
22911
22763
|
|
|
22912
22764
|
// src/generated/sandboxName.ts
|
|
@@ -22974,6 +22826,7 @@ var webhookPullEnvs = ({
|
|
|
22974
22826
|
{
|
|
22975
22827
|
keyName,
|
|
22976
22828
|
label,
|
|
22829
|
+
secretKey,
|
|
22977
22830
|
envKey,
|
|
22978
22831
|
listWebhooks: () => listWebhooks({ secretKey })
|
|
22979
22832
|
}
|
|
@@ -22981,8 +22834,28 @@ var webhookPullEnvs = ({
|
|
|
22981
22834
|
});
|
|
22982
22835
|
};
|
|
22983
22836
|
|
|
22837
|
+
// src/actions/pull/webhooks/webhookEnvSkipWarning.ts
|
|
22838
|
+
var skipReason = (error) => {
|
|
22839
|
+
if (error instanceof ForeignOrgKeyError)
|
|
22840
|
+
return "belongs to another org";
|
|
22841
|
+
if (!(error instanceof AutumnApiError))
|
|
22842
|
+
return null;
|
|
22843
|
+
if (error.status === 401 || error.status === 403)
|
|
22844
|
+
return "was rejected";
|
|
22845
|
+
if (error.status !== 404)
|
|
22846
|
+
return null;
|
|
22847
|
+
const { message } = error.body ?? {};
|
|
22848
|
+
return `failed: ${typeof message === "string" ? message : error.status}`;
|
|
22849
|
+
};
|
|
22850
|
+
var webhookEnvSkipWarning = ({
|
|
22851
|
+
env,
|
|
22852
|
+
error
|
|
22853
|
+
}) => {
|
|
22854
|
+
const reason = skipReason(error);
|
|
22855
|
+
return reason === null ? undefined : `⚠ webhooks: skipped ${env.label} (${env.keyName} ${reason})`;
|
|
22856
|
+
};
|
|
22857
|
+
|
|
22984
22858
|
// src/actions/pull/webhooks/readWebhookEnvs.ts
|
|
22985
|
-
var isRejectedKey = (error) => error instanceof AutumnApiError && (error.status === 401 || error.status === 403);
|
|
22986
22859
|
var readWebhookEnvs = async ({
|
|
22987
22860
|
envs
|
|
22988
22861
|
}) => {
|
|
@@ -22994,11 +22867,10 @@ var readWebhookEnvs = async ({
|
|
|
22994
22867
|
]);
|
|
22995
22868
|
return { envKey, list: list2 };
|
|
22996
22869
|
} catch (error) {
|
|
22997
|
-
|
|
22998
|
-
|
|
22999
|
-
if (!isRejectedKey(error))
|
|
22870
|
+
const warning = webhookEnvSkipWarning({ env, error });
|
|
22871
|
+
if (warning === undefined)
|
|
23000
22872
|
throw error;
|
|
23001
|
-
return
|
|
22873
|
+
return warning;
|
|
23002
22874
|
}
|
|
23003
22875
|
}));
|
|
23004
22876
|
return {
|
|
@@ -23095,7 +22967,8 @@ var renderWebhooks = ({
|
|
|
23095
22967
|
if (rows.length === 0)
|
|
23096
22968
|
return null;
|
|
23097
22969
|
const count = webhooks.changes.filter((change) => change.action !== "unmanaged").length;
|
|
23098
|
-
|
|
22970
|
+
const heading = webhooks.env === undefined ? `Webhooks (${count})` : `Webhooks · ${webhooks.env} (${count})`;
|
|
22971
|
+
return [chalk4.bold(heading), ...rows].join(`
|
|
23099
22972
|
`);
|
|
23100
22973
|
};
|
|
23101
22974
|
|
|
@@ -23522,8 +23395,10 @@ var renderPreview = ({
|
|
|
23522
23395
|
const referralPrograms = (preview.referralPrograms ?? []).filter(rowHasWork);
|
|
23523
23396
|
const migrations = preview.migrations ?? [];
|
|
23524
23397
|
const settings = settingChanges(preview.settings);
|
|
23525
|
-
const webhooks = preview.webhooks
|
|
23526
|
-
|
|
23398
|
+
const webhooks = (preview.webhooks ?? []).map((lane) => renderWebhooks({ webhooks: lane })).filter((block) => block !== null);
|
|
23399
|
+
const productionWebhooks = preview.productionWebhooks ?? [];
|
|
23400
|
+
const productionHint = productionWebhooks.length > 0 ? chalk5.yellow(`Production webhooks differ from your config (${productionWebhooks.join(", ")}). Run atmn push -p to update production.`) : preview.productionWebhooksUnchecked === true ? chalk5.yellow("Production webhooks weren't checked, so this push can't say they match your config.") : null;
|
|
23401
|
+
if (features.length === 0 && plans.length === 0 && rewards.length === 0 && referralPrograms.length === 0 && settings.length === 0 && webhooks.length === 0 && productionHint === null) {
|
|
23527
23402
|
return chalk5.dim("No changes. Your catalog matches your config.");
|
|
23528
23403
|
}
|
|
23529
23404
|
const sections = [];
|
|
@@ -23592,8 +23467,9 @@ var renderPreview = ({
|
|
|
23592
23467
|
].join(`
|
|
23593
23468
|
`));
|
|
23594
23469
|
}
|
|
23595
|
-
|
|
23596
|
-
|
|
23470
|
+
sections.push(...webhooks);
|
|
23471
|
+
if (productionHint !== null)
|
|
23472
|
+
sections.push(productionHint);
|
|
23597
23473
|
if (migrations.length > 0) {
|
|
23598
23474
|
sections.push(renderPlannedMigrations({ migrations, plans }));
|
|
23599
23475
|
}
|
|
@@ -23603,7 +23479,7 @@ var renderPreview = ({
|
|
|
23603
23479
|
};
|
|
23604
23480
|
var previewIsEmpty = ({
|
|
23605
23481
|
preview
|
|
23606
|
-
}) => !(preview.features ?? []).some(rowHasWork) && !(preview.plans ?? []).some(rowHasWork) && !(preview.rewards ?? []).some(rowHasWork) && !(preview.referralPrograms ?? []).some(rowHasWork) && !settingsHaveWork({ settings: preview.settings }) && !webhooksHaveWork({ webhooks
|
|
23482
|
+
}) => !(preview.features ?? []).some(rowHasWork) && !(preview.plans ?? []).some(rowHasWork) && !(preview.rewards ?? []).some(rowHasWork) && !(preview.referralPrograms ?? []).some(rowHasWork) && !settingsHaveWork({ settings: preview.settings }) && !(preview.webhooks ?? []).some((webhooks) => webhooksHaveWork({ webhooks }));
|
|
23607
23483
|
|
|
23608
23484
|
// src/actions/push/backfillInternalIds.ts
|
|
23609
23485
|
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
|
|
@@ -24059,18 +23935,35 @@ var withScopeHint = ({
|
|
|
24059
23935
|
hint: hint2
|
|
24060
23936
|
}) => error instanceof AutumnApiError && error.message.includes(SCOPE_REFUSAL) ? new Error(hint2, { cause: error }) : error;
|
|
24061
23937
|
|
|
23938
|
+
// src/actions/webhooks/webhookEnvFailures.ts
|
|
23939
|
+
var throwWebhookEnvFailures = ({
|
|
23940
|
+
failures,
|
|
23941
|
+
envCount,
|
|
23942
|
+
warnings = []
|
|
23943
|
+
}) => {
|
|
23944
|
+
if (failures.length === 0)
|
|
23945
|
+
return;
|
|
23946
|
+
throw new Error([
|
|
23947
|
+
...failures.map(({ env, message }) => envCount > 1 ? message.replace(/^/gm, `${env}: `) : message),
|
|
23948
|
+
...warnings
|
|
23949
|
+
].join(`
|
|
23950
|
+
`));
|
|
23951
|
+
};
|
|
23952
|
+
var messageOf2 = (error) => error instanceof Error ? error.message : String(error);
|
|
23953
|
+
|
|
24062
23954
|
// src/actions/webhooks/writeWebhookSecrets.ts
|
|
24063
23955
|
import { existsSync as existsSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
|
|
24064
23956
|
import { dirname as dirname11, join as join13, relative as relative6 } from "node:path";
|
|
24065
23957
|
|
|
24066
23958
|
// src/actions/webhooks/webhookSecretName.ts
|
|
24067
23959
|
var envNameSegment = (value) => value.toUpperCase().replace(/-/g, "_");
|
|
23960
|
+
var envKeySegment = (envKey) => envKey.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
24068
23961
|
var webhookSecretName = ({
|
|
24069
23962
|
id,
|
|
24070
|
-
|
|
23963
|
+
envKey
|
|
24071
23964
|
}) => {
|
|
24072
|
-
const
|
|
24073
|
-
return `AUTUMN_WEBHOOK_${envNameSegment(id)}${
|
|
23965
|
+
const suffix = envKey === undefined ? "" : `_${envKeySegment(envKey)}`;
|
|
23966
|
+
return `AUTUMN_WEBHOOK_${envNameSegment(id)}${suffix}_SECRET`;
|
|
24074
23967
|
};
|
|
24075
23968
|
|
|
24076
23969
|
// src/actions/webhooks/writeWebhookSecrets.ts
|
|
@@ -24087,8 +23980,11 @@ var writeWebhookSecrets = async ({
|
|
|
24087
23980
|
}) => {
|
|
24088
23981
|
if (secrets.length === 0)
|
|
24089
23982
|
return [];
|
|
24090
|
-
const
|
|
24091
|
-
const values = Object.fromEntries(secrets.map(({ id, secret }) => [
|
|
23983
|
+
const envKey = env.live ? undefined : env.key;
|
|
23984
|
+
const values = Object.fromEntries(secrets.map(({ id, secret }) => [
|
|
23985
|
+
webhookSecretName({ id, envKey }),
|
|
23986
|
+
secret
|
|
23987
|
+
]));
|
|
24092
23988
|
let path;
|
|
24093
23989
|
if (env.live) {
|
|
24094
23990
|
path = prodEnvPath({ envDirs });
|
|
@@ -24102,16 +23998,13 @@ var writeWebhookSecrets = async ({
|
|
|
24102
23998
|
};
|
|
24103
23999
|
|
|
24104
24000
|
// src/actions/webhooks/applyWebhooks.ts
|
|
24105
|
-
var
|
|
24106
|
-
client,
|
|
24001
|
+
var applyLane = async ({
|
|
24107
24002
|
lane,
|
|
24108
24003
|
envDirs,
|
|
24109
24004
|
cwd,
|
|
24110
24005
|
write
|
|
24111
24006
|
}) => {
|
|
24112
|
-
|
|
24113
|
-
return;
|
|
24114
|
-
const result = await client.syncWebhooks(lane.body);
|
|
24007
|
+
const result = await lane.client.syncWebhooks(lane.body);
|
|
24115
24008
|
const saved = await writeWebhookSecrets({
|
|
24116
24009
|
secrets: result.secrets,
|
|
24117
24010
|
env: lane.env,
|
|
@@ -24124,54 +24017,168 @@ var applyWebhooks = async ({
|
|
|
24124
24017
|
]);
|
|
24125
24018
|
const adopted = lane.preview.changes.filter((change) => change.action === "adopt" && !failed.has(change.id)).map((change) => `Adopted ${change.id} (existing dashboard webhook); its signing secret is unchanged, nothing written`);
|
|
24126
24019
|
write(`
|
|
24127
|
-
Applied webhooks.
|
|
24020
|
+
Applied webhooks to ${lane.env.key}.
|
|
24128
24021
|
${[...adopted, ...saved].map((line2) => `${line2}
|
|
24129
24022
|
`).join("")}`);
|
|
24130
24023
|
if (result.errors.length > 0)
|
|
24131
24024
|
throw new Error(result.errors.map(({ id, message }) => `${id}: ${message}`).join(`
|
|
24132
24025
|
`));
|
|
24133
24026
|
};
|
|
24027
|
+
var applyWebhooks = async ({
|
|
24028
|
+
lanes,
|
|
24029
|
+
envDirs,
|
|
24030
|
+
cwd,
|
|
24031
|
+
write
|
|
24032
|
+
}) => {
|
|
24033
|
+
const withWork = lanes.filter((lane) => webhooksHaveWork({ webhooks: lane.preview }));
|
|
24034
|
+
const results = await Promise.allSettled(withWork.map((lane) => applyLane({ lane, envDirs, cwd, write })));
|
|
24035
|
+
const failures = results.flatMap((result, index) => result.status === "rejected" ? [
|
|
24036
|
+
{
|
|
24037
|
+
env: withWork[index]?.env.key ?? "",
|
|
24038
|
+
message: messageOf2(result.reason)
|
|
24039
|
+
}
|
|
24040
|
+
] : []);
|
|
24041
|
+
throwWebhookEnvFailures({ failures, envCount: withWork.length });
|
|
24042
|
+
};
|
|
24134
24043
|
|
|
24135
24044
|
// src/actions/webhooks/resolveWebhooksForEnv.ts
|
|
24136
24045
|
var resolveWebhooksForEnv = ({
|
|
24137
24046
|
rows,
|
|
24138
24047
|
envKey
|
|
24139
24048
|
}) => {
|
|
24140
|
-
const {
|
|
24141
|
-
|
|
24142
|
-
for (const row of rows) {
|
|
24143
|
-
const values = envKeyed.map((field) => row[field]?.[envKey]);
|
|
24144
|
-
if (values.some((value) => typeof value !== "string"))
|
|
24145
|
-
continue;
|
|
24146
|
-
webhooks.push({
|
|
24147
|
-
...row,
|
|
24148
|
-
...Object.fromEntries(envKeyed.map((field, index) => [field, values[index]]))
|
|
24149
|
-
});
|
|
24150
|
-
}
|
|
24151
|
-
return webhooks;
|
|
24049
|
+
const { envField } = SYNCED_LISTS.webhooks;
|
|
24050
|
+
return rows.filter((row) => row[envField] === envKey).map(({ [envField]: _env, ...row }) => row);
|
|
24152
24051
|
};
|
|
24153
24052
|
|
|
24154
|
-
// src/actions/webhooks/
|
|
24155
|
-
var
|
|
24156
|
-
|
|
24053
|
+
// src/actions/webhooks/previewWebhookEnvs.ts
|
|
24054
|
+
var isLive = (env) => env.keyName === "AUTUMN_PROD_SECRET_KEY";
|
|
24055
|
+
var assertDistinctSecretNames = ({
|
|
24056
|
+
lanes
|
|
24057
|
+
}) => {
|
|
24058
|
+
const secretName = ({ lane, id }) => webhookSecretName({
|
|
24059
|
+
id,
|
|
24060
|
+
...lane.env.live ? {} : { envKey: lane.env.key }
|
|
24061
|
+
});
|
|
24062
|
+
const clashes = new Map;
|
|
24063
|
+
for (const lane of lanes) {
|
|
24064
|
+
for (const change of lane.preview.changes) {
|
|
24065
|
+
if (change.action !== "create")
|
|
24066
|
+
continue;
|
|
24067
|
+
const name = secretName({ lane, id: change.id });
|
|
24068
|
+
for (const other of lanes) {
|
|
24069
|
+
if (other === lane)
|
|
24070
|
+
continue;
|
|
24071
|
+
const shares = other.body.webhooks.some((webhook) => secretName({ lane: other, id: webhook.id }) === name);
|
|
24072
|
+
if (!shares)
|
|
24073
|
+
continue;
|
|
24074
|
+
const [first, second] = [other.env.key, lane.env.key].sort();
|
|
24075
|
+
clashes.set(`${first}|${second}|${name}`, `${first} and ${second} would both save ${change.id}'s signing secret as ${name}. Rename one of those sandboxes so their names differ by more than punctuation.`);
|
|
24076
|
+
}
|
|
24077
|
+
}
|
|
24078
|
+
}
|
|
24079
|
+
if (clashes.size > 0)
|
|
24080
|
+
throw new Error([...clashes.values()].join(`
|
|
24081
|
+
`));
|
|
24082
|
+
};
|
|
24083
|
+
var previewEnv = async ({
|
|
24084
|
+
env,
|
|
24085
|
+
rows,
|
|
24086
|
+
clientFor
|
|
24087
|
+
}) => {
|
|
24088
|
+
try {
|
|
24089
|
+
const key = await env.envKey();
|
|
24090
|
+
const webhooks = resolveWebhooksForEnv({ rows, envKey: key });
|
|
24091
|
+
if (webhooks.length === 0)
|
|
24092
|
+
return;
|
|
24093
|
+
const client = clientFor({ secretKey: env.secretKey });
|
|
24094
|
+
const body = { webhooks };
|
|
24095
|
+
const { changes, errors } = await client.previewSyncWebhooks(body);
|
|
24096
|
+
return {
|
|
24097
|
+
lane: {
|
|
24098
|
+
preview: { env: key, changes },
|
|
24099
|
+
body,
|
|
24100
|
+
env: { key, live: isLive(env) },
|
|
24101
|
+
client
|
|
24102
|
+
},
|
|
24103
|
+
errors
|
|
24104
|
+
};
|
|
24105
|
+
} catch (error) {
|
|
24106
|
+
const skipped = webhookEnvSkipWarning({ env, error });
|
|
24107
|
+
if (skipped === undefined)
|
|
24108
|
+
throw error;
|
|
24109
|
+
return { skipped };
|
|
24110
|
+
}
|
|
24111
|
+
};
|
|
24112
|
+
var previewWebhookEnvs = async ({
|
|
24157
24113
|
rows,
|
|
24158
|
-
|
|
24114
|
+
envs,
|
|
24115
|
+
prod,
|
|
24116
|
+
clientFor
|
|
24159
24117
|
}) => {
|
|
24160
24118
|
if (rows === undefined)
|
|
24161
24119
|
return;
|
|
24162
|
-
if (
|
|
24163
|
-
throw new Error("webhooks need the
|
|
24164
|
-
const
|
|
24165
|
-
const
|
|
24166
|
-
|
|
24167
|
-
|
|
24120
|
+
if (envs === undefined || clientFor === undefined)
|
|
24121
|
+
throw new Error("webhooks need the env keys to pick each url.");
|
|
24122
|
+
const all = envs();
|
|
24123
|
+
const synced = all.filter((env) => isLive(env) === prod);
|
|
24124
|
+
const probed = prod ? [] : all.filter(isLive);
|
|
24125
|
+
const [syncedResults, probedResults] = await Promise.all([
|
|
24126
|
+
Promise.allSettled(synced.map((env) => previewEnv({ env, rows, clientFor }))),
|
|
24127
|
+
Promise.allSettled(probed.map((env) => previewEnv({ env, rows, clientFor })))
|
|
24128
|
+
]);
|
|
24129
|
+
const lanes = [];
|
|
24130
|
+
const skipped = [];
|
|
24131
|
+
probedResults.forEach((result, index) => {
|
|
24132
|
+
if (result.status === "rejected")
|
|
24133
|
+
skipped.push(`⚠ webhooks: couldn't check ${probed[index]?.label ?? "live"} for changes (${messageOf2(result.reason)})`);
|
|
24168
24134
|
});
|
|
24169
|
-
const
|
|
24170
|
-
const
|
|
24171
|
-
|
|
24172
|
-
|
|
24173
|
-
|
|
24174
|
-
|
|
24135
|
+
const productionUnchecked = probedResults.some((result) => result.status === "rejected" || result.value !== undefined && ("skipped" in result.value));
|
|
24136
|
+
const failures = [];
|
|
24137
|
+
syncedResults.forEach((result, index) => {
|
|
24138
|
+
const label = synced[index]?.label ?? "";
|
|
24139
|
+
if (result.status === "rejected") {
|
|
24140
|
+
failures.push({ env: label, message: messageOf2(result.reason) });
|
|
24141
|
+
return;
|
|
24142
|
+
}
|
|
24143
|
+
const preview = result.value;
|
|
24144
|
+
if (preview === undefined)
|
|
24145
|
+
return;
|
|
24146
|
+
if ("skipped" in preview) {
|
|
24147
|
+
skipped.push(preview.skipped);
|
|
24148
|
+
return;
|
|
24149
|
+
}
|
|
24150
|
+
lanes.push(preview.lane);
|
|
24151
|
+
if (preview.errors.length > 0)
|
|
24152
|
+
failures.push({
|
|
24153
|
+
env: preview.lane.env.key,
|
|
24154
|
+
message: preview.errors.map(({ id, message }) => `${id}: ${message}`).join(`
|
|
24155
|
+
`)
|
|
24156
|
+
});
|
|
24157
|
+
});
|
|
24158
|
+
throwWebhookEnvFailures({
|
|
24159
|
+
failures,
|
|
24160
|
+
envCount: synced.length,
|
|
24161
|
+
warnings: skipped
|
|
24162
|
+
});
|
|
24163
|
+
assertDistinctSecretNames({ lanes });
|
|
24164
|
+
const productionDiffers = [];
|
|
24165
|
+
for (const result of probedResults) {
|
|
24166
|
+
if (result.status === "rejected")
|
|
24167
|
+
continue;
|
|
24168
|
+
const preview = result.value;
|
|
24169
|
+
if (preview === undefined)
|
|
24170
|
+
continue;
|
|
24171
|
+
if ("skipped" in preview) {
|
|
24172
|
+
skipped.push(preview.skipped);
|
|
24173
|
+
continue;
|
|
24174
|
+
}
|
|
24175
|
+
const changed = preview.lane.preview.changes.filter((change) => change.action !== "unmanaged");
|
|
24176
|
+
productionDiffers.push(...new Set([
|
|
24177
|
+
...changed.map((change) => change.id),
|
|
24178
|
+
...preview.errors.map((error) => error.id)
|
|
24179
|
+
]));
|
|
24180
|
+
}
|
|
24181
|
+
return { lanes, productionDiffers, productionUnchecked, skipped };
|
|
24175
24182
|
};
|
|
24176
24183
|
|
|
24177
24184
|
// src/actions/push.ts
|
|
@@ -24337,7 +24344,9 @@ var runPush = async ({
|
|
|
24337
24344
|
dryRun = false,
|
|
24338
24345
|
write = (text) => process.stdout.write(text),
|
|
24339
24346
|
migrationLinkBase,
|
|
24340
|
-
|
|
24347
|
+
prod = false,
|
|
24348
|
+
webhookEnvs,
|
|
24349
|
+
webhookClientFor
|
|
24341
24350
|
}) => {
|
|
24342
24351
|
const project = resolveProject({ cwd, configFlag });
|
|
24343
24352
|
const dirs = configSearchDirs({ cwd, configPath: configFlag });
|
|
@@ -24360,10 +24369,11 @@ var runPush = async ({
|
|
|
24360
24369
|
const previews = await settleLanes({
|
|
24361
24370
|
settings: previewSettings({ client, body: settingsBody }),
|
|
24362
24371
|
catalog: client.previewUpdate(wire),
|
|
24363
|
-
webhooks:
|
|
24364
|
-
client,
|
|
24372
|
+
webhooks: previewWebhookEnvs({
|
|
24365
24373
|
rows: lists.webhooks,
|
|
24366
|
-
|
|
24374
|
+
envs: webhookEnvs,
|
|
24375
|
+
prod,
|
|
24376
|
+
clientFor: webhookClientFor
|
|
24367
24377
|
})
|
|
24368
24378
|
});
|
|
24369
24379
|
const { settings, webhooks } = previews.values;
|
|
@@ -24375,8 +24385,17 @@ var runPush = async ({
|
|
|
24375
24385
|
const preview = {
|
|
24376
24386
|
...catalogPreview,
|
|
24377
24387
|
settings,
|
|
24378
|
-
...webhooks === undefined ? {} : {
|
|
24388
|
+
...webhooks === undefined ? {} : {
|
|
24389
|
+
webhooks: webhooks.lanes.map((lane) => lane.preview),
|
|
24390
|
+
productionWebhooks: webhooks.productionDiffers,
|
|
24391
|
+
productionWebhooksUnchecked: webhooks.productionUnchecked
|
|
24392
|
+
}
|
|
24379
24393
|
};
|
|
24394
|
+
if (webhooks !== undefined && webhooks.skipped.length > 0)
|
|
24395
|
+
write(`${webhooks.skipped.join(`
|
|
24396
|
+
`)}
|
|
24397
|
+
|
|
24398
|
+
`);
|
|
24380
24399
|
write(`${renderPreview({ preview, migrationLinkBase })}
|
|
24381
24400
|
`);
|
|
24382
24401
|
if (catalogDeferral !== undefined)
|
|
@@ -24413,8 +24432,7 @@ ${renderCatalogDeferral({ error: catalogDeferral.error })}
|
|
|
24413
24432
|
const applied = await settleLanes({
|
|
24414
24433
|
catalog: settled === undefined ? settingsStep().then(catalogStep) : catalogStep(settled),
|
|
24415
24434
|
webhooks: applyWebhooks({
|
|
24416
|
-
|
|
24417
|
-
lane: webhooks,
|
|
24435
|
+
lanes: webhooks?.lanes ?? [],
|
|
24418
24436
|
envDirs: project.envDirs,
|
|
24419
24437
|
cwd,
|
|
24420
24438
|
write
|
|
@@ -25163,28 +25181,6 @@ var runSandboxUse = async ({
|
|
|
25163
25181
|
return result;
|
|
25164
25182
|
};
|
|
25165
25183
|
|
|
25166
|
-
// src/actions/webhooks/resolveWebhookEnv.ts
|
|
25167
|
-
var memoized = (fetchOrgInfo2) => {
|
|
25168
|
-
let pending;
|
|
25169
|
-
return () => {
|
|
25170
|
-
pending ??= fetchOrgInfo2();
|
|
25171
|
-
return pending;
|
|
25172
|
-
};
|
|
25173
|
-
};
|
|
25174
|
-
var resolveWebhookEnv = async ({
|
|
25175
|
-
target,
|
|
25176
|
-
prod,
|
|
25177
|
-
fetchOrgInfo: fetchOrgInfo2
|
|
25178
|
-
}) => {
|
|
25179
|
-
const orgInfo = memoized(fetchOrgInfo2);
|
|
25180
|
-
const orgId = async () => (await orgInfo()).id;
|
|
25181
|
-
if (prod)
|
|
25182
|
-
return { key: "live", live: true, orgId };
|
|
25183
|
-
if (target.sandboxId === undefined)
|
|
25184
|
-
return { key: "sandbox", live: false, orgId };
|
|
25185
|
-
return { key: sandboxSlug((await orgInfo()).name), live: false, orgId };
|
|
25186
|
-
};
|
|
25187
|
-
|
|
25188
25184
|
// src/cli.ts
|
|
25189
25185
|
var normalizeVersionFlag = ({ argv }) => argv.map((token) => token === "-v" ? "--version" : token);
|
|
25190
25186
|
var withTargetFlags = (program) => program.option("-p, --prod", "target production instead of sandbox").option("--sandbox <sandboxId>", "target a specific sandbox").option("-l, --local", "target a local server (default port 8080)").option("--port <port>", "port of a local server (implies --local)").option("-b, --base-url <url>", "send to this URL instead").option("--client-id <id>", "OAuth client id the CLI identifies as").option("-c, --config <path>", "autumn.config.ts, or the folder holding it (default: found from cwd or the root package.json)").option("--headless", "never prompt: print what a command needs and stop (default when not a TTY)");
|
|
@@ -25276,13 +25272,15 @@ var pinnedSandboxName = async ({
|
|
|
25276
25272
|
return {};
|
|
25277
25273
|
}
|
|
25278
25274
|
};
|
|
25279
|
-
var
|
|
25280
|
-
|
|
25281
|
-
|
|
25282
|
-
|
|
25283
|
-
|
|
25284
|
-
|
|
25285
|
-
|
|
25275
|
+
var webhookClientFor = ({ target }) => ({ secretKey }) => createClient({
|
|
25276
|
+
secretKey,
|
|
25277
|
+
fetch: autumnFetch,
|
|
25278
|
+
...target.baseUrl ? { baseUrl: target.baseUrl } : {}
|
|
25279
|
+
});
|
|
25280
|
+
var webhookEnvsFor = ({ target }) => () => webhookPullEnvs({
|
|
25281
|
+
targetKeyName: target.secretKeyName,
|
|
25282
|
+
listWebhooks: ({ secretKey }) => webhookClientFor({ target })({ secretKey }).listWebhooks({}),
|
|
25283
|
+
fetchOrgInfo: ({ secretKey }) => fetchOrgInfo({ baseUrl: targetBaseUrl({ target }), secretKey })
|
|
25286
25284
|
});
|
|
25287
25285
|
var mainOrgInfo = ({ target }) => {
|
|
25288
25286
|
const main = managementTarget({ target });
|
|
@@ -25382,7 +25380,9 @@ Linking a keyless org to an account:
|
|
|
25382
25380
|
client: clientFor({ target }),
|
|
25383
25381
|
configPath: configFlagOf({ command }),
|
|
25384
25382
|
dryRun: !apply,
|
|
25385
|
-
|
|
25383
|
+
prod: command.optsWithGlobals().prod === true,
|
|
25384
|
+
webhookEnvs: webhookEnvsFor({ target }),
|
|
25385
|
+
webhookClientFor: webhookClientFor({ target })
|
|
25386
25386
|
});
|
|
25387
25387
|
process.exitCode = pushExitCode({ apply, result });
|
|
25388
25388
|
if (!apply && !previewIsEmpty({ preview: result.preview }))
|
|
@@ -25432,15 +25432,7 @@ Nothing applied. Re-run with --yes to apply these changes.
|
|
|
25432
25432
|
overwrite: options.overwrite === true,
|
|
25433
25433
|
yes: options.yes === true,
|
|
25434
25434
|
prompter: prompterFor({ command }),
|
|
25435
|
-
webhookEnvs: (
|
|
25436
|
-
targetKeyName: target.secretKeyName,
|
|
25437
|
-
listWebhooks: ({ secretKey }) => createClient({
|
|
25438
|
-
secretKey,
|
|
25439
|
-
fetch: autumnFetch,
|
|
25440
|
-
...target.baseUrl ? { baseUrl: target.baseUrl } : {}
|
|
25441
|
-
}).listWebhooks({}),
|
|
25442
|
-
fetchOrgInfo: ({ secretKey }) => fetchOrgInfo({ baseUrl: targetBaseUrl({ target }), secretKey })
|
|
25443
|
-
})
|
|
25435
|
+
webhookEnvs: webhookEnvsFor({ target })
|
|
25444
25436
|
});
|
|
25445
25437
|
writeStaleSkillsHint({ command });
|
|
25446
25438
|
});
|