abcli

module
v0.4.33 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2026 License: AGPL-3.0

README

abcli

abcli

abcli is Apple Business command-line and desktop tooling from Gigaion, LLC. It ships two tools: abctl, the GitOps/imperative CLI, and abgui, a cross-platform desktop app that drives the same engine.

abctl keeps your organization's built-in-MDM Configurations (custom .mobileconfig profiles) and full Blueprint membership — configurations, apps, packages, devices, users, and groups — in sync with a git-declarative desired state — read-only by default, every write gated, with an archive-on-overwrite audit trail. It also gives deep read-only inspection of the whole tenant (device detail + AppleCare, built-in-MDM enrollment posture, users/roles, status device <serial>) and gated device→MDM-server assignment. abgui is a Flutter desktop app (macOS, Windows, Linux) that bundles and shells out to abctl.

Status: pre-1.0. Auth + read + plan are live-verified. Config CRUD and blueprint config-membership GitOps are built, unit-tested, and their core write operations verified live against a production Apple Business tenant. The Apple API v2.0/v2.1 surface (detail reads, posture, blueprint lifecycle + all six membership collections, device assignment, MDM-server lifecycle) is built + unit-tested; its write verbs await first live runs. The abgui desktop app (dashboard · browse + inspectors · diff/drift · gated apply · archive rollback) is being rewritten in Flutter for macOS, Windows and Linux; the outgoing macOS-only macOS-only SwiftUI build was removed at v0.4.28 (see docs/abgui-flutter-port.md). See HANDOFF.md for exact state and TODO.md for the roadmap.


Tools

Tool What it is Ships as
abctl GitOps + imperative CLI for the Apple Business API Cross-platform Go binary
abgui Cross-platform desktop GUI on top of the embedded abctl engine Signed/notarized macOS .app + DMG, Windows .zip, Linux .tar.gz

Why abctl

Apple Business's built-in MDM has no batch/declarative API — only per-resource POST/PATCH/DELETE. abctl is the reconcile engine: it diffs your git desired state against the live tenant and converges them, so your MDM profiles and blueprint membership live in version control like any other infrastructure.

  • Read-only by default. Every mutating operation is gated behind --apply + an interactive confirm.
  • Bidirectional, newest-wins. Edit in git or the Apple Business console; sync reconciles both, with the more-recently-changed side winning a conflict.
  • Archive-on-overwrite. Before overwriting or deleting any live config, abctl downloads and files the current version to gitops/archive/ — a permanent, greppable record of everything ever replaced.
  • Exact drift detection. Apple stores custom profiles byte-for-byte (verified live), so drift is a plain SHA-256 of the profile XML — of the profile Apple actually stored.
  • Confirmed writes, not hopeful ones. Apple can answer a POST/PATCH with a 2xx and then silently discard an out-of-spec profile: the live bytes never change, and a hash comparison alone reads that as drift and re-plans the identical change forever. So abctl reads every write back and records the baseline from what Apple stored, never from what it sent; a write that didn't land is an error with the archived pre-overwrite copy attached, and validate catches the top cause (a top-level PayloadVersion1) offline, before anything is pushed.
  • Safe blueprint GitOps. Declare which profiles — and optionally which apps, packages, devices, users, and groups — each Blueprint carries; abctl attaches/detaches to match (detach gated behind --prune), creates git-only blueprints (members ride inside the create POST), leaves any collection you don't declare untouched, and never deletes a blueprint from GitOps.
  • Whole-tenant visibility. get device <serial> (assigned MDM server + --applecare coverage), get mdmdevices (built-in-MDM enrollment posture: FileVault, firewall, last check-in, storage, lock), get user|usergroup|app|package|mdmserver detail, get blueprint with all six member collections resolved to names, status device <serial> (which blueprints/configs apply to this machine), and -o csv on every list.
  • Deployment plumbing, gated. assign|unassign --server (bulk device→MDM assignment with --wait activity polling), blueprint + MDM-server lifecycle commands — same confirm gates as every other write.
  • Enterprise-grade engineering. Cobra CLI, AGPL-3.0-or-later, race-tested unit + httptest suite, golangci-lint clean, gated live integration tests, and a Makefile — Linux/macOS CI.

abgui — desktop app

