atmn 2.0.21 → 2.0.23

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
@@ -2960,7 +2960,7 @@ var API_ROUTES = [
2960
2960
  name: "net_terms_days",
2961
2961
  type: "number",
2962
2962
  required: false,
2963
- description: "Number of days the customer has to pay the replacement invoice. Defaults to the original invoice's due date; required when that date has already passed."
2963
+ description: "Number of days the customer has to pay the replacement invoice. Defaults to the original invoice's due date; required when that date has already passed. A card-charged invoice has no due date and its replacement is charged immediately; setting this makes the replacement a send-invoice one instead."
2964
2964
  },
2965
2965
  {
2966
2966
  name: "preview",
@@ -3747,7 +3747,7 @@ var LINT_RULES = {
3747
3747
  label: "feature",
3748
3748
  idField: "featureId",
3749
3749
  required: ["featureId", "name", "type"],
3750
- hidden: ["display", "newFeatureId"],
3750
+ hidden: ["display", "eventNames", "newFeatureId"],
3751
3751
  fields: {
3752
3752
  type: {
3753
3753
  enum: ["boolean", "metered", "credit_system", "ai_credit_system"]
@@ -5766,9 +5766,6 @@ var LINT_RULES = {
5766
5766
  name: {
5767
5767
  minLength: 1
5768
5768
  },
5769
- planIds: {
5770
- minItems: 1
5771
- },
5772
5769
  internalId: {
5773
5770
  minLength: 1
5774
5771
  },
@@ -5780,6 +5777,11 @@ var LINT_RULES = {
5780
5777
  }
5781
5778
  },
5782
5779
  rules: [
5780
+ {
5781
+ kind: "nonEmpty",
5782
+ field: "planIds",
5783
+ because: "A coupon must apply to at least one plan: list the plans it discounts, set planIds: null to apply it to every plan, or remove the coupon from your config."
5784
+ },
5783
5785
  {
5784
5786
  kind: "exists",
5785
5787
  field: "planIds",
@@ -6092,6 +6094,12 @@ var entryRuleFailures = ({
6092
6094
  const missing = (Array.isArray(value) ? value : [value]).filter((wanted) => !names(wanted));
6093
6095
  return missing.map((wanted) => `${rule.field} ${show(wanted)} is not in ${rule.in}. ${rule.because}`);
6094
6096
  }
6097
+ case "nonEmpty": {
6098
+ const value = entry[rule.field];
6099
+ if (!Array.isArray(value) || value.length > 0)
6100
+ return [];
6101
+ return [`${rule.field} is empty. ${rule.because}`];
6102
+ }
6095
6103
  case "compare": {
6096
6104
  const a = entry[rule.field];
6097
6105
  const b = entry[rule.than];
@@ -6785,7 +6793,7 @@ var createClient = (options) => ({
6785
6793
  });
6786
6794
 
6787
6795
  // src/version.ts
6788
- var version = "2.0.21";
6796
+ var version = "2.0.23";
6789
6797
 
6790
6798
  // src/http/autumnFetch.ts
6791
6799
  var runtime = typeof Bun !== "undefined" ? `bun ${Bun.version}` : `node ${process.versions.node}`;
@@ -7733,7 +7741,6 @@ var COLLECTIONS = {
7733
7741
  "modelMarkups",
7734
7742
  "defaultMarkup",
7735
7743
  "providerMarkups",
7736
- "eventNames",
7737
7744
  "featureId",
7738
7745
  "internalId",
7739
7746
  "archived",
@@ -7781,7 +7788,6 @@ var COLLECTIONS = {
7781
7788
  "creditSchema.tiers.creditCost",
7782
7789
  "creditSchema.tiers.to",
7783
7790
  "defaultMarkup",
7784
- "eventNames",
7785
7791
  "featureId",
7786
7792
  "internalId",
7787
7793
  "modelMarkups",
@@ -9029,16 +9035,16 @@ import { dirname as dirname4, join as join7 } from "node:path";
9029
9035
  import chalk3 from "chalk";
9030
9036
 
9031
9037
  // src/generated/skills.ts
9032
- var SKILLS_VERSION = "2.0.21";
9038
+ var SKILLS_VERSION = "2.0.23";
9033
9039
  var SKILLS = [
9034
9040
  {
9035
9041
  name: "autumn-setup",
9036
9042
  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.",
9037
- version: "2.0.21",
9043
+ version: "2.0.23",
9038
9044
  markdown: `---
9039
9045
  name: autumn-setup
9040
9046
  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.
9041
- version: 2.0.21
9047
+ version: 2.0.23
9042
9048
  ---
9043
9049
 
9044
9050
  # Setup
@@ -9196,11 +9202,11 @@ Then stop. Don't keep building, don't tour the dashboard, don't deploy anything.
9196
9202
  {
9197
9203
  name: "autumn-catalog",
9198
9204
  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.",
9199
- version: "2.0.21",
9205
+ version: "2.0.23",
9200
9206
  markdown: `---
9201
9207
  name: autumn-catalog
9202
9208
  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.
9203
- version: 2.0.21
9209
+ version: 2.0.23
9204
9210
  ---
9205
9211
 
9206
9212
  # Catalog
@@ -13082,11 +13088,11 @@ Pitch: "Pro $20/mo or $200/yr — 1,000 messages per month either way."
13082
13088
  {
13083
13089
  name: "autumn-integrate",
13084
13090
  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.",
13085
- version: "2.0.21",
13091
+ version: "2.0.23",
13086
13092
  markdown: `---
13087
13093
  name: autumn-integrate
13088
13094
  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.
13089
- version: 2.0.21
13095
+ version: 2.0.23
13090
13096
  ---
13091
13097
 
13092
13098
  # Integrate
@@ -17684,11 +17690,11 @@ Monitor your logs for this message. If you see it, check [status.useautumn.com](
17684
17690
  {
17685
17691
  name: "autumn-concepts",
17686
17692
  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.",
17687
- version: "2.0.21",
17693
+ version: "2.0.23",
17688
17694
  markdown: `---
17689
17695
  name: autumn-concepts
17690
17696
  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.
17691
- version: 2.0.21
17697
+ version: 2.0.23
17692
17698
  ---
17693
17699
 
17694
17700
  # Concepts
@@ -17786,7 +17792,7 @@ For reasoning about licenses — seat plans a parent plan hands out, seat pools,
17786
17792
  references: [
17787
17793
  {
17788
17794
  path: "references/feature.md",
17789
- contents: "### Feature\n\n- Feature is the atomic thing Autumn gates, tracks, or bills.\n- `id` is used in plan items, check/track calls, balances, and flags.\n- `name` and optional `display` labels are for dashboard and billing UI.\n\n</intro>\n\n<relationships>\n\n- `Plan Item -> Feature`: defines how a plan grants or bills the feature.\n- `Balance -> Feature`: runtime state for metered features.\n- `Flag -> Feature`: runtime access for boolean features.\n\n</relationships>\n\n<types>\n\n- `boolean`: on/off access, exposed as flags.\n- `metered`, `consumable: true`: usage is spent and can reset, e.g. API calls or AI messages.\n- `metered`, `consumable: false`: persistent quantity, e.g. seats or storage.\n- `credit_system`: user-defined currency with credit costs for metered consumable features.\n- `ai_credit_system`: a monetary (dollar) balance for AI/LLM token usage, priced from Models.dev model pricing + a configured markup.\n\n</types>\n\n<credit-systems>\n\n- Classic `credit_system`: one shared balance for several metered features; `credit_schema` maps each `metered_feature_id` to a `credit_cost`. Track via the underlying `feature_id`.\n- A schema entry's cost can be flat (`credit_cost`) or a graduated rate card: `tier_behavior: \"graduated\"` with tiers, so an action costs fewer credits at higher volume. Useful for enterprise credit deals.\n- `invoice_credit`: credits that appear as line items on the invoice; requires a price of exactly one currency unit per credit. Activation is currently gated — confirm before promising it.\n- Direct balances always drain before credit-system balances, whatever their intervals.\n- `ai_credit_system`: a monetary balance (units = dollars) for AI/LLM token usage; no `credit_schema`. Cost = Models.dev model pricing + markup.\n - Markups, low to high priority: `default_markup` (global %), `provider_markups` (keyed by the model id's provider prefix), `model_markups` (per model). No markup = Models.dev base cost; `-100` = free (recorded, not deducted).\n - Model ids are `provider/model` (e.g. `anthropic/claude-opus-4-5`, `openrouter/anthropic/...`, `custom/...`). Standard models auto-price from Models.dev; `custom/...` models must set `input_cost`/`output_cost` ($/M tokens) and bill input/output only.\n - Track usage with `trackTokens` (modelId + token counts); Autumn converts to dollars and deducts.\n\n</credit-systems>\n\n<feature-rules>\n\n- Do not create duplicate features for the same resource; use Plan Items to vary allowance, interval, package, or price.\n- Example: `tokens` should be one feature, not separate `monthly_tokens` and `one_time_tokens` features.\n\n</feature-rules>\n\n<additional>\n\n- `event_names`: optional aliases so one `track` request can target usage for multiple features.\n- `credit_schema`: classic `credit_system` only (not `ai_credit_system`); maps `metered_feature_id` to `credit_cost`.\n- `archived`: deprecated config; may still exist in grandfathered plans or subscriptions.\n- Legacy pricing-agent wording: `single_use` means metered consumable; `continuous_use` means metered non-consumable.\n\n</additional>\n\n<useful-docs>\n\n- Concepts overview: https://docs.useautumn.com/documentation/concepts/overview\n- Features concept: https://docs.useautumn.com/documentation/concepts/features\n- Credit systems: https://docs.useautumn.com/documentation/modelling-pricing/credit-systems\n\n</useful-docs>"
17795
+ contents: "### Feature\n\n- Feature is the atomic thing Autumn gates, tracks, or bills.\n- `id` is used in plan items, check/track calls, balances, and flags.\n- `name` and optional `display` labels are for dashboard and billing UI.\n\n</intro>\n\n<relationships>\n\n- `Plan Item -> Feature`: defines how a plan grants or bills the feature.\n- `Balance -> Feature`: runtime state for metered features.\n- `Flag -> Feature`: runtime access for boolean features.\n\n</relationships>\n\n<types>\n\n- `boolean`: on/off access, exposed as flags.\n- `metered`, `consumable: true`: usage is spent and can reset, e.g. API calls or AI messages.\n- `metered`, `consumable: false`: persistent quantity, e.g. seats or storage.\n- `credit_system`: user-defined currency with credit costs for metered consumable features.\n- `ai_credit_system`: a monetary (dollar) balance for AI/LLM token usage, priced from Models.dev model pricing + a configured markup.\n\n</types>\n\n<credit-systems>\n\n- Classic `credit_system`: one shared balance for several metered features; `credit_schema` maps each `metered_feature_id` to a `credit_cost`. Track via the underlying `feature_id`.\n- A schema entry's cost can be flat (`credit_cost`) or a graduated rate card: `tier_behavior: \"graduated\"` with tiers, so an action costs fewer credits at higher volume. Useful for enterprise credit deals.\n- `invoice_credit`: credits that appear as line items on the invoice; requires a price of exactly one currency unit per credit. Activation is currently gated — confirm before promising it.\n- Direct balances always drain before credit-system balances, whatever their intervals.\n- `ai_credit_system`: a monetary balance (units = dollars) for AI/LLM token usage; no `credit_schema`. Cost = Models.dev model pricing + markup.\n - Markups, low to high priority: `default_markup` (global %), `provider_markups` (keyed by the model id's provider prefix), `model_markups` (per model). No markup = Models.dev base cost; `-100` = free (recorded, not deducted).\n - Model ids are `provider/model` (e.g. `anthropic/claude-opus-4-5`, `openrouter/anthropic/...`, `custom/...`). Standard models auto-price from Models.dev; `custom/...` models must set `input_cost`/`output_cost` ($/M tokens) and bill input/output only.\n - Track usage with `trackTokens` (modelId + token counts); Autumn converts to dollars and deducts.\n\n</credit-systems>\n\n<feature-rules>\n\n- Do not create duplicate features for the same resource; use Plan Items to vary allowance, interval, package, or price.\n- Example: `tokens` should be one feature, not separate `monthly_tokens` and `one_time_tokens` features.\n\n</feature-rules>\n\n<additional>\n\n- `credit_schema`: classic `credit_system` only (not `ai_credit_system`); maps `metered_feature_id` to `credit_cost`.\n- `archived`: deprecated config; may still exist in grandfathered plans or subscriptions.\n- Legacy pricing-agent wording: `single_use` means metered consumable; `continuous_use` means metered non-consumable.\n\n</additional>\n\n<useful-docs>\n\n- Concepts overview: https://docs.useautumn.com/documentation/concepts/overview\n- Features concept: https://docs.useautumn.com/documentation/concepts/features\n- Credit systems: https://docs.useautumn.com/documentation/modelling-pricing/credit-systems\n\n</useful-docs>"
17790
17796
  },
17791
17797
  {
17792
17798
  path: "references/plan.md",
@@ -20356,39 +20362,55 @@ var appendElementToArray = ({
20356
20362
  source,
20357
20363
  root,
20358
20364
  array,
20359
- text
20365
+ text,
20366
+ after
20360
20367
  }) => {
20361
- const elements = array.namedChildren();
20368
+ const elements = array.namedChildren().filter((child) => child.kind() !== "comment");
20362
20369
  if (elements.length === 0) {
20363
20370
  const indent = leadingIndentOfLine(source, array.range().start.index);
20364
20371
  const resolved2 = resolveText({ text, elementIndent: `${indent} ` });
20365
20372
  return root.commitEdits([
20366
- {
20367
- startPos: array.range().start.index,
20368
- endPos: array.range().end.index,
20369
- insertedText: `[
20370
- ${indent} ${resolved2},
20371
- ${indent}]`
20372
- }
20373
+ rebuildArrayEdit({
20374
+ array,
20375
+ lines: [...commentLines({ array }), `${resolved2},`],
20376
+ indent
20377
+ })
20373
20378
  ]);
20374
20379
  }
20375
20380
  const last = elements[elements.length - 1];
20376
- const lastEnd = last.range().end.index;
20377
- const after = source.slice(lastEnd);
20378
- const commaAfter = after.indexOf(",");
20379
- const hasTrailingComma = commaAfter !== -1 && after.slice(0, commaAfter).trim() === "";
20380
- const insertAt = hasTrailingComma ? lastEnd + commaAfter + 1 : lastEnd;
20381
- const missingComma = hasTrailingComma ? "" : ",";
20381
+ let anchorIndex = -1;
20382
+ if (after !== undefined) {
20383
+ for (let index = elements.length - 1;index >= 0; index -= 1) {
20384
+ if (after(elements[index])) {
20385
+ anchorIndex = index;
20386
+ break;
20387
+ }
20388
+ }
20389
+ }
20390
+ const anchor = anchorIndex === -1 ? last : elements[anchorIndex];
20391
+ const { insertAt, hasTrailingComma } = insertionAfter({
20392
+ source,
20393
+ array,
20394
+ anchor
20395
+ });
20396
+ const commaEdits = hasTrailingComma ? [] : [
20397
+ {
20398
+ startPos: anchor.range().end.index,
20399
+ endPos: anchor.range().end.index,
20400
+ insertedText: ","
20401
+ }
20402
+ ];
20382
20403
  const spansLines = source.slice(array.range().start.index, last.range().start.index).includes(`
20383
20404
  `);
20384
20405
  if (spansLines) {
20385
- const indent = leadingIndentOfLine(source, last.range().start.index);
20406
+ const indent = leadingIndentOfLine(source, anchor.range().start.index);
20386
20407
  const resolved2 = resolveText({ text, elementIndent: indent });
20387
20408
  return root.commitEdits([
20409
+ ...commaEdits,
20388
20410
  {
20389
20411
  startPos: insertAt,
20390
20412
  endPos: insertAt,
20391
- insertedText: `${missingComma}
20413
+ insertedText: `
20392
20414
  ${indent}${resolved2},`
20393
20415
  }
20394
20416
  ]);
@@ -20399,24 +20421,63 @@ ${indent}${resolved2},`
20399
20421
  if (!resolved.includes(`
20400
20422
  `)) {
20401
20423
  return root.commitEdits([
20424
+ ...commaEdits,
20402
20425
  {
20403
20426
  startPos: insertAt,
20404
20427
  endPos: insertAt,
20405
- insertedText: `${missingComma} ${resolved},`
20428
+ insertedText: ` ${resolved},`
20406
20429
  }
20407
20430
  ]);
20408
20431
  }
20409
- const lines = [...elements.map((element) => element.text()), resolved].map((element) => `${elementIndent}${element},`);
20432
+ const children = array.namedChildren();
20433
+ const lines = children.map((child) => child.kind() === "comment" ? child.text() : `${child.text()},`);
20434
+ const anchorStart = anchor.range().start.index;
20435
+ const position = anchorIndex === -1 ? lines.length : children.findIndex((child) => child.range().start.index === anchorStart) + 1;
20436
+ lines.splice(position, 0, `${resolved},`);
20410
20437
  return root.commitEdits([
20411
- {
20412
- startPos: array.range().start.index,
20413
- endPos: array.range().end.index,
20414
- insertedText: `[
20415
- ${lines.join(`
20438
+ rebuildArrayEdit({ array, lines, indent: lineIndent })
20439
+ ]);
20440
+ };
20441
+ var commentLines = ({ array }) => array.namedChildren().filter((child) => child.kind() === "comment").map((child) => child.text());
20442
+ var rebuildArrayEdit = ({
20443
+ array,
20444
+ lines,
20445
+ indent
20446
+ }) => ({
20447
+ startPos: array.range().start.index,
20448
+ endPos: array.range().end.index,
20449
+ insertedText: `[
20450
+ ${lines.map((line) => `${indent} ${line}`).join(`
20416
20451
  `)}
20417
- ${lineIndent}]`
20452
+ ${indent}]`
20453
+ });
20454
+ var insertionAfter = ({
20455
+ source,
20456
+ array,
20457
+ anchor
20458
+ }) => {
20459
+ const children = array.children();
20460
+ const start = children.findIndex((child) => child.range().start.index === anchor.range().start.index);
20461
+ let insertAt = anchor.range().end.index;
20462
+ let hasTrailingComma = false;
20463
+ for (const child of children.slice(start + 1)) {
20464
+ const kind = child.kind();
20465
+ if (kind === ",") {
20466
+ if (hasTrailingComma)
20467
+ break;
20468
+ hasTrailingComma = true;
20469
+ insertAt = child.range().end.index;
20470
+ continue;
20418
20471
  }
20419
- ]);
20472
+ if (kind !== "comment")
20473
+ break;
20474
+ const sameLine = !source.slice(insertAt, child.range().start.index).includes(`
20475
+ `);
20476
+ if (!sameLine)
20477
+ break;
20478
+ insertAt = child.range().end.index;
20479
+ }
20480
+ return { insertAt, hasTrailingComma };
20420
20481
  };
20421
20482
  var resolveText = ({
20422
20483
  text,
@@ -20449,13 +20510,14 @@ var findArrayBinding = ({
20449
20510
  var appendToBinding = ({
20450
20511
  source,
20451
20512
  name,
20452
- text
20513
+ text,
20514
+ after
20453
20515
  }) => {
20454
20516
  const root = parse5(Lang3.TypeScript, source).root();
20455
20517
  const array = findArrayBinding({ root, name });
20456
20518
  if (array === null)
20457
20519
  return null;
20458
- return appendElementToArray({ source, root, array, text });
20520
+ return appendElementToArray({ source, root, array, text, after });
20459
20521
  };
20460
20522
 
20461
20523
  // src/surgery/appendToCollection.ts
@@ -20463,7 +20525,8 @@ import { Lang as Lang4, parse as parse6 } from "@ast-grep/napi";
20463
20525
  var appendToCollection = ({
20464
20526
  source,
20465
20527
  collection,
20466
- text
20528
+ text,
20529
+ after
20467
20530
  }) => {
20468
20531
  const root = parse6(Lang4.TypeScript, source).root();
20469
20532
  const anchor = root.find(`atmn({ $$$, ${collection}: [$$$ITEMS], $$$ })`) ?? root.find(`atmn({ ${collection}: [$$$ITEMS] })`);
@@ -20472,7 +20535,7 @@ var appendToCollection = ({
20472
20535
  const array = collectionArray({ anchor, collection });
20473
20536
  if (array === null)
20474
20537
  return null;
20475
- return appendElementToArray({ source, root, array, text });
20538
+ return appendElementToArray({ source, root, array, text, after });
20476
20539
  };
20477
20540
  var collectionArray = ({
20478
20541
  anchor,
@@ -20945,6 +21008,9 @@ var changedFixtureKeys = ({
20945
21008
  return ordered.length === 0 ? null : ordered;
20946
21009
  };
20947
21010
 
21011
+ // src/actions/pull/elementStatesPlanId.ts
21012
+ import { Lang as Lang11, parse as parse13 } from "@ast-grep/napi";
21013
+
20948
21014
  // src/actions/pull/resolveCollectionTarget.ts
20949
21015
  import { dirname as dirname8, resolve as resolve4 } from "node:path";
20950
21016
  import { Lang as Lang10, parse as parse12 } from "@ast-grep/napi";
@@ -21058,6 +21124,46 @@ var moduleFileOf = ({
21058
21124
  return null;
21059
21125
  };
21060
21126
 
21127
+ // src/actions/pull/elementStatesPlanId.ts
21128
+ var callStatesId = ({
21129
+ node,
21130
+ builder,
21131
+ idField,
21132
+ id
21133
+ }) => node.kind() === "call_expression" && node.field("function")?.text() === builder && fixturePropertyString({ call: node, property: idField }) === id;
21134
+ var bindingValue = ({
21135
+ root,
21136
+ name
21137
+ }) => root.findAll({ rule: { kind: "variable_declarator" } }).find((declarator) => declarator.field("name")?.text() === name)?.field("value") ?? null;
21138
+ var elementStatesPlanId = ({
21139
+ element,
21140
+ file,
21141
+ files,
21142
+ builder,
21143
+ idField,
21144
+ id
21145
+ }) => {
21146
+ if (element.kind() !== "identifier")
21147
+ return callStatesId({ node: element, builder, idField, id });
21148
+ const root = parse13(Lang11.TypeScript, files.get(file) ?? "").root();
21149
+ const local = bindingValue({ root, name: element.text() });
21150
+ if (local !== null)
21151
+ return callStatesId({ node: local, builder, idField, id });
21152
+ const imported = importedFrom({ root, name: element.text() });
21153
+ if (imported === null)
21154
+ return false;
21155
+ const moduleFile = moduleFileOf({
21156
+ from: file,
21157
+ specifier: imported.specifier,
21158
+ files
21159
+ });
21160
+ if (moduleFile === null)
21161
+ return false;
21162
+ const module = parse13(Lang11.TypeScript, files.get(moduleFile) ?? "").root();
21163
+ const exported = bindingValue({ root: module, name: imported.exportedName });
21164
+ return exported !== null && callStatesId({ node: exported, builder, idField, id });
21165
+ };
21166
+
21061
21167
  // src/actions/pull/applyPreview.ts
21062
21168
  var NESTED_VARIANT = NESTED_FIXTURES.variants;
21063
21169
  var nestedVariantEdges = (row) => Array.isArray(row[NESTED_VARIANT.path]) ? row[NESTED_VARIANT.path] : [];
@@ -21288,7 +21394,20 @@ var applyPreview = ({
21288
21394
  context: { featureTypes, nestedBuilders }
21289
21395
  });
21290
21396
  const targetSource = files.get(resolved.file) ?? "";
21291
- const updated = resolved.kind === "inline" ? appendToCollection({ source: targetSource, collection, text }) : appendToBinding({ source: targetSource, name: resolved.name, text });
21397
+ const after = versioned ? (element) => elementStatesPlanId({
21398
+ element,
21399
+ file: resolved.file,
21400
+ files,
21401
+ builder: rowSpec.builder,
21402
+ idField: rowSpec.idField,
21403
+ id
21404
+ }) : undefined;
21405
+ const updated = resolved.kind === "inline" ? appendToCollection({ source: targetSource, collection, text, after }) : appendToBinding({
21406
+ source: targetSource,
21407
+ name: resolved.name,
21408
+ text,
21409
+ after
21410
+ });
21292
21411
  if (updated === null) {
21293
21412
  result.unlocated.push({
21294
21413
  id: key,
@@ -21470,7 +21589,7 @@ var applyPreview = ({
21470
21589
  };
21471
21590
 
21472
21591
  // src/surgery/patchSingletonProperty.ts
21473
- import { Lang as Lang11, parse as parse13 } from "@ast-grep/napi";
21592
+ import { Lang as Lang12, parse as parse14 } from "@ast-grep/napi";
21474
21593
  var atmnObject = (root) => {
21475
21594
  const object = root.find("atmn($ARG)")?.getMatch("ARG") ?? null;
21476
21595
  return object !== null && object.kind() === "object" ? object : null;
@@ -21544,7 +21663,7 @@ var patchSingletonProperty = ({
21544
21663
  block,
21545
21664
  edit
21546
21665
  }) => {
21547
- const root = parse13(Lang11.TypeScript, source).root();
21666
+ const root = parse14(Lang12.TypeScript, source).root();
21548
21667
  const object = blockObject({ root, block });
21549
21668
  if (object === null)
21550
21669
  return null;
@@ -21581,7 +21700,7 @@ var insertSingleton = ({
21581
21700
  source,
21582
21701
  singleton
21583
21702
  }) => {
21584
- const root = parse13(Lang11.TypeScript, source).root();
21703
+ const root = parse14(Lang12.TypeScript, source).root();
21585
21704
  const object = atmnObject(root);
21586
21705
  if (object === null)
21587
21706
  return null;
@@ -21594,7 +21713,7 @@ var insertSingleton = ({
21594
21713
  const withArray = insertCollection({ source, collection: singleton });
21595
21714
  if (withArray === null || withArray === source)
21596
21715
  return withArray;
21597
- const seededRoot = parse13(Lang11.TypeScript, withArray).root();
21716
+ const seededRoot = parse14(Lang12.TypeScript, withArray).root();
21598
21717
  const seeded = atmnObject(seededRoot);
21599
21718
  const pair = seeded === null ? null : pairFor2({ object: seeded, key: singleton });
21600
21719
  const value = pair?.namedChildren()[1];
@@ -22370,7 +22489,7 @@ ${indent}`
22370
22489
  };
22371
22490
 
22372
22491
  // src/surgery/setFixtureProperty.ts
22373
- import { Lang as Lang12, parse as parse14 } from "@ast-grep/napi";
22492
+ import { Lang as Lang13, parse as parse15 } from "@ast-grep/napi";
22374
22493
  var setFixtureProperty = ({
22375
22494
  source,
22376
22495
  builder,
@@ -22399,7 +22518,7 @@ var setFixtureProperty = ({
22399
22518
  const [key, current] = member.namedChildren();
22400
22519
  if (key?.text() !== property || current === undefined)
22401
22520
  continue;
22402
- const root = parse14(Lang12.TypeScript, source).root();
22521
+ const root = parse15(Lang13.TypeScript, source).root();
22403
22522
  return root.commitEdits([
22404
22523
  {
22405
22524
  startPos: current.range().start.index,
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ var LINT_RULES = {
22
22
  label: "feature",
23
23
  idField: "featureId",
24
24
  required: ["featureId", "name", "type"],
25
- hidden: ["display", "newFeatureId"],
25
+ hidden: ["display", "eventNames", "newFeatureId"],
26
26
  fields: {
27
27
  type: {
28
28
  enum: ["boolean", "metered", "credit_system", "ai_credit_system"]
@@ -2041,9 +2041,6 @@ var LINT_RULES = {
2041
2041
  name: {
2042
2042
  minLength: 1
2043
2043
  },
2044
- planIds: {
2045
- minItems: 1
2046
- },
2047
2044
  internalId: {
2048
2045
  minLength: 1
2049
2046
  },
@@ -2055,6 +2052,11 @@ var LINT_RULES = {
2055
2052
  }
2056
2053
  },
2057
2054
  rules: [
2055
+ {
2056
+ kind: "nonEmpty",
2057
+ field: "planIds",
2058
+ because: "A coupon must apply to at least one plan: list the plans it discounts, set planIds: null to apply it to every plan, or remove the coupon from your config."
2059
+ },
2058
2060
  {
2059
2061
  kind: "exists",
2060
2062
  field: "planIds",
@@ -2367,6 +2369,12 @@ var entryRuleFailures = ({
2367
2369
  const missing = (Array.isArray(value) ? value : [value]).filter((wanted) => !names(wanted));
2368
2370
  return missing.map((wanted) => `${rule.field} ${show(wanted)} is not in ${rule.in}. ${rule.because}`);
2369
2371
  }
2372
+ case "nonEmpty": {
2373
+ const value = entry[rule.field];
2374
+ if (!Array.isArray(value) || value.length > 0)
2375
+ return [];
2376
+ return [`${rule.field} is empty. ${rule.because}`];
2377
+ }
2370
2378
  case "compare": {
2371
2379
  const a = entry[rule.field];
2372
2380
  const b = entry[rule.than];
@@ -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/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/push/backfillInternalIds.ts","../src/actions/push/deprecatedFields.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/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/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/settings.ts","../src/generated/skills.ts","../src/generated/variants.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/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"]}
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/push/backfillInternalIds.ts","../src/actions/push/deprecatedFields.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/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/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/settings.ts","../src/generated/skills.ts","../src/generated/variants.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/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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atmn",
3
- "version": "2.0.21",
3
+ "version": "2.0.23",
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": "3f21a77c3434ec8d46be4b4f3ffe7973f29e07d3"
52
+ "gitHead": "9496baa36a1f244f3d9fce1c50e6ac48877b2ef5"
53
53
  }
@@ -1,3 +1,4 @@
1
+ import type { SgNode } from "@ast-grep/napi";
1
2
  import { NESTED_FIXTURES } from "../../generated/emit";
2
3
  import {
3
4
  branchSpecs,
@@ -24,6 +25,7 @@ import {
24
25
  } from "../../surgery/patchFixtureProperty";
25
26
  import { replaceFixture } from "../../surgery/replaceFixture";
26
27
  import { changedFixtureKeys } from "./changedFixtureKeys";
28
+ import { elementStatesPlanId } from "./elementStatesPlanId";
27
29
  import { type FixtureConstraint, locateFixture } from "./locateFixture";
28
30
  import { resolveCollectionTarget } from "./resolveCollectionTarget";
29
31
 
@@ -342,10 +344,27 @@ export const applyPreview = ({
342
344
  context: { featureTypes, nestedBuilders },
343
345
  });
344
346
  const targetSource = files.get(resolved.file) ?? "";
347
+ // Versions of one plan sit together: a new one lands after its siblings.
348
+ const after = versioned
349
+ ? (element: SgNode) =>
350
+ elementStatesPlanId({
351
+ element,
352
+ file: resolved.file,
353
+ files,
354
+ builder: rowSpec.builder,
355
+ idField: rowSpec.idField,
356
+ id,
357
+ })
358
+ : undefined;
345
359
  const updated =
346
360
  resolved.kind === "inline"
347
- ? appendToCollection({ source: targetSource, collection, text })
348
- : appendToBinding({ source: targetSource, name: resolved.name, text });
361
+ ? appendToCollection({ source: targetSource, collection, text, after })
362
+ : appendToBinding({
363
+ source: targetSource,
364
+ name: resolved.name,
365
+ text,
366
+ after,
367
+ });
349
368
  if (updated === null) {
350
369
  result.unlocated.push({
351
370
  id: key,