{"_id":"@anthnyalxndr/gtm-apply","_rev":"2-e3310056e9124bc14f4ee6de360f6ea7","name":"@anthnyalxndr/gtm-apply","dist-tags":{"latest":"3.0.0"},"versions":{"2.0.0":{"name":"@anthnyalxndr/gtm-apply","version":"2.0.0","keywords":["google-tag-manager","gtm","tagmanager","infrastructure-as-code"],"license":"Apache-2.0","_id":"@anthnyalxndr/gtm-apply@2.0.0","maintainers":[{"name":"anthnyalxndr","email":"avail-fetes.43@icloud.com"}],"homepage":"https://github.com/anthnyalxndr/gtm_v2#readme","bugs":{"url":"https://github.com/anthnyalxndr/gtm_v2/issues"},"bin":{"gtm-apply":"dist/bin.js"},"dist":{"shasum":"67266a27a7db9f153b5c1c7f9a49f3e485ec7e4e","tarball":"https://registry.npmjs.org/@anthnyalxndr/gtm-apply/-/gtm-apply-2.0.0.tgz","fileCount":75,"integrity":"sha512-HzUnc6Oy5bO9S0W8xRBlQT9ToJ1LXqGiF1XCoSUiDwr1zJEI4+E0w2qGHcoPygOFb316jc8a0Knaw537+PIFTw==","signatures":[{"sig":"MEUCIB0d48gTVJ05Fbkwxl+K4bszduady4KJg7IdI2lLDXO8AiEA1q9CQdUQIGsNxqLtlzwyelLocqPH7uW4mGCto5BGy5k=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":137856},"main":"dist/index.js","type":"module","_from":"file:anthnyalxndr-gtm-apply-2.0.0.tgz","types":"dist/index.d.ts","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"scripts":{"dev":"tsx example.ts","test":"vitest run","build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.test.json"},"_npmUser":{"name":"anthnyalxndr","email":"avail-fetes.43@icloud.com"},"_resolved":"/private/var/folders/r6/j2vfy3g508394ff38b2n9n_00000gn/T/3f06ae060cf8ffd58beb81c566968a9f/anthnyalxndr-gtm-apply-2.0.0.tgz","_integrity":"sha512-HzUnc6Oy5bO9S0W8xRBlQT9ToJ1LXqGiF1XCoSUiDwr1zJEI4+E0w2qGHcoPygOFb316jc8a0Knaw537+PIFTw==","repository":{"url":"git+https://github.com/anthnyalxndr/gtm_v2.git","type":"git","directory":"packages/gtm-apply"},"_npmVersion":"11.4.2","description":"Declarative apply tool for Google Tag Manager: plan and reconcile a container against a JSON spec in the shape of a GTM container export.","directories":{},"_nodeVersion":"22.16.0","dependencies":{"@googleapis/tagmanager":"^16.0.0","@anthnyalxndr/gtm-client":"^1.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/gtm-apply_2.0.0_1789066555698_0.38370343583087974","host":"s3://npm-registry-packages-npm-production"}},"3.0.0":{"_id":"@anthnyalxndr/gtm-apply@3.0.0","bin":{"gtm-apply":"dist/bin.js"},"bugs":{"url":"https://github.com/anthnyalxndr/gtm_v2/issues"},"dist":{"shasum":"ba020cb6ea8a594bf02f1d95fc9caa5648cfaf8a","tarball":"https://registry.npmjs.org/@anthnyalxndr/gtm-apply/-/gtm-apply-3.0.0.tgz","fileCount":131,"integrity":"sha512-sg3VnwWxKdWiU0++ZxAxCyGzNhb3bFwjdbocLO48ChtWLSGPet9uV9EJneC1mRc3phTWZJS2vqyD+Cl49K9cBg==","signatures":[{"sig":"MEUCIEmqAuIX2R3EC7zqqHxxnO09k/r/qpuQdDGE1BYmbWKiAiEAtBdcZYGqM1IN0Dkv2AaQCw0qsZFfbZ1izRpfXg5jroY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCtD78qdEu4d6/S9+JpCVCwHpm/dxlUAutMCniYf3YLpAIhAJQIc+AQ0c5+4uv48bzvik276MV4wOkZeO83RvXHaNu2"}],"unpackedSize":401208},"main":"dist/index.js","name":"@anthnyalxndr/gtm-apply","type":"module","_from":"file:anthnyalxndr-gtm-apply-3.0.0.tgz","types":"dist/index.d.ts","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"license":"Apache-2.0","scripts":{"dev":"tsx example.ts","test":"vitest run","build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.test.json","gen:discovery":"tsx scripts/generate-discovery.ts"},"version":"3.0.0","_npmUser":{"name":"anthnyalxndr","email":"avail-fetes.43@icloud.com"},"homepage":"https://github.com/anthnyalxndr/gtm_v2#readme","keywords":["google-tag-manager","gtm","tagmanager","infrastructure-as-code"],"_resolved":"/private/var/folders/r6/j2vfy3g508394ff38b2n9n_00000gn/T/b23650599c0d234ce2891b82b8532011/anthnyalxndr-gtm-apply-3.0.0.tgz","_integrity":"sha512-sg3VnwWxKdWiU0++ZxAxCyGzNhb3bFwjdbocLO48ChtWLSGPet9uV9EJneC1mRc3phTWZJS2vqyD+Cl49K9cBg==","repository":{"url":"git+https://github.com/anthnyalxndr/gtm_v2.git","type":"git","directory":"packages/gtm-apply"},"_npmVersion":"11.4.2","description":"Declarative apply tool for Google Tag Manager: plan and reconcile a container against a JSON spec in the shape of a GTM container export.","directories":{},"maintainers":[{"name":"anthnyalxndr","email":"avail-fetes.43@icloud.com"}],"_nodeVersion":"22.16.0","dependencies":{"@googleapis/tagmanager":"^16.0.0","@anthnyalxndr/gtm-client":"^1.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/gtm-apply_3.0.0_1789141330432_0.5932888711926034"}}},"time":{"created":"2026-09-10T18:55:55.512Z","modified":"2026-09-11T15:42:10.667Z","2.0.0":"2026-09-10T18:55:55.808Z","3.0.0":"2026-09-11T15:42:10.513Z"},"bugs":{"url":"https://github.com/anthnyalxndr/gtm_v2/issues"},"license":"Apache-2.0","homepage":"https://github.com/anthnyalxndr/gtm_v2#readme","keywords":["google-tag-manager","gtm","tagmanager","infrastructure-as-code"],"repository":{"url":"git+https://github.com/anthnyalxndr/gtm_v2.git","type":"git","directory":"packages/gtm-apply"},"description":"Declarative apply tool for Google Tag Manager: plan and reconcile a container against a JSON spec in the shape of a GTM container export.","maintainers":[{"name":"anthnyalxndr","email":"avail-fetes.43@icloud.com"}],"readme":"# @anthnyalxndr/gtm-apply\n\nDeclarative apply tool and CLI for Google Tag Manager. Describe a container in the same JSON shape the GTM UI exports, and apply it to any container you can access. The engine plans every change first, reports every problem at once, and only then writes, in dependency order, into a named workspace.\n\n## Install\n\n```bash\npnpm add @anthnyalxndr/gtm-apply\n```\n\nAuth, throttling, and the raw API service come from [`@anthnyalxndr/gtm-client`](../gtm-client/README.md), which this package depends on and re-exports, so one import covers most scripts. Both build on `@googleapis/tagmanager`, the per-API client, not the monolithic `googleapis` bundle.\n\n## Credentials\n\nPlace your OAuth client file at `~/.config/gtm-apply/client_secrets.json` (see `client_secrets.json.example`). On first run gtm-apply opens a browser, receives the callback on a random localhost port, and stores the token at `~/.config/gtm-apply/token.json`. That one token serves every repo on the machine. Set `GTM_APPLY_CONFIG_DIR` to use another directory, or pass `clientSecretsPath` and `tokenPath` to the client.\n\n## Getting started\n\n`Gtm` is the entry point. It holds the client and nothing else, so one instance serves every container a script touches; everything it does is also available as a function that takes the client first.\n\n```ts\nimport { Gtm } from \"@anthnyalxndr/gtm-apply\";\nimport { library } from \"@anthnyalxndr/gtm-web-recipes\";\n\nconst gtm = await Gtm.fromConfig().init();               // OAuth from ~/.config/gtm-apply\nconst spec = await gtm.export({ container: \"GTM-XXXXXXX\" });          // normalized spec\nconst snap = await gtm.snapshot({ container: \"GTM-TPLXXXX\" });        // GtmSnapshot, memoized per source\nawait gtm.apply({ container: \"GTM-XXXXXXX\", workspace: \"fix\", spec, dryRun: true });\nawait gtm.applyPlan({ library, plan, container: \"GTM-XXXXXXX\", workspace: \"onboarding\" });\n```\n\n## The spec\n\nA spec is a GTM container export with three changes: server fields (`accountId`, `*Id`, `fingerprint`, `path`) are removed, id references become name references (`firingTriggerName`, `blockingTriggerName`, `parentFolderName`), and enum values are lower camel case (`customEvent`, `template`, `equals`). Everything else is exactly what the Tag Manager API accepts.\n\n```json\n{\n  \"variable\": [\n    { \"name\": \"Const - Google Ads Conversion ID\", \"type\": \"c\",\n      \"parameter\": [{ \"type\": \"template\", \"key\": \"value\", \"value\": \"AW-123\" }] }\n  ],\n  \"trigger\": [\n    { \"name\": \"Custom Event - lead\", \"type\": \"customEvent\",\n      \"customEventFilter\": [{ \"type\": \"equals\", \"parameter\": [\n        { \"type\": \"template\", \"key\": \"arg0\", \"value\": \"{{_event}}\" },\n        { \"type\": \"template\", \"key\": \"arg1\", \"value\": \"lead\" } ] }] }\n  ],\n  \"tag\": [\n    { \"name\": \"Ads - Lead\", \"type\": \"awct\", \"firingTriggerName\": [\"Custom Event - lead\"],\n      \"parameter\": [\n        { \"type\": \"template\", \"key\": \"conversionId\", \"value\": \"{{Const - Google Ads Conversion ID}}\" },\n        { \"type\": \"template\", \"key\": \"conversionLabel\", \"value\": \"xyz\" } ] }\n  ]\n}\n```\n\nThe fastest way to write a spec is to build the entities once in the GTM UI, export the container, and run `gtm-apply normalize export.json`. Or capture a container with `gtm-apply export --container GTM-XXXXXXX`, which reads the latest version by default (published or not), `--live` for the published one, or `--workspace <name>` for work in progress. Keep customer-specific values in constant variables so the rest of the spec is reusable.\n\n### Writing a spec in TypeScript\n\nA spec file can also be a `.ts`, `.js`, or `.mjs` module whose default export is the spec. Wrap it in `defineContainer()` and every enum-valued field is a string-literal union, so your editor completes `type: \"customEvent\"` and `tsc` rejects `\"custom_event\"` before anything reaches Tag Manager. See [`spec.example.ts`](spec.example.ts).\n\n```ts\nimport { defineContainer } from \"@anthnyalxndr/gtm-apply\";\n\nexport default defineContainer({\n  trigger: [{ name: \"Custom Event - lead\", type: \"customEvent\", customEventFilter: [/* ... */] }],\n  tag: [{ name: \"Ads - Lead\", type: \"awct\", firingTriggerName: [\"Custom Event - lead\"],\n          tagFiringOption: \"oncePerEvent\", parameter: [/* ... */] }],\n});\n```\n\n```bash\ngtm-apply apply --container GTM-XXXXXXX --workspace onboarding --spec spec.ts --dry-run\n```\n\nTypeScript files are imported through Node's own type stripping, which is on by default from Node 22.18 and 23.6. On Node 22.6 to 22.17 run `node --experimental-strip-types $(which gtm-apply) …` or go through `tsx`. Type stripping handles types only: a spec module can't use enums or parameter properties.\n\nThe types come from Google's [Discovery document](https://tagmanager.googleapis.com/$discovery/rest?version=v2) for the Tag Manager API v2 (Google publishes no OpenAPI spec). `pnpm gen:discovery --fetch` refreshes the committed copy under `scripts/discovery/` and regenerates `src/spec/generated/tagmanager-v2.ts`; a test fails if the two drift. Two things the document does not carry: which parameter keys a given tag or variable template (`awct`, `gaawe`, `c`) accepts, and which trigger fields belong to which trigger type. Those are still checked by the API at apply time.\n\n### Validation\n\nBefore any API call, `gtm-apply apply` checks the spec against the same schemas: unknown fields, wrong primitive types, `null` values, leftover id fields, and enum values the API would reject are all reported at once with the entity name and field path, and the command exits 1. From code, `validateSpec(spec)` returns the issues and `planContainerSpec` throws a `SpecValidationError` listing them.\n\n```\nSpec spec.json has 2 problem(s):\n[!] trigger \"Custom Event - lead\": type must be one of pageview, domReady, … (got \"custom_event\")\n[!] tag \"Ads - Lead\": tagFiringOption must be one of unlimited, oncePerEvent, oncePerLoad (got \"once\")\n```\n\n### Snapshots\n\nA spec is the apply-able part of a container. A snapshot is everything the API exposes for it, as the API returns it: the container and its type (from `usageContext`), the workspace or version read, the container's environments and the one serving that version, linked Google tag destinations, version headers, and every entity collection including gtag configs, custom templates, clients and transformations. It's the input for a library, an audit, or anything that needs more than tags, triggers and variables.\n\n```bash\ngtm-apply snapshot --container GTM-XXXXXXX                  # latest version\ngtm-apply snapshot --container GTM-XXXXXXX --live           # published version\ngtm-apply snapshot --container GTM-XXXXXXX --version 42\ngtm-apply snapshot --container GTM-XXXXXXX --workspace wip  # work in progress\n```\n\nFrom code, `pullSnapshot(client, source)` returns an `ApiSnapshotData` and `snapshotToSpec(snapshot)` normalizes the apply-able part, tagged with its `containerType`. `GtmSnapshot` (below) adds the recipe index on top of it.\n\n### Container types\n\nA spec may carry `containerType` (`web`, `server`, `amp`, `android`, `ios`); `normalize` sets it from an export's `usageContext`. Applying a spec to a container of another type is a plan error before any write. Server containers add two sections, `client` and `transformation`, with the same rules as other entities: name is identity, `parentFolderName` names the folder, `{{Name}}` references are resolved, and the engine applies them after variables and before triggers. A `web` spec that declares clients is rejected by validation. Custom templates and gtag configs are carried in snapshots but not yet applied.\n\n## Applying a spec\n\n```bash\ngtm-apply apply --container GTM-XXXXXXX --workspace conversions-2026-09 --spec spec.json --dry-run\ngtm-apply apply --container GTM-XXXXXXX --workspace conversions-2026-09 --spec spec.json\ngtm-apply apply --container GTM-XXXXXXX --workspace conversions-2026-09 --spec spec.json --publish\n```\n\nOr from code:\n\n```ts\nimport { GtmClient, applySpec, normalizeExport, formatPlan } from \"@anthnyalxndr/gtm-apply\";\n\nconst client = new GtmClient();\nawait client.init();\n\nconst spec = normalizeExport(JSON.parse(await readFile(\"spec.json\", \"utf-8\")));\nconst { plan, result } = await applySpec(client, {\n  container: \"GTM-XXXXXXX\",\n  workspace: \"conversions-2026-09\",\n  spec,\n  dryRun: true,\n});\nconsole.log(formatPlan(plan));\n```\n\n### What apply does\n\n1. Resolves the container by public id and reads the target workspace if it exists.\n2. Resolves every reference in the spec and builds a plan. Names are identity: an entity that exists by name is compared and updated only if it differs. The plan output uses `[+]` create, `[~]` update, `[=]` unchanged, `[!]` error.\n3. Refuses to write while the plan has errors. All errors are reported together.\n4. Applies folders, then variables (ordered by their `{{ }}` references), then triggers, then tags, resolving names to ids as it goes. Updates send the current fingerprint.\n5. Checks the workspace for merge conflicts, creates a version when something changed, and publishes only with `--publish`.\n\n`--dry-run` prints the plan and makes no write calls. What the dry run shows is exactly what apply does.\n\n### Versions and workspaces\n\nTwo Tag Manager behaviors shape the apply flow, both verified against the live API:\n\n- **Creating a version deletes the workspace it came from.** After an apply that changed something, the named workspace is gone and the changes live in the new version. Open a fresh workspace in the UI to preview.\n- **A new workspace branches from the latest version, not the live one.** So the next apply sees everything earlier applies created, whether or not it was published, and reports it `[=]`. The planner reads the latest version when the target workspace does not exist yet.\n\nWhen nothing changed, no version is created and the workspace is left in place.\n\n### Naming\n\nTag Manager rejects `:` in entity names. The planner reports it before writing. Notes accept any text, so conventions like `#recipe:ga4-event` belong in an entity's notes field, not its name.\n\n### What gets created implicitly\n\nIf an entity needs no information beyond its name, the engine creates it and marks the operation `(implicit)` in the plan. That covers the workspace, folders named in `parentFolderName`, and built-in variables referenced as `{{Page Path}}` or `{{Form ID}}`. Everything that needs a type or a value must be in the spec, and a reference to something that is neither in the spec nor in the container is an error. Containers are never created implicitly; use `createContainer()`.\n\nThe default workspace is never written to.\n\n### Limits\n\n- Tags built on custom or community templates (`cvt_*` types) are rejected by the normalizer. Import the template into the target container first; direct support is a backlog item.\n- Trigger groups (`triggerReference` parameters) are rejected.\n- Validation covers field names, primitive types, and enum values, not which parameter keys a tag template accepts or which fields a trigger type uses. Those errors still come back from the API during apply.\n- The planner compares only the fields the spec provides. Fields stripped from an export, such as `monitoringMetadata`, are not corrected if someone changes them in the UI.\n- The Tag Manager API has tight per-minute quotas. Every call is throttled and retried with backoff; large specs take a while.\n\n## Libraries and recipes\n\nA library is a GTM container you build in the UI and pull into a committed snapshot. Recipes are declared on the entities that fire, tags (and clients and transformations in a server container), through an encoding the library chooses; everything else a recipe needs, triggers, variables, setup tags, folders and built-ins, is discovered by following references. `GtmSnapshot` is a container pulled at one moment. `data` is the pull exactly as the API returned it (`ApiSnapshotData`: container, environments, destinations, version header, every entity collection) and never changes. On top of it sit name-keyed views per entity kind (`tags`, `triggers`, `variables`, `clients`, …), a `recipes` index with `recipe(name)`, and `select`, `lint`, `push`.\n\n```ts\nconst lib = await gtm.snapshot({ container: \"GTM-TPLXXXX\" });   // or new GtmSnapshot(client, source).init()\nlib.data.destinations;                          // raw pull\nlib.recipe(\"form_submit\");                      // roots, entities, description, dependencies\nlib.tags.get(\"Ads - lead\");\nconst spec = lib.select([\"form_submit\"], { destinations: [\"ga4\", \"googleAds\"] });\nawait gtm.apply({ container: \"GTM-CUST\", workspace: \"onboarding\", spec });\n\nawait writeFile(\"library.json\", JSON.stringify(lib, null, 2));            // { data, manifest, encoding, recipes }\nconst same = gtm.snapshotFrom(JSON.parse(await readFile(\"library.json\", \"utf-8\")));\n```\n\nThe views are a working copy. Assign one to stage an edit: `lib.tags = tags` (a Map or an array) replaces the tags, re-indexes recipes, and changes what `spec`, `select` and `push` produce, while `data` and `toJSON()` still describe the pull. `isDirty` says whether anything is staged and `reset()` discards it. This is the seam a change report hangs off: the pull is the before, the staged state is the after.\n\n`select` returns the union of the recipes' closures in library order, strips recipe declarations from tags, leaves the manifest out, and filters destination tags by family (`gaawe` is `ga4`, `awct` and `gclidw` are `googleAds`, `googtag` is `googleTag`; tags of no family are always kept). `push(client, { workspace })` applies the staged state, declarations intact, back to its own container. `lint()` reports tags naming recipes the manifest doesn't declare, recipes that reach no trigger, and dependencies naming constants outside the recipe.\n\n### Encodings\n\nAn encoding is an object with a name, `recipesOf(entity)` returning the recipe names an entity declares, and an optional `strip(entity)` for declarations that must not reach a customer container. Two ship:\n\n- `notes`: a `recipes: a, b` line anywhere in the entity's notes. Notes never ship in a container, so nothing to strip. The default when a library has no manifest.\n- `metadata`: a key (default `recipes`) in a tag's Additional Tag Metadata. Metadata ships in the container and reaches tag monitors, so `select` strips it. Only tags carry metadata.\n\nRegister your own with `registerEncoding(name, factory)`; a manifest refers to encodings by name, so the code stays in your package and never in the container.\n\n### The manifest\n\nA Constant variable named `Library - Manifest` whose value is JSON. It is never referenced by a tag, so it is never selected. A snapshot literal typed `as const` (or passed to `fromData`, which infers `const`) gives literal recipe names, so `select([\"form_submti\"])` is a compile error.\n\n```json\n{\n  \"encoding\": { \"name\": \"metadata\", \"options\": { \"key\": \"recipes\" } },\n  \"recipes\": {\n    \"form_submit\": {\n      \"description\": \"Lead form submitted\",\n      \"dependencies\": [\n        { \"constant\": \"Const - Ads Label - lead\", \"platform\": \"googleAds\",\n          \"resource\": \"conversionAction\", \"nameTemplate\": \"GTM - ${recipe}\" }\n      ]\n    }\n  },\n  \"destinations\": { \"cvt_123_45\": \"googleAds\" }\n}\n```\n\nDependencies name the constant that carries an identifier from another platform and how the resource is expected to be named there. Nothing verifies them against Google Ads or GA4 yet; `lint` only checks that the constant is in the recipe.\n\n## Naming conventions\n\nRecipes, `select`, and audits all lean on names, so the rules live in one place: `DEFAULT_CONVENTIONS` in gtm-apply. Prefixes are keyed by entity type (`gaawe` tags start with `GA4 - `, `awct` with `Ads - `, `c` variables with `Const - `, `v` with `DLV - `, `customEvent` triggers with `Custom Event - `, and so on), `patterns` add a regular expression per entity kind, `forbidden` bans characters Tag Manager rejects, and `externalNames` says what a recipe's resources are called on other platforms (`googleAds.conversionAction` is `GTM - ${recipe}`).\n\n`checkNames(spec, conventions)` reports every violating entity with the rule it breaks. Overrides layer over the defaults with `mergeConventions`, and a library carries its own under `conventions` in the manifest; a consumer such as gtm_audit passes its per-container overrides the same way. `GtmSnapshot.lint()` includes naming issues whenever the manifest or the constructor options declare conventions, and stays quiet otherwise.\n\n```ts\nconst lib = await new GtmSnapshot(client, { container }, {\n  conventions: { tagPrefixes: { gaawe: \"GA4 Event - \" }, patterns: { folder: \"^[A-Z]\" } },\n}).init();\nlib.lint();                                        // naming issues included\nlib.externalNameOf(\"form_submit\", dependency);     // \"GTM - form_submit\"\n```\n\n## Tracking plans\n\nA customer's onboarding is a plan: which recipes to install, which destination families to keep, and the values of the constants those recipes need. `defineTrackingPlan(library, plan)` checks recipe and constant names against the library's literal types, so a typo is a compile error. `compilePlan` selects the recipes, fills in the constants, and reports problems before any API call: a constant whose library value is a placeholder (`<AW-XXXXXXXXX>`) with no value in the plan, a value that fails a dependency's pattern, a constant that isn't in the library, and naming violations when the library declares conventions. A supplied value that still looks like a placeholder is a warning. `applyPlan` compiles, optionally writes the spec to a file, and applies it through the same engine.\n\n```ts\nimport { applyPlan, defineTrackingPlan } from \"@anthnyalxndr/gtm-apply\";\nimport { library } from \"@anthnyalxndr/gtm-web-recipes\";\n\nconst plan = defineTrackingPlan(library, {\n  recipes: [\"form_submit\", \"call_click\"],\n  destinations: [\"ga4\", \"googleAds\"],\n  constants: {\n    \"Const - GA4 Measurement ID\": \"G-XXXXXXX\",\n    \"Const - Ads Conversion ID\": \"AW-123456789\",\n    \"Const - Ads Label - form_submit\": \"AbC-dEf\",\n  },\n});\n\nconst { plan: ops, warnings } = await gtm.applyPlan({\n  library, plan, container: \"GTM-XXXXXXX\", workspace: \"onboarding-2026-09\", dryRun: true,\n  writeSpecTo: \"compiled.json\",\n});\n```\n\nOr from the CLI, with a plan module and a library file or module:\n\n```bash\ngtm-apply apply --container GTM-XXXXXXX --workspace onboarding --plan plan.ts --library library.json --dry-run\n```\n\nA content package holds the library: its pull script reads the template container with `GtmSnapshot`, lints it, and writes the snapshot as a `const` TypeScript module with `libraryModuleSource`, so recipe and constant names are literal types wherever the package is imported. See `packages/gtm-web-recipes`.\n\n## Ad hoc work: use gtm-cli\n\nFor discovery, inspection, and one-off edits, use owntag's [gtm-cli](https://github.com/owntag/gtm-cli) (`npm i -g @owntag/gtm-cli`). It covers per-resource commands with JSON output and needs no spec. gtm-apply is for the repeatable path: reconciling a container against a spec by name, with dry run and version handling. The reasoning is recorded in `backlog/decisions/`.\n\n## Development\n\n```bash\npnpm install\npnpm verify        # typecheck + tests\npnpm dev           # runs example.ts (dry run)\n```\n\nRun these from the repo root. Pre-commit hooks run prettier, the build, the typecheck, and the tests for both packages. Design notes are in `docs/superpowers/plans/2026-09-09-gtm-sdk.md` and `backlog/decisions/`.\n\n## License\n\nApache-2.0\n","readmeFilename":"README.md"}