abgui puts a Flutter control plane on top of abctl, for macOS, Windows and Linux. It ships as one self-contained bundle with abctl embedded inside it (no separate install, no PATH), reuses your abctl connection contexts, and re-implements none of the API — it shells out to the embedded CLI, decodes its JSON, and renders it.

  • Connect from the GUI (no shell needed): a Settings window (⌘,) to enter your Apple Business API Client ID + EC private key (paste the PEM or point at a .pem) and save it as an abctl context — so a Finder-launched app that inherits no AB_* environment still authenticates. The key is stored 0600 and handed to abctl as a file path (never on argv/logs); switch tenants from the same picker. Credentials live outside the app bundle — the context in ~/.abctl/contexts.yaml and a pasted key under ~/Library/Application Support/abgui/keys/ — so they survive app updates (replacing abgui.app doesn't touch them). Only actively deleting those files, or an uninstaller that does, removes them.
  • Browse — read-only where the API is: Configurations & Blueprints, plus clearly-badged read-only views of Devices, Enrolled Devices (built-in-MDM posture: FileVault, firewall, last check-in), Users (with roles), User Groups, Apps (catalog), Packages, MDM Servers, and the audit log — grouped in the sidebar as GitOps vs Read-only, under a Dashboard of click-through stat tiles. Every list has search, column sort, and CSV export; every entity opens a labeled detail sheet (device → assigned MDM server, opt-in AppleCare coverage, and which blueprints/configs apply; user → roles + HR fields; group → members; blueprint → all six member collections) with a raw-JSON fallback. (There is intentionally no VPP content-token screen: those tokens connect external MDM services and conflict with the primary organization’s built-in-management ownership. Apps are managed through Apple Business Blueprints.)
  • The GitOps hero: a visual 3-way diff / drift view and a gated sync --apply. abgui defaults to Git source-of-truth with deletes/detaches enabled, uses smart Apple refresh by default, and exposes --prune, --limit-writes, refresh mode, and verification mode in the apply sheet. Git source of truth reads as a literal ON / OFF pill and never flips on a stray click — the switch spells out what the new mode does to the tenant, waits for a confirmation, then announces the change in a banner.
  • Verify before you apply: a Verify Configs sheet runs the credential-free validate --json over the workspace and lists every failing profile, warning, and dangling blueprint reference — with its code — before anything is pushed. It informs rather than blocks: a failed report still lets you apply, behind one more confirm.
  • Write, gated: create / edit / delete configs, attach / detach blueprint membership, and multi-select device Assign/Unassign to an MDM server (with activity-status check) — each behind an in-app confirm (abctl is still invoked with its own --yes gate and archive-on-overwrite).
  • Shows the CLI it runs: every gated sheet previews the exact abctl command it will shell out (built from the same argv the client executes, so the two can't drift), the GitOps progress logs narrate $ abctl sync … and → exit 0 in 2.4s as work proceeds, and a Command Log page lists every invocation of the session — with its working directory, exit code, and duration — copyable one at a time or as one shell script. Secrets are redacted before a command is ever recorded, so an administrator can watch, copy, and replicate abgui in a terminal.
  • Says what went wrong, in one line: the Apply sheet is topped by a verdict banner that can't be scrolled away — Applied N change(s) · Applied N, M failed · Sync FAILED — carrying abctl's own short reason (including a 2xx-but-not-persisted verdict, which arrives on stderr with every item still marked done) and a Copy Error button that pastes the headline, the failing rows and the log path in one go.
  • Logs you can actually copy: every progress pane is one selectable transcript with a Copy Log button and an expand toggle, and each run is also written to a plain-text log in ~/Library/Logs/abgui/ — path shown, copyable, revealable in Finder — with a self-describing header (abgui/abctl versions, context, workspace, the redacted command), an outcome footer, and 50-file / 14-day / 20 MiB retention.
  • Archive / rollback: browse every pre-overwrite live version abctl archived and restore one in a click.
  • Distribution: macOS builds are Developer ID-signed and notarized when the Apple signing secrets are configured; Windows and Linux ship unsigned. Build with make gui-macos / make gui-windows / make gui-linux — each on its own platform, since Flutter does not cross-compile desktop targets.

See the port plan in docs/abgui-flutter-port.md and the original design in docs/abgui-design.md.

Captured from the Flutter build on Windows. The same app runs on macOS and Linux — abgui is one codebase across all three, with abctl embedded inside each bundle.

Install & build

Requires Go 1.26+ (the race-tested suite needs a C compiler / CGO).

make build          # → bin/abctl (version injected via ldflags)
make test           # go test -race ./...
make lint           # golangci-lint
./bin/abctl --help

No production credentials are needed to build or test — the unit suite mocks the API with httptest.

Prerequisites (Apple Business)

  1. Enable built-in ("Included") MDM for your organization in Apple Business → Settings. Without it, every /configurations and /blueprints request returns 403 INCLUDED_MDM_NOT_ENABLED — this is an org capability toggle, not a key permission (verified live).
  2. Create an API account: Apple Business → Settings → Integrations → API → create an account and download the private key once (it cannot be re-downloaded). SEC1 or PKCS#8 EC P-256 both work.
  3. If the account was migrated from ABM/Essentials, grant it Manage Blueprints + Create/edit device configurations and regenerate the key (otherwise 403).

Users and user groups are read-only via the API (create/edit only in the console or via federation/SCIM). abctl never writes identity.

Configure

Copy the template to a gitignored .env and fill it in:

cp .env.example .env
AB_CLIENT_ID=BUSINESSAPI.<uuid>
AB_PRIVATE_KEY=secrets/key.pem     # unencrypted EC P-256 — SEC1 or PKCS#8, abctl reads either

There is no Key ID: the client-assertion JWT deliberately omits kid (a kid400 invalid_client), and aud is the /v2/token form — both handled for you. Verify with:

abctl auth whoami

Quick start (the GitOps loop)

abctl seed                 # 1. download the live tenant → gitops/ tree + committed baseline
git add gitops/            #    review, then commit the tree as your source of truth
                           # 2. edit gitops/lib/**/*.mobileconfig and gitops/blueprints/*.yml in git
abctl validate             #    check those files locally — no credentials, no tenant calls
abctl diff                 # 3. see the 3-way plan (configs + blueprint membership); no writes
abctl sync                 # 4. same plan (dry-run is the default) — nothing is written
abctl sync --apply         # 5. execute it: archive-before-overwrite, confirm before any write

sync --apply runs in two phases — configs first (create/update/pull/delete), then blueprint membership (so a profile created in step 5 can be attached in the same run). It asks you to type yes before touching the tenant unless --yes (or $ABCTL_APPROVE=1 for CI) is set.

For a one-way GitOps converge where the repo is authoritative, use:

abctl diff --git-source-of-truth
abctl sync --apply --git-source-of-truth --prune

--git-source-of-truth governs both halves of the reconcile, and the two behave the same way. Without it, sync is additive: a config that exists only in Apple is pulled into gitops/lib/, and a blueprint member attached only in Apple is adopted into gitops/blueprints/<bp>.yml. With it, gitops/ is the complete desired state: that same config is deleted and that same member is detached. On apply it implies prune in the engine, and spelling --prune keeps the intent obvious.

Adopting is also available on its own, for a single member — the way to keep a change you made in the Apple Business console: abctl adopt config <name> --blueprint <bp> writes only the manifest and never the tenant. Run it from the workspace: like every tree command, it resolves gitops/ against the working directory.

The default live-read behavior is --refresh=smart: abctl performs a cheap Apple metadata list, reuses cached profile hashes when the Apple ID and updatedDateTime match the committed baseline, and fetches profile XML only when comparison, pulling, pruning, or archive-before-overwrite safety requires it.

On-disk layout (gitops/)
gitops/
├── lib/…/<name>.mobileconfig         # CUSTOM_SETTING profiles — the desired state, one file per config
├── blueprints/<slug>.yml             # per-blueprint: name, id, and its list of attached config names
├── state/sync-state.json             # COMMITTED baseline: name → {id, hash, updatedDateTime}
└── archive/<name>/<UTC-ts>--<reason>.mobileconfig (+ .json sidecar)   # pre-overwrite live versions

The tree is gitignored by default (seeded profiles can carry secrets); un-ignore it deliberately once you adopt it as your committed source of truth.

A blueprint manifest can manage its content plus device/user/group targets. The five newer collection keys are optional: absent means unmanaged; present (even []) means reconcile to that exact set.

name: Sales Team
id: 1c6f7213-236f-4f5b-837c-71acb16a0b9b
description: field sales macs
configurations:
  - wifi-corp.mobileconfig
  - vpn-always-on.mobileconfig
apps:
  - Pages
devices:
  - C02EXAMPLE
users:
  - rep@example.com
groups:
  - Field Sales

abctl attaches declared members missing from Apple Business and, only with --prune, detaches managed members removed from git. Blueprint lifecycle and all six relationship collections are implemented and gated; app/package/device/user/group writes are unit-tested but still await their first controlled live-device test.

Pre-sync check (abctl validate)

validate reads local files only — no credentials, no Apple Business calls, works offline (so it runs in CI before any AB_* secret is in scope, and on a workspace whose tenant was never configured). It parses every lib/ profile as an XML plist and checks the Configuration / PayloadContent structure, a top-level PayloadIdentifier, a top-level PayloadVersion of exactly 1, the 1 MiB Apple Business size cap, and an identifier declared by two profiles (they overwrite each other on the device — both files are flagged, each naming the other). Then it checks the blueprint manifests: a configurations: entry with no matching file in lib/ is an error — that's the mistake that syncs cleanly and attaches nothing. Warnings (missing-payload-uuid, missing-display-name, no-inner-payloads, inner-payload-version, approaching-size-cap at ≥ 512 KiB, a stray non-.mobileconfig file sync will ignore) never fail the run.

Why payload-version is an error and not a warning. The outer PayloadVersion is the version of the profile format, not of your content, and Apple pins it to exactly 1 (TopLevel). Apple Business accepts an upload carrying any other value with a 2xx and then silently never stores it — the live copy never changes, so every subsequent sync recomputes the same change and archives another snapshot, forever. Set it back to <integer>1</integer> and track your own revisions in git. A missing key is the same error; a per-payload PayloadVersion other than 1 is only a warning (inner-payload-version), since only the outer one is known to trigger the silent drop.

abctl validate                # tree issues, then per-file errors/warnings, then the "N profile(s): …" summary
abctl validate --json         # the machine report (also -o json | -o yaml); -o csv is rejected

Exit 1 means something failed — and on --json the report is still printed on stdout before that non-zero exit, so a CI job (or abgui) can render exactly what it just gated on. Every finding carries a stable code (size-cap, binary-plist, duplicate-identifier, missing-config, payload-version, …) plus one plain sentence. $ABCTL_VALIDATOR still hands lib/ to your own linter: on the human path it owns stdout and its exit code; on --json it runs alongside the built-in pass and lands in validator / validatorCommand / validatorExitCode / validatorOutput (16 KiB cap), where a non-zero exit fails the report.

Command reference

abctl has two modes — a GitOps engine and imperative commands — in one binary. Global flags: -o/--output table|json|yaml, --context <name> (see abctl context). Every write is gated (confirm unless --yes/$ABCTL_APPROVE); imperative writes also update the local gitops/ tree + baseline inline (--no-write-tree to skip). Full design: docs/imperative-cli.md.

# read
abctl auth whoami                                   # verify auth + reachability
abctl get configurations|blueprints|devices|audit   # + users|usergroups|apps|mdmservers  (--filter key=substr)
abctl get os-releases [--platform macOS --catalog managed] # Apple's read-only software-release catalog
abctl get audit --since 7d --type DEVICE --actor admin       # tenant activity filters
abctl status device <serial> --releases                      # opt-in OS catalog comparison
abctl get configuration <name|id> [--profile]       # show one (--profile dumps raw .mobileconfig XML)

# GitOps (declarative, whole-tree)
abctl seed                                          # live tenant → gitops/ tree + baseline
abctl validate [--json]                             # check lib/ profiles + blueprint refs (no credentials)
abctl diff | sync [--exit-on-diff] [--refresh smart|full|metadata-only]
abctl sync --apply [--git-source-of-truth] [--prune] [--yes] [--limit-writes N] [--verify targeted|full|none]

# imperative (one resource at a time)
abctl create  config <name> -f profile.mobileconfig # POST a new CUSTOM_SETTING config
abctl replace config <name|id> -f profile.mobileconfig   # archive live, then PATCH
abctl edit    config <name|id>                      # fetch → $EDITOR → PATCH on save
abctl delete  config <name|id> [--yes]              # archive live, then DELETE
abctl apply -f a.yml [-f b.yml] [--dry-run]         # upsert abctl/v1 Configuration|Blueprint specs (bulk)
abctl attach|detach config <name> --blueprint <bp>  # add/remove a config from a blueprint
abctl attach|detach app <name|id> --blueprint <bp>  # built-in-MDM Apps & Books: assign an owned app to a blueprint
abctl attach|detach device|user|group <target> --blueprint <bp> # manage blueprint targets
abctl pull [config <name>]                          # adopt a console edit into git (scoped seed)
abctl adopt <kind> <name> --blueprint <bp>           # adopt a console ATTACH into git (writes the blueprint manifest; no tenant write)

# status (honest proxies — NOT on-device install verification)
abctl status config <name>                          # which blueprints carry it + devices targeted
abctl status audit [--since 24h --type … --actor …] # config/device change history

# contexts + escape hatch
abctl context set|use|get|list|current              # named-connection tenants (~/.abctl/contexts.yaml)
abctl api <path> [-X POST -F k=v --input body.json] # raw request (non-GET is gated)
abctl version | completion | help

Flags that matter: --apply (execute; default is dry-run) · --prune (allow deletes/detaches; off by default) · --yes / $ABCTL_APPROVE=1 (skip the confirm, for CI) · --limit-writes N (circuit breaker on tenant writes, shared across configs + blueprints) · --exit-on-diff (exit 3 when changes are pending — for PR gating).

Current sync controls also include --git-source-of-truth (treat gitops/ as authoritative), --refresh smart|full|metadata-only (smart is the default cheap-list/cache mode), and --verify targeted|full|none after apply (targeted is the default).

What --verify now checks. All three modes answer one question — does the tenant match git? — and differ only in what they read. The apply itself already confirms each write as it makes it (one detail GET per configuration actually written), so targeted refreshes blueprint membership and re-reads only the writes that could not be confirmed — normally none, and never a fan-out over the tenant, because Apple rate-limits hard. full re-reads every configuration and blueprint and compares the written ones against the refresh it already paid for (no extra calls). none reads nothing and prints no verdict. A config that can't be read back at all counts as a mismatch, not as verified. Mismatches print one of two lines — post-apply verification FAILED: <name> still differs from desired on Apple Business when Apple showed us a difference, or post-apply verification FAILED: <name> could NOT be verified … when it never answered — and exit 1. Grep for the uppercase FAILED, which both carry. The verdict is printed after the per-item result table (or the --json object), so a failed run never hides what was written.

Exit codes: 0 ok · 1 error · 2 usage · 3 changes pending (with --exit-on-diff). Data → stdout, diagnostics → stderr; --json for machine output.

Safety model

  • Read-only by defaultdiff and sync (no --apply) never write.
  • Every write gated behind --apply + confirmation; --prune off by default (nothing is deleted or detached unless you ask).
  • Archive-before-overwrite — a failed archive skips the write it protects, so the audit trail is never bypassed.
  • Read-back-and-confirm — the sync baseline records what Apple stored (re-read after the write), never what was sent. A write Apple accepted but didn't persist is reported as an error with its archived copy, and the baseline is left alone so the next run re-checks it — a stale baseline costs one redundant, archived re-write; an optimistic one hides real drift.
  • Newest-wins conflicts compare the live updatedDateTime against the git file's commit time (else its mtime); an ambiguous case is skipped, never guessed.
  • Never commit secrets.env, secrets/, keys, tokens, and the generated gitops/ tree are all gitignored. See SECURITY.md.

CI/CD

Works on both GitHub and GitLab.github/workflows/ and .gitlab-ci.yml drive the identical flow (abctl is CI-agnostic: reads AB_* from the environment, signals via exit codes). Full setup — including the GitLab job map — in docs/cicd.md.

CI runs build/vet/race-test on Linux + macOS, golangci-lint, and gated live integration tests (read-only + a strictly-gated write round-trip) that self-skip without secrets.

CD is three GitOps pipelines (also self-skipping); the GitHub workflows:

Workflow Trigger Does
Plan (cd-plan.yml) PR touching gitops/** sync --dry-run → posts the plan as a PR comment (no writes)
Apply (cd-apply.yml) merge to main (or manual) gated sync --apply behind a protected production environment → reconciles + commits the baseline back
Drift (cd-drift.yml) daily cron sync --dry-run --exit-on-diff → alerts if the console drifted from git

On GitLab the same three run as the plan / apply / drift jobs in .gitlab-ci.yml (MR / manual on a protected production environment / pipeline schedule). In CI either way, abctl reads config from the environment (AB_CLIENT_ID + AB_PRIVATE_KEY) — no .env needed.

Run it locally too. scripts/pipeline.sh drives the identical stages on your own machine — no CI required — so you can plan/apply/drift against the current branch by hand:

./scripts/pipeline.sh ci      # build + gofmt + vet + test (+ lint) — no secrets
./scripts/pipeline.sh plan    # READ-ONLY: what `apply` would change
./scripts/pipeline.sh drift   # READ-ONLY: exit 3 if git and the tenant diverge
./scripts/pipeline.sh apply   # LIVE, gated writes (--commit to mirror CI's baseline commit-back)

It's the same logic the workflows run, so local and CI never disagree.

Verified API facts (from live testing — trust these)

  • Auth omits kid; aud = …/oauth2/v2/token; bearer TTL 60 min.
  • Only CUSTOM_SETTING configs are API-writable; their profile XML is raw (not base64) and GET round-trips it byte-identically → drift = raw SHA-256. POST 201 / PATCH 200 / DELETE 204.
  • The API validates uploads: a malformed profile (e.g. empty PayloadContent) → 400 PARAMETER_ERROR.
  • That validation is not exhaustive, and the gap fails silently: Apple can answer a write 2xx and then not store the profile at all (stored XML and updatedDateTime unchanged, no error). Confirmed trigger: a top-level PayloadVersion1 — Apple requires exactly 1 (TopLevel). Never treat a 2xx as proof of persistence; abctl reads the config back instead.
  • A Blueprint create requires both a member (device/user/group) and content (app/package/config).
  • Blueprint membership POST is additive (merges); DELETE-with-body removes a specific member — so abctl converges membership per-member.
  • Users/userGroups are read-only via the API; built-in MDM must be enabled in the console.
  • The API rate-limits hard — abctl backs off (Retry-After aware); avoid rapid loops.

Docs

Built by AI

abcli, including abctl and abgui, is AI-authored under Gigaion, LLC's direction. Review it with the same judgment you would apply to any production infrastructure tool.

License

AGPL-3.0-or-later — Copyright © 2026 Gigaion, LLC.

Directories

Path Synopsis
cmd
abctl command
Command abctl is a GitOps CLI for the Apple Business API (Configurations + Blueprints).
Command abctl is a GitOps CLI for the Apple Business API (Configurations + Blueprints).
internal
ab
Package ab is the Apple Business API client: ES256 client-assertion auth + typed read methods.
Package ab is the Apple Business API client: ES256 client-assertion auth + typed read methods.
archive
Package archive is the audit safety net for bidirectional sync.
Package archive is the audit safety net for bidirectional sync.
cli
Package cli is the abctl command surface, built on Cobra.
Package cli is the abctl command surface, built on Cobra.
config
Package config loads the Apple Business API settings from the gitignored .env.
Package config loads the Apple Business API settings from the gitignored .env.
gdmf
Package gdmf reads Apple's public software-update catalog.
Package gdmf reads Apple's public software-update catalog.
gitops
Package gitops is the on-disk desired-state tree: lib/ profiles, blueprint manifests, and the committed baseline.
Package gitops is the on-disk desired-state tree: lib/ profiles, blueprint manifests, and the committed baseline.
hash
Package hash is the drift signal.
Package hash is the drift signal.
reconcile
Package reconcile computes the 3-way plan (git desired ↔ committed baseline ↔ live ABM) for bidirectional, newest-wins sync.
Package reconcile computes the 3-way plan (git desired ↔ committed baseline ↔ live ABM) for bidirectional, newest-wins sync.
state
Package state is the committed sync baseline (gitops/state/sync-state.json) — the last-synced snapshot that makes the 3-way (git ↔ baseline ↔ ABM) diff possible.
Package state is the committed sync baseline (gitops/state/sync-state.json) — the last-synced snapshot that makes the 3-way (git ↔ baseline ↔ ABM) diff possible.
vpp
Package vpp is a read client for Apple's App and Book Management API v2 (vpp.itunes.apple.com/mdm/v2) — the Apps & Books license inventory: which apps/books the organization owns licenses for, how many are free/assigned, and to whom.
Package vpp is a read client for Apple's App and Book Management API v2 (vpp.itunes.apple.com/mdm/v2) — the Apps & Books license inventory: which apps/books the organization owns licenses for, how many are free/assigned, and to whom.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL