acumatica-cli 0.29.0__tar.gz → 0.31.0__tar.gz

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.
Files changed (77) hide show
  1. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/PKG-INFO +27 -30
  2. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/README.md +26 -29
  3. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/pyproject.toml +1 -1
  4. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/pyproject.toml.orig +1 -1
  5. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/cli.py +45 -213
  6. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/reconcile.py +2 -2
  7. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/README.md +3 -4
  8. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/__init__.py +0 -0
  9. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/bootstrap.py +0 -0
  10. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
  11. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/bootstrap_project.xml +0 -0
  12. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/client.py +0 -0
  13. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/config.py +0 -0
  14. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/extract.py +0 -0
  15. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/firstlogin.py +0 -0
  16. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/inventory.py +0 -0
  17. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/models.py +0 -0
  18. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/output.py +0 -0
  19. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/run.py +0 -0
  20. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/seed.py +0 -0
  21. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/seed_catalog.yaml +0 -0
  22. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/snapshot_map.yaml +0 -0
  23. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/state.py +0 -0
  24. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/baseline/10-subaccounts.yaml +0 -0
  25. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/baseline/20-accounts.yaml +0 -0
  26. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/baseline/40-ledger.yaml +0 -0
  27. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/baseline/50-gl-preferences.yaml +0 -0
  28. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/baseline/60-ledger-company.yaml +0 -0
  29. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/baseline/90-uoms.yaml +0 -0
  30. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/baseline/91-company-packaging.yaml +0 -0
  31. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/bootstrap/company.yaml +0 -0
  32. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/bootstrap/credit-terms.yaml +0 -0
  33. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/bootstrap/features.yaml +0 -0
  34. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/bootstrap/segmented-key.yaml +0 -0
  35. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/05-numbering-sequences.yaml +0 -0
  36. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/10-reason-codes.yaml +0 -0
  37. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/20-in-preferences.yaml +0 -0
  38. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/30-availability-rules.yaml +0 -0
  39. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/40-posting-classes.yaml +0 -0
  40. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/50-warehouse.yaml +0 -0
  41. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/51-warehouse-locations.yaml +0 -0
  42. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/52-warehouse-defaults.yaml +0 -0
  43. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/53-tax-categories.yaml +0 -0
  44. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/54-item-classes.yaml +0 -0
  45. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/56-so-preferences.yaml +0 -0
  46. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/57-po-preferences.yaml +0 -0
  47. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/58-order-types.yaml +0 -0
  48. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/60-ar-preferences.yaml +0 -0
  49. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/61-ap-preferences.yaml +0 -0
  50. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/62-ca-preferences.yaml +0 -0
  51. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/63-cash-account.yaml +0 -0
  52. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/64-payment-methods.yaml +0 -0
  53. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/65-statement-cycles.yaml +0 -0
  54. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/70-vendor-classes.yaml +0 -0
  55. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/71-customer-classes.yaml +0 -0
  56. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/75-vendors.yaml +0 -0
  57. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/76-customers.yaml +0 -0
  58. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/80-stock-items-parts.yaml +0 -0
  59. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/82-stock-items-kits.yaml +0 -0
  60. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/85-kit-specifications.yaml +0 -0
  61. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/90-roles.yaml +0 -0
  62. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/91-users.yaml +0 -0
  63. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/master/92-role-users.yaml +0 -0
  64. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/setup/10-financial-year.yaml +0 -0
  65. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/setup/20-master-calendar.yaml +0 -0
  66. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/setup/30-open-periods.yaml +0 -0
  67. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/config/views/10-trial-balance.yaml +0 -0
  68. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/env +0 -0
  69. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/gitignore +0 -0
  70. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/overlays/README.md +0 -0
  71. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/overlays/default-24.200.001/README.md +0 -0
  72. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/overlays/default-24.200.001/scenario/30-build.yaml +0 -0
  73. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/scenario/10-seed-capital.yaml +0 -0
  74. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/scenario/20-buy.yaml +0 -0
  75. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/scenario/30-build.yaml +0 -0
  76. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/templates/scenario/40-sell.yaml +0 -0
  77. {acumatica_cli-0.29.0 → acumatica_cli-0.31.0}/src/acumatica_cli/tenant.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acumatica-cli
3
- Version: 0.29.0
3
+ Version: 0.31.0
4
4
  Summary: Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data
5
5
  Author: Konstantin Borovik
6
6
  Author-email: Konstantin Borovik <kb@lab5.ca>
@@ -40,21 +40,21 @@ YAML in git is the source of truth; the live tenant is the target. `apply` is th
40
40
  | ------- | ------- | ------ |
41
41
  | Seed a tenant from YAML | `apply` | Idempotent PUT of `config/` into the live tenant |
42
42
  | Detect drift | `diff` | Compare seed YAML to live (exit 2 when they diverge) |
43
- | Pull live config into YAML | `extract` | Inverse of `apply` — GET into `config/{bootstrap,baseline,setup,master}/` |
43
+ | Pull live config into YAML | `survey extract` | Inverse of `apply` — GET into `config/{bootstrap,baseline,setup,master}/` |
44
44
  | Run transaction scripts | `run` | Forward documents from `scenario/` (capital, buy, build, sell) |
45
45
  | Capture derived balances | `state` | Inquire trial-balance etc. into `state/` (not seed) |
46
46
  | Create and destroy tenants | `tenant` | SSH control plane: list / create / delete / recycle |
47
47
  | Publish the Bootstrap contract | `bootstrap` | REST publish of AcuBootstrap (or `--export` zip for the UI) |
48
- | Prove a cold rebuild | `check` | create, then apply, then run, then diff on a fresh tenant |
49
- | Snapshot a site offline | `inventory` | SM203520 XML ZIP or `ac.exe export xml` writes `inventory/` |
50
- | Cross-check snapshot vs seed | `reconcile` | `inventory/` plus optional `config/` writes `findings/` only |
48
+ | Prove a cold rebuild | `tenant create` then `apply` then `run` then `diff` | Compose on a fresh tenant. No wrap command. |
49
+ | Snapshot a site offline | `survey inventory` | SM203520 XML ZIP or `ac.exe export xml` writes `inventory/` |
50
+ | Cross-check snapshot vs seed | `survey reconcile` | `inventory/` plus optional `config/` writes `findings/` only |
51
51
  | Dump the contract schema | `schema` | OpenAPI `swagger.json` for the pinned endpoint |
52
52
  | Scaffold a data repo | `config init` | Full `config/`, `scenario/`, `.env` tree |
53
53
  | Preflight a target | `config check` | Discovery, secrets, REST, endpoints, SSH |
54
54
 
55
55
  Seed YAML covers features, company, credit terms, subaccounts, chart of accounts, ledger, UOMs, financial year / calendar / periods, numbering sequences, inventory and distribution masters, roles, and users.
56
56
 
57
- REST is the data plane (`apply`, `diff`, `run`, `extract`, `state`, `bootstrap`, `schema`).
57
+ REST is the data plane (`apply`, `diff`, `run`, `survey extract`, `state`, `bootstrap`, `schema`).
58
58
  SSH is the control plane (`tenant *`).
59
59
 
60
60
  Hosted sites skip SSH.
@@ -76,7 +76,6 @@ acu --tenant DEV apply config/ # seed config/{bootstrap,baseline,setup
76
76
  acu --tenant DEV run scenario/ # once capital → buy → build → sell
77
77
  acu --tenant DEV diff config/ # prove zero drift (exit 2 on drift)
78
78
  acu --tenant DEV state # capture state/ trial-balance
79
- acu check --yes --tenant DEV # cold lifecycle create→apply→run→diff (leave tenant)
80
79
  ```
81
80
 
82
81
  Bare `apply` / `diff` (no path args) also prefer `config/` when those trees exist.
@@ -115,14 +114,14 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
115
114
  ├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
116
115
  ├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
117
116
  ├── run [--dry-run] [FILES...] execute transaction scenario YAML (exit 1 on any miss)
118
- ├── check [--yes] [--tenant L] cold lifecycle create→apply→run→diff; leave tenant (V47)
119
117
  ├── state [--out DIR] [--diff] [--assert-unchanged] [--dry-run] [FILES...]
120
118
  │ capture derived state into state/ (not seed)
121
- ├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
122
- │ inverse of apply into config/{bootstrap,baseline,setup,master}/
123
- ├── inventory [--out DIR] [--force] [--dry-run] ARTIFACT
124
- │ offline snapshot artifact → inventory/ (not seed)
125
- ├── reconcile [--inventory DIR] [--config DIR] [--out DIR] [--force] [--dry-run]
119
+ ├── survey existing-tenant dual-reader (never L1 extract/inventory/reconcile)
120
+ │ ├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
121
+ │ │ inverse of apply into config/{bootstrap,baseline,setup,master}/
122
+ │ ├── inventory [--out DIR] [--force] [--dry-run] ARTIFACT
123
+ │ │ offline snapshot artifact → inventory/ (not seed)
124
+ │ └── reconcile [--inventory DIR] [--config DIR] [--out DIR] [--force] [--dry-run]
126
125
  │ inventory/ + optional config/ → findings/ only
127
126
  ├── schema [--out DIR] dump the endpoint's OpenAPI schema (swagger.json)
128
127
  │
@@ -143,12 +142,12 @@ When you omit FILES:
143
142
  Scenario YAML may use `${current_period}` (host-local `MMyyyy`) on steps, expect params, and `once.present` params.
144
143
  `config/views` and `state` keep Period pinned — see [docs/demo-seed.md](docs/demo-seed.md#period-token-current_period-vs-pinned-views).
145
144
 
146
- `extract` always writes under `config/{bootstrap,baseline,setup,master}/` (catalog-driven; never root SEED_DIRS).
145
+ `survey extract` always writes under `config/{bootstrap,baseline,setup,master}/` (catalog-driven; never root SEED_DIRS).
147
146
 
148
- `inventory` and `reconcile` are offline (no REST, SSH, or password).
149
- `inventory` turns an SM203520 Settings XML ZIP or `ac.exe export xml` folder into `inventory/`.
147
+ `survey inventory` and `survey reconcile` are offline (no REST, SSH, or password).
148
+ `survey inventory` turns an SM203520 Settings XML ZIP or `ac.exe export xml` folder into `inventory/`.
150
149
 
151
- `reconcile` compares `inventory/` to optional `config/` and writes `findings/` only.
150
+ `survey reconcile` compares `inventory/` to optional `config/` and writes `findings/` only.
152
151
  It never writes seed.
153
152
 
154
153
  Optional `snapshot_map.yaml` (data-repo root, or package defaults) maps DAC tables to catalog entities.
@@ -170,9 +169,9 @@ Do not confuse them with each other or with `state`:
170
169
 
171
170
  | Command | Plane | Input | Writes | Role |
172
171
  | ------- | ----- | ----- | ------ | ---- |
173
- | `extract` | REST (live) | tenant via contract API | `config/{bootstrap,baseline,setup,master}/` | Inverse of `apply` — seed YAML |
174
- | `inventory` | Offline | SM203520 Settings XML ZIP or `ac.exe export xml` folder | `inventory/` (`summary.yaml` + `tables/`) | Full-table snapshot IR — not seed |
175
- | `reconcile` | Offline | `inventory/` + optional `config/` | `findings/` only | Cross-check gaps/deltas — never mutates seed or tenant |
172
+ | `survey extract` | REST (live) | tenant via contract API | `config/{bootstrap,baseline,setup,master}/` | Inverse of `apply` — seed YAML |
173
+ | `survey inventory` | Offline | SM203520 Settings XML ZIP or `ac.exe export xml` folder | `inventory/` (`summary.yaml` + `tables/`) | Full-table snapshot IR — not seed |
174
+ | `survey reconcile` | Offline | `inventory/` + optional `config/` | `findings/` only | Cross-check gaps/deltas — never mutates seed or tenant |
176
175
  | `state` | REST (live) | `config/views/` | `state/` | Derived balances/totals — not seed, not inventory |
177
176
  | `apply` | REST (live) | seed YAML under `config/` | tenant | **Sole** tenant writer (keyed PUT) |
178
177
 
@@ -200,8 +199,8 @@ There is no `--flavor`.
200
199
  | `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build, sell |
201
200
  | `config/views/` | observer views for `acu state` (`inquire:` / `entity:` / `gi:`; not SEED_DIRS) |
202
201
  | `state/` | committed derived-state observations (evidence, not seed; money/qty fixed-point) |
203
- | `inventory/` | engagement: offline snapshot tables from `acu inventory` (not seed; not SEED_DIRS) |
204
- | `findings/` | engagement: `acu reconcile` cross-check output (never apply path) |
202
+ | `inventory/` | engagement: offline snapshot tables from `acu survey inventory` (not seed; not SEED_DIRS) |
203
+ | `findings/` | engagement: `acu survey reconcile` cross-check output (never apply path) |
205
204
  | `.env` | secrets + where (`ACU_BASE_URL`) + pin (`ACU_API_VERSION`) |
206
205
 
207
206
  Legacy data repos may still keep root `bootstrap/`…`master/`; bare `apply`/`diff` prefer `config/` when present and never merge both trees.
@@ -210,7 +209,7 @@ Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-
210
209
  Commit seed YAML; keep `.env` out of git (scaffold `.gitignore` already lists it).
211
210
 
212
211
  Seed YAML is state: `apply` upserts it, `diff` proves it.
213
- `acu extract` is the inverse of `apply`: GET live tenant rows into seed YAML under `config/{bootstrap,baseline,setup,master}/` (hard-cut).
212
+ `acu survey extract` is the inverse of `apply`: GET live tenant rows into seed YAML under `config/{bootstrap,baseline,setup,master}/` (hard-cut).
214
213
 
215
214
  Packaged `seed_catalog.yaml` is the sole extract registry (entity, endpoint, keys, file, strip/include, filter-split).
216
215
  The demo entity map in [docs/demo-seed.md](docs/demo-seed.md) mirrors those catalog paths.
@@ -222,7 +221,7 @@ Row failures continue (exit 1 only if any row failed).
222
221
  Drift stays with `diff`.
223
222
 
224
223
  ```sh
225
- acu --tenant DEV extract --out . --force # refresh config/** from live tenant
224
+ acu --tenant DEV survey extract --out . --force # refresh config/** from live tenant
226
225
  git diff config/ # review extract delta before commit
227
226
  acu --tenant DEV apply config/ # replay extracted seed
228
227
  acu --tenant DEV diff config/ # expect exit 0
@@ -285,7 +284,6 @@ Ad-hoc override: `acu --api-version 24.200.001 …` (version half only, never
285
284
  `base_url` resolves as `--url`, else `ACU_BASE_URL`, else a hard error naming sources.
286
285
 
287
286
  `acu config show` prints the resolved `.env` (password excluded; includes `ACU_API_VERSION`).
288
- `acu check` (lifecycle) uses the same resolved instance.
289
287
 
290
288
  ### Multi-host overlays (V44)
291
289
 
@@ -317,9 +315,6 @@ acu diff
317
315
  # explicit path args (no auto) — later path wins same keys
318
316
  acu apply config/ overlays/default-24.200.001/
319
317
  acu diff config/ overlays/default-24.200.001/
320
-
321
- # cold lifecycle vs the resolved .env instance (SSH + tenant required)
322
- acu check --yes --tenant LAB5
323
318
  ```
324
319
 
325
320
  Add a future half by creating `overlays/default-<new-half>/` with the
@@ -335,7 +330,7 @@ Worth knowing:
335
330
  - Without a `.env`, global flags plus the process environment supply the configuration.
336
331
  - When `ACU_SSH` is **absent**, acu defaults to `Administrator@` + the resolved base_url hostname.
337
332
  A **present blank** `ACU_SSH=` is the hosted opt-out.
338
- Only `acu tenant` / `acu check` require a non-empty value post-default.
333
+ Only `acu tenant` requires a non-empty value post-default.
339
334
  - `acu config show` prints the resolved `.env` (password excluded).
340
335
  - Redirect it to turn resolved state into a working config: `acu config show > .env`.
341
336
 
@@ -383,7 +378,9 @@ gmake release patch # or minor | major
383
378
  2. Fail if `## Unreleased` has no bullets
384
379
  3. Bump `pyproject.toml` version (`major` | `minor` | `patch`)
385
380
  4. Promote Unreleased body to `## [vX.Y.Z] - YYYY-MM-DD`, leave an empty `## Unreleased`
386
- 5. Commit `CHANGELOG.md` + `pyproject.toml` (+ lock if bumped) together, tag `vX.Y.Z`, push
381
+ 5. Commit `CHANGELOG.md` + `pyproject.toml` (+ lock if bumped) together
382
+ 6. `gmake check` again on the promoted tree — fail keeps the commit, creates no tag, pushes nothing
383
+ 7. Tag `vX.Y.Z` and push branch + that tag — GitHub Actions publishes
387
384
 
388
385
  GitHub Actions on tag `v*` re-runs CI, builds sdist+wheel, publishes to PyPI via OIDC trusted publishing, and creates a GitHub Release whose notes are the promoted CHANGELOG section for that tag (plus the artifacts).
389
386
 
@@ -22,21 +22,21 @@ YAML in git is the source of truth; the live tenant is the target. `apply` is th
22
22
  | ------- | ------- | ------ |
23
23
  | Seed a tenant from YAML | `apply` | Idempotent PUT of `config/` into the live tenant |
24
24
  | Detect drift | `diff` | Compare seed YAML to live (exit 2 when they diverge) |
25
- | Pull live config into YAML | `extract` | Inverse of `apply` — GET into `config/{bootstrap,baseline,setup,master}/` |
25
+ | Pull live config into YAML | `survey extract` | Inverse of `apply` — GET into `config/{bootstrap,baseline,setup,master}/` |
26
26
  | Run transaction scripts | `run` | Forward documents from `scenario/` (capital, buy, build, sell) |
27
27
  | Capture derived balances | `state` | Inquire trial-balance etc. into `state/` (not seed) |
28
28
  | Create and destroy tenants | `tenant` | SSH control plane: list / create / delete / recycle |
29
29
  | Publish the Bootstrap contract | `bootstrap` | REST publish of AcuBootstrap (or `--export` zip for the UI) |
30
- | Prove a cold rebuild | `check` | create, then apply, then run, then diff on a fresh tenant |
31
- | Snapshot a site offline | `inventory` | SM203520 XML ZIP or `ac.exe export xml` writes `inventory/` |
32
- | Cross-check snapshot vs seed | `reconcile` | `inventory/` plus optional `config/` writes `findings/` only |
30
+ | Prove a cold rebuild | `tenant create` then `apply` then `run` then `diff` | Compose on a fresh tenant. No wrap command. |
31
+ | Snapshot a site offline | `survey inventory` | SM203520 XML ZIP or `ac.exe export xml` writes `inventory/` |
32
+ | Cross-check snapshot vs seed | `survey reconcile` | `inventory/` plus optional `config/` writes `findings/` only |
33
33
  | Dump the contract schema | `schema` | OpenAPI `swagger.json` for the pinned endpoint |
34
34
  | Scaffold a data repo | `config init` | Full `config/`, `scenario/`, `.env` tree |
35
35
  | Preflight a target | `config check` | Discovery, secrets, REST, endpoints, SSH |
36
36
 
37
37
  Seed YAML covers features, company, credit terms, subaccounts, chart of accounts, ledger, UOMs, financial year / calendar / periods, numbering sequences, inventory and distribution masters, roles, and users.
38
38
 
39
- REST is the data plane (`apply`, `diff`, `run`, `extract`, `state`, `bootstrap`, `schema`).
39
+ REST is the data plane (`apply`, `diff`, `run`, `survey extract`, `state`, `bootstrap`, `schema`).
40
40
  SSH is the control plane (`tenant *`).
41
41
 
42
42
  Hosted sites skip SSH.
@@ -58,7 +58,6 @@ acu --tenant DEV apply config/ # seed config/{bootstrap,baseline,setup
58
58
  acu --tenant DEV run scenario/ # once capital → buy → build → sell
59
59
  acu --tenant DEV diff config/ # prove zero drift (exit 2 on drift)
60
60
  acu --tenant DEV state # capture state/ trial-balance
61
- acu check --yes --tenant DEV # cold lifecycle create→apply→run→diff (leave tenant)
62
61
  ```
63
62
 
64
63
  Bare `apply` / `diff` (no path args) also prefer `config/` when those trees exist.
@@ -97,14 +96,14 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
97
96
  ├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
98
97
  ├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
99
98
  ├── run [--dry-run] [FILES...] execute transaction scenario YAML (exit 1 on any miss)
100
- ├── check [--yes] [--tenant L] cold lifecycle create→apply→run→diff; leave tenant (V47)
101
99
  ├── state [--out DIR] [--diff] [--assert-unchanged] [--dry-run] [FILES...]
102
100
  │ capture derived state into state/ (not seed)
103
- ├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
104
- │ inverse of apply into config/{bootstrap,baseline,setup,master}/
105
- ├── inventory [--out DIR] [--force] [--dry-run] ARTIFACT
106
- │ offline snapshot artifact → inventory/ (not seed)
107
- ├── reconcile [--inventory DIR] [--config DIR] [--out DIR] [--force] [--dry-run]
101
+ ├── survey existing-tenant dual-reader (never L1 extract/inventory/reconcile)
102
+ │ ├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
103
+ │ │ inverse of apply into config/{bootstrap,baseline,setup,master}/
104
+ │ ├── inventory [--out DIR] [--force] [--dry-run] ARTIFACT
105
+ │ │ offline snapshot artifact → inventory/ (not seed)
106
+ │ └── reconcile [--inventory DIR] [--config DIR] [--out DIR] [--force] [--dry-run]
108
107
  │ inventory/ + optional config/ → findings/ only
109
108
  ├── schema [--out DIR] dump the endpoint's OpenAPI schema (swagger.json)
110
109
  │
@@ -125,12 +124,12 @@ When you omit FILES:
125
124
  Scenario YAML may use `${current_period}` (host-local `MMyyyy`) on steps, expect params, and `once.present` params.
126
125
  `config/views` and `state` keep Period pinned — see [docs/demo-seed.md](docs/demo-seed.md#period-token-current_period-vs-pinned-views).
127
126
 
128
- `extract` always writes under `config/{bootstrap,baseline,setup,master}/` (catalog-driven; never root SEED_DIRS).
127
+ `survey extract` always writes under `config/{bootstrap,baseline,setup,master}/` (catalog-driven; never root SEED_DIRS).
129
128
 
130
- `inventory` and `reconcile` are offline (no REST, SSH, or password).
131
- `inventory` turns an SM203520 Settings XML ZIP or `ac.exe export xml` folder into `inventory/`.
129
+ `survey inventory` and `survey reconcile` are offline (no REST, SSH, or password).
130
+ `survey inventory` turns an SM203520 Settings XML ZIP or `ac.exe export xml` folder into `inventory/`.
132
131
 
133
- `reconcile` compares `inventory/` to optional `config/` and writes `findings/` only.
132
+ `survey reconcile` compares `inventory/` to optional `config/` and writes `findings/` only.
134
133
  It never writes seed.
135
134
 
136
135
  Optional `snapshot_map.yaml` (data-repo root, or package defaults) maps DAC tables to catalog entities.
@@ -152,9 +151,9 @@ Do not confuse them with each other or with `state`:
152
151
 
153
152
  | Command | Plane | Input | Writes | Role |
154
153
  | ------- | ----- | ----- | ------ | ---- |
155
- | `extract` | REST (live) | tenant via contract API | `config/{bootstrap,baseline,setup,master}/` | Inverse of `apply` — seed YAML |
156
- | `inventory` | Offline | SM203520 Settings XML ZIP or `ac.exe export xml` folder | `inventory/` (`summary.yaml` + `tables/`) | Full-table snapshot IR — not seed |
157
- | `reconcile` | Offline | `inventory/` + optional `config/` | `findings/` only | Cross-check gaps/deltas — never mutates seed or tenant |
154
+ | `survey extract` | REST (live) | tenant via contract API | `config/{bootstrap,baseline,setup,master}/` | Inverse of `apply` — seed YAML |
155
+ | `survey inventory` | Offline | SM203520 Settings XML ZIP or `ac.exe export xml` folder | `inventory/` (`summary.yaml` + `tables/`) | Full-table snapshot IR — not seed |
156
+ | `survey reconcile` | Offline | `inventory/` + optional `config/` | `findings/` only | Cross-check gaps/deltas — never mutates seed or tenant |
158
157
  | `state` | REST (live) | `config/views/` | `state/` | Derived balances/totals — not seed, not inventory |
159
158
  | `apply` | REST (live) | seed YAML under `config/` | tenant | **Sole** tenant writer (keyed PUT) |
160
159
 
@@ -182,8 +181,8 @@ There is no `--flavor`.
182
181
  | `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build, sell |
183
182
  | `config/views/` | observer views for `acu state` (`inquire:` / `entity:` / `gi:`; not SEED_DIRS) |
184
183
  | `state/` | committed derived-state observations (evidence, not seed; money/qty fixed-point) |
185
- | `inventory/` | engagement: offline snapshot tables from `acu inventory` (not seed; not SEED_DIRS) |
186
- | `findings/` | engagement: `acu reconcile` cross-check output (never apply path) |
184
+ | `inventory/` | engagement: offline snapshot tables from `acu survey inventory` (not seed; not SEED_DIRS) |
185
+ | `findings/` | engagement: `acu survey reconcile` cross-check output (never apply path) |
187
186
  | `.env` | secrets + where (`ACU_BASE_URL`) + pin (`ACU_API_VERSION`) |
188
187
 
189
188
  Legacy data repos may still keep root `bootstrap/`…`master/`; bare `apply`/`diff` prefer `config/` when present and never merge both trees.
@@ -192,7 +191,7 @@ Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-
192
191
  Commit seed YAML; keep `.env` out of git (scaffold `.gitignore` already lists it).
193
192
 
194
193
  Seed YAML is state: `apply` upserts it, `diff` proves it.
195
- `acu extract` is the inverse of `apply`: GET live tenant rows into seed YAML under `config/{bootstrap,baseline,setup,master}/` (hard-cut).
194
+ `acu survey extract` is the inverse of `apply`: GET live tenant rows into seed YAML under `config/{bootstrap,baseline,setup,master}/` (hard-cut).
196
195
 
197
196
  Packaged `seed_catalog.yaml` is the sole extract registry (entity, endpoint, keys, file, strip/include, filter-split).
198
197
  The demo entity map in [docs/demo-seed.md](docs/demo-seed.md) mirrors those catalog paths.
@@ -204,7 +203,7 @@ Row failures continue (exit 1 only if any row failed).
204
203
  Drift stays with `diff`.
205
204
 
206
205
  ```sh
207
- acu --tenant DEV extract --out . --force # refresh config/** from live tenant
206
+ acu --tenant DEV survey extract --out . --force # refresh config/** from live tenant
208
207
  git diff config/ # review extract delta before commit
209
208
  acu --tenant DEV apply config/ # replay extracted seed
210
209
  acu --tenant DEV diff config/ # expect exit 0
@@ -267,7 +266,6 @@ Ad-hoc override: `acu --api-version 24.200.001 …` (version half only, never
267
266
  `base_url` resolves as `--url`, else `ACU_BASE_URL`, else a hard error naming sources.
268
267
 
269
268
  `acu config show` prints the resolved `.env` (password excluded; includes `ACU_API_VERSION`).
270
- `acu check` (lifecycle) uses the same resolved instance.
271
269
 
272
270
  ### Multi-host overlays (V44)
273
271
 
@@ -299,9 +297,6 @@ acu diff
299
297
  # explicit path args (no auto) — later path wins same keys
300
298
  acu apply config/ overlays/default-24.200.001/
301
299
  acu diff config/ overlays/default-24.200.001/
302
-
303
- # cold lifecycle vs the resolved .env instance (SSH + tenant required)
304
- acu check --yes --tenant LAB5
305
300
  ```
306
301
 
307
302
  Add a future half by creating `overlays/default-<new-half>/` with the
@@ -317,7 +312,7 @@ Worth knowing:
317
312
  - Without a `.env`, global flags plus the process environment supply the configuration.
318
313
  - When `ACU_SSH` is **absent**, acu defaults to `Administrator@` + the resolved base_url hostname.
319
314
  A **present blank** `ACU_SSH=` is the hosted opt-out.
320
- Only `acu tenant` / `acu check` require a non-empty value post-default.
315
+ Only `acu tenant` requires a non-empty value post-default.
321
316
  - `acu config show` prints the resolved `.env` (password excluded).
322
317
  - Redirect it to turn resolved state into a working config: `acu config show > .env`.
323
318
 
@@ -365,7 +360,9 @@ gmake release patch # or minor | major
365
360
  2. Fail if `## Unreleased` has no bullets
366
361
  3. Bump `pyproject.toml` version (`major` | `minor` | `patch`)
367
362
  4. Promote Unreleased body to `## [vX.Y.Z] - YYYY-MM-DD`, leave an empty `## Unreleased`
368
- 5. Commit `CHANGELOG.md` + `pyproject.toml` (+ lock if bumped) together, tag `vX.Y.Z`, push
363
+ 5. Commit `CHANGELOG.md` + `pyproject.toml` (+ lock if bumped) together
364
+ 6. `gmake check` again on the promoted tree — fail keeps the commit, creates no tag, pushes nothing
365
+ 7. Tag `vX.Y.Z` and push branch + that tag — GitHub Actions publishes
369
366
 
370
367
  GitHub Actions on tag `v*` re-runs CI, builds sdist+wheel, publishes to PyPI via OIDC trusted publishing, and creates a GitHub Release whose notes are the promoted CHANGELOG section for that tag (plus the artifacts).
371
368
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acumatica-cli"
3
- version = "0.29.0"
3
+ version = "0.31.0"
4
4
  description = "Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data"
5
5
  license = "PolyForm-Noncommercial-1.0.0"
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acumatica-cli"
3
- version = "0.29.0"
3
+ version = "0.31.0"
4
4
  description = "Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data"
5
5
  authors = [{ name = "Konstantin Borovik", email = "kb@lab5.ca" }]
6
6
  license = "PolyForm-Noncommercial-1.0.0"
@@ -5,13 +5,13 @@ Configure Acumatica ERP from YAML in a git data repo. No UI wizards.
5
5
  \b
6
6
  MENTAL MODEL
7
7
  Data repo = directory with .env (walk-up from cwd). Holds:
8
- config/{bootstrap,baseline,setup,master}/ seed YAML (apply/diff/extract)
8
+ config/{bootstrap,baseline,setup,master}/ seed YAML (apply/diff/survey extract)
9
9
  config/views/ observer views (state)
10
10
  scenario/ transaction scripts (run)
11
11
  .env secrets + where + API pin
12
12
  state/ inventory/ findings/ schemas/ command outputs (not seed)
13
13
  Two planes (do not mix):
14
- REST data plane — apply diff run bootstrap extract state schema
14
+ REST data plane — apply diff run bootstrap survey extract state schema
15
15
  needs: base_url, tenant, password (SSH optional)
16
16
  SSH control plane — tenant list|create|delete|recycle
17
17
  needs: non-empty ACU_SSH (user@host)
@@ -55,10 +55,10 @@ COMMAND MAP (pick by intent)
55
55
  seed write apply [--dry-run] [FILES...] (sole mutator)
56
56
  seed check diff [FILES...] (exit 2 on drift)
57
57
  txns run [--dry-run] [FILES...]
58
- cold CI check [--tenant LOGIN] (!= config check)
59
- pull seed extract [--only ENTITY]... (inverse of apply)
58
+ cold rebuild tenant create then apply then run then diff (no wrap cmd)
59
+ pull seed survey extract [--only ENTITY]... (inverse of apply)
60
60
  observe state [--diff|--assert-unchanged]
61
- offline inventory ARTIFACT | reconcile
61
+ offline survey inventory ARTIFACT | survey reconcile
62
62
  reference schema
63
63
 
64
64
  \b
@@ -66,7 +66,6 @@ EXIT CODES (common)
66
66
  0 success
67
67
  1 operational / parse / expectation failure
68
68
  2 seed drift (diff) or state moved (state --assert-unchanged)
69
- check (lifecycle) never exits 2 — drift fails the run as exit 1
70
69
 
71
70
  \b
72
71
  DEFAULT PATHS (when FILES omitted)
@@ -74,7 +73,7 @@ DEFAULT PATHS (when FILES omitted)
74
73
  then overlays/default-<api>/ when present
75
74
  run -> scenario/ (overlay same-basename wins)
76
75
  state -> config/views/ -> writes state/
77
- extract -> always writes config/{bootstrap,baseline,setup,master}/
76
+ survey extract -> always writes config/{bootstrap,baseline,setup,master}/
78
77
 
79
78
  Run `acu <command> --help` for flags, examples, and prerequisites.
80
79
  See package README and docs/demo-seed.md for seed YAML shape.
@@ -674,8 +673,8 @@ def config_group() -> None:
674
673
  show print resolved config as a complete .env (password redacted)
675
674
  check read-only live preflight (REST + optional SSH)
676
675
 
677
- Not the same as `acu check` (cold lifecycle rebuild). Start here on a
678
- new machine: init → edit .env → config check → tenant create / bootstrap.
676
+ Start here on a new machine: init → edit .env → config check →
677
+ tenant create / bootstrap.
679
678
  """
680
679
 
681
680
 
@@ -721,7 +720,6 @@ def config_init(host: str | None, directory: Path | None) -> None:
721
720
  output.data(" 5. acu run scenario/")
722
721
  output.data(" 6. acu diff config/")
723
722
  output.data(" 7. acu state")
724
- output.data(" 8. acu check # cold lifecycle (SSH + tenant)")
725
723
 
726
724
 
727
725
  @config_group.command("show")
@@ -751,7 +749,7 @@ def config_check(ctx: click.Context) -> None:
751
749
  """Read-only preflight of the resolved target (ok/fail/warn/skip lines).
752
750
 
753
751
  Proves connectivity before apply/run. Writes nothing (no PUT, no tenant
754
- CRUD). Not `acu check` (that is a destructive cold lifecycle rebuild).
752
+ CRUD).
755
753
 
756
754
  \b
757
755
  Probe order
@@ -875,192 +873,6 @@ def _probe_endpoints(endpoints: list[tuple[str, str]], inst: Instance) -> bool:
875
873
  return False
876
874
 
877
875
 
878
- @cli.command("check")
879
- @click.option(
880
- "--yes",
881
- is_flag=True,
882
- help="Accepted for muscle-memory (pre-clean delete is always unattended)",
883
- )
884
- @click.option(
885
- "--tenant",
886
- "tenant_login",
887
- default=None,
888
- help="Tenant login for pre-clean create (else ACU_TENANT / global --tenant)",
889
- )
890
- @click.pass_context
891
- def check_cmd(ctx: click.Context, yes: bool, tenant_login: str | None) -> None:
892
- """Cold lifecycle: delete → create → apply → run → diff (leave tenant).
893
-
894
- Destructive rebuild for CI against the resolved .env instance. Not
895
- `acu config check` (read-only preflight). Requires non-empty ACU_SSH
896
- and a tenant login (--tenant / ACU_TENANT). Pre-clean delete is
897
- always unattended; rebuilt tenant is left on the host for inspect /
898
- state. --yes is accepted for muscle-memory only.
899
-
900
- \b
901
- Flow
902
- delete tenant (if present) → create+bootstrap → apply → run → diff
903
- Diff drift fails the run as exit 1 (never exit 2 from this command).
904
-
905
- \b
906
- Examples
907
- acu check --tenant DEV
908
- acu check --yes --tenant DEV
909
-
910
- Prefer `config check` for "can I connect?" Prefer this for "rebuild
911
- green from empty."
912
- """
913
- _ = yes
914
- overrides: dict[str, str] = dict(ctx.obj or {})
915
- tenant = _resolve_check_tenant(tenant_login, overrides)
916
- try:
917
- inst = load_instance({**overrides, "tenant": tenant})
918
- except SystemExit as exc:
919
- raise SystemExit(str(exc)) from exc
920
- if not inst.ssh:
921
- raise SystemExit(
922
- "ACU_SSH not set (pass --ssh, or put ACU_SSH in .env or the environment)"
923
- )
924
- output.data(f"check {inst.base_url} (api={inst.api_version})")
925
- if _run_lifecycle(inst, tenant) != 0:
926
- raise SystemExit(1)
927
- output.success("check: green")
928
-
929
-
930
- def _resolve_check_tenant(tenant_login: str | None, overrides: dict[str, str]) -> str:
931
- """Tenant login from flag, global, env, or .env; hard-error if unset."""
932
- tenant = tenant_login or overrides.get("tenant") or os.environ.get("ACU_TENANT")
933
- if not tenant and (root := find_data_root()) is not None:
934
- tenant = read_env_values(root / ".env").get("tenant") or None
935
- if not tenant:
936
- raise SystemExit(
937
- "tenant not set (pass --tenant, "
938
- "or put ACU_TENANT in .env or the environment)"
939
- )
940
- return tenant
941
-
942
-
943
- def _run_lifecycle(inst: Instance, tenant: str) -> int:
944
- """pre-clean→create→apply→run→diff; leave tenant (V47); return 0 or 1."""
945
- mgr = TenantManager(inst)
946
- _lifecycle_delete_best_effort(mgr, tenant, label="pre-clean")
947
- try:
948
- _lifecycle_create_and_bootstrap(inst, mgr, tenant)
949
- data_inst = inst.model_copy(update={"tenant": tenant})
950
- _lifecycle_apply(data_inst)
951
- _lifecycle_run(data_inst)
952
- _lifecycle_diff(data_inst)
953
- except SystemExit as exc:
954
- code = exc.code if isinstance(exc.code, int) else 1
955
- if code == 2:
956
- code = 1 # V47: check never exits 2
957
- output.error(f"lifecycle: {exc}")
958
- # Leave tenant for manual inspect (no post-clean — V47).
959
- return 1 if code != 0 else 0
960
- except (RuntimeError, httpx.HTTPError) as exc:
961
- output.error(f"lifecycle: {_format_failure(exc, target=inst.base_url)}")
962
- # Leave tenant for manual inspect (no post-clean — V47).
963
- return 1
964
- output.success(
965
- f"lifecycle green on {inst.base_url} tenant {tenant} (left for inspection)"
966
- )
967
- return 0
968
-
969
-
970
- def _lifecycle_create_and_bootstrap(
971
- inst: Instance, mgr: TenantManager, tenant: str
972
- ) -> None:
973
- """Create (or exists-skip) tenant + init + bootstrap chain."""
974
- tenants = mgr.list()
975
- existing = next((t for t in tenants if t.login_name == tenant), None)
976
- if existing is not None:
977
- company_id = existing.company_id
978
- output.data(f"skip create: tenant {tenant} exists (id {company_id})")
979
- else:
980
- company_id = max((t.company_id for t in tenants), default=0) + 1
981
- with output.step(f"creating tenant {company_id} ({tenant}) on {inst.base_url}"):
982
- mgr.create(company_id, tenant, 1, True, "")
983
- output.data("created")
984
- with output.step(f"aligning CompanyCD to login ({tenant})"):
985
- if mgr.set_company_cd(company_id, tenant):
986
- output.data(f"CompanyCD set to {tenant}")
987
- else:
988
- output.data(f"CompanyCD already {tenant}")
989
- _init_tenant(inst, mgr, tenant)
990
- _bootstrap_tenant(inst, mgr, tenant)
991
-
992
-
993
- def _lifecycle_delete_best_effort(
994
- mgr: TenantManager, tenant: str, *, label: str
995
- ) -> None:
996
- try:
997
- tenants = mgr.list()
998
- if any(t.login_name == tenant for t in tenants):
999
- with output.step(f"{label} delete tenant {tenant}"):
1000
- mgr.delete(login_name=tenant)
1001
- with output.step(f"recycling app pool after {label}"):
1002
- mgr.recycle_app_pool()
1003
- output.data(f"{label}: deleted {tenant}")
1004
- else:
1005
- output.data(f"{label}: tenant {tenant} absent")
1006
- except (RuntimeError, SystemExit) as exc:
1007
- output.warn(f"{label}: {exc}")
1008
-
1009
-
1010
- def _lifecycle_apply(inst: Instance) -> None:
1011
- """Bare apply (pin overlay auto-compose); SystemExit 1 on failure."""
1012
- total_ok = 0
1013
- all_errors: list[str] = []
1014
- with AcumaticaClient(inst) as client:
1015
- for path in expand_files(default_apply_dirs(inst)):
1016
- baseline = seed.load_baseline(path)
1017
- output.data(
1018
- f"{path} -> {inst.tenant} on {inst.base_url} ({baseline.entity})"
1019
- )
1020
- n, errors = seed.apply(client, baseline, dry_run=False)
1021
- total_ok += n
1022
- all_errors.extend(f"{path}: {e}" for e in errors)
1023
- output.data(f" {n} record(s)")
1024
- if all_errors:
1025
- output.error(f"apply: {len(all_errors)} error(s), {total_ok} record(s) applied")
1026
- for err in all_errors:
1027
- output.error(err)
1028
- raise SystemExit(1)
1029
-
1030
-
1031
- def _lifecycle_run(inst: Instance) -> None:
1032
- """Bare run (pin overlay); SystemExit 1 on failure."""
1033
- paths = list(default_scenario_files(inst))
1034
- scenarios = [run.load_scenario(path) for path in paths]
1035
- ok = True
1036
- with AcumaticaClient(inst) as client:
1037
- for scenario in scenarios:
1038
- ok = run.run(client, scenario) and ok
1039
- if not ok:
1040
- raise SystemExit(1)
1041
- output.success(f"{len(scenarios)} scenario(s) passed on {inst.tenant}")
1042
-
1043
-
1044
- def _lifecycle_diff(inst: Instance) -> None:
1045
- """Bare diff; SystemExit 2 on drift (caller maps to 1 for check)."""
1046
- paths = expand_files(default_apply_dirs(inst))
1047
- drifts: list[str] = []
1048
- with AcumaticaClient(inst) as client:
1049
- for path in paths:
1050
- baseline = seed.load_baseline(path)
1051
- output.data(
1052
- f"{path} -> {inst.tenant} on {inst.base_url} ({baseline.entity})"
1053
- )
1054
- file_drifts = seed.diff(client, baseline)
1055
- drifts += file_drifts
1056
- n = 1 if isinstance(baseline, seed.ActionFile) else len(baseline.records)
1057
- if file_drifts:
1058
- output.data(f" {n} record(s), {len(file_drifts)} drift(s)")
1059
- else:
1060
- output.data(f" {n} record(s) ok")
1061
- _exit_on_drift(inst, drifts, len(paths))
1062
-
1063
-
1064
876
  SEED_DIRS = ("bootstrap", "baseline", "setup", "master")
1065
877
 
1066
878
 
@@ -1215,7 +1027,7 @@ def apply_cmd(inst: Instance, files: tuple[Path, ...], dry_run: bool) -> None:
1215
1027
  acu --tenant DEV apply --dry-run config/master/
1216
1028
  acu --tenant DEV apply config/baseline/20-accounts.yaml
1217
1029
 
1218
- Related: `diff` (drift) · `extract` (inverse pull) · `run` (txns).
1030
+ Related: `diff` (drift) · `survey extract` (inverse pull) · `run` (txns).
1219
1031
  """
1220
1032
  total_ok = 0
1221
1033
  all_errors: list[str] = []
@@ -1294,7 +1106,7 @@ def diff_cmd(inst: Instance, files: tuple[Path, ...]) -> None:
1294
1106
  acu --tenant DEV diff config/
1295
1107
  acu --tenant DEV diff config/master/90-roles.yaml
1296
1108
 
1297
- Related: `apply` (fix) · `extract` (pull) · `state` (balances).
1109
+ Related: `apply` (fix) · `survey extract` (pull) · `state` (balances).
1298
1110
  """
1299
1111
  paths = expand_files(files or default_apply_dirs(inst))
1300
1112
  drifts: list[str] = []
@@ -1377,7 +1189,27 @@ def _complete_only(
1377
1189
  ]
1378
1190
 
1379
1191
 
1380
- @cli.command("extract")
1192
+ @cli.group("survey", context_settings=_HELP_CTX)
1193
+ def survey_group() -> None:
1194
+ """Existing-tenant dual-reader (REST extract + offline inventory/reconcile).
1195
+
1196
+ Noun owns extract|inventory|reconcile. Never L1 those verbs. No aliases.
1197
+
1198
+ \b
1199
+ Subcommands
1200
+ extract [--only ENTITY]... live GET -> config/ SEED_DIRS (REST)
1201
+ inventory ARTIFACT snapshot ZIP/folder -> inventory/ (offline)
1202
+ reconcile inventory/ + optional config/ -> findings/ (offline)
1203
+
1204
+ \b
1205
+ Examples
1206
+ acu --tenant DEV survey extract
1207
+ acu survey inventory ./export-xml/
1208
+ acu survey reconcile
1209
+ """
1210
+
1211
+
1212
+ @survey_group.command("extract")
1381
1213
  @click.option(
1382
1214
  "--out",
1383
1215
  "out_dir",
@@ -1418,11 +1250,11 @@ def extract_cmd(
1418
1250
 
1419
1251
  \b
1420
1252
  Examples
1421
- acu --tenant DEV extract
1422
- acu --tenant DEV extract --only Account --only Vendor
1423
- acu --tenant DEV extract --force --dry-run
1253
+ acu --tenant DEV survey extract
1254
+ acu --tenant DEV survey extract --only Account --only Vendor
1255
+ acu --tenant DEV survey extract --force --dry-run
1424
1256
 
1425
- Related: `apply` · `diff` · `inventory` · `state`.
1257
+ Related: `apply` · `diff` · `survey inventory` · `state`.
1426
1258
  """
1427
1259
  with AcumaticaClient(inst) as client:
1428
1260
  failed = extract.run(
@@ -1446,7 +1278,7 @@ def _exit_on_drift(inst: Instance, drifts: list[str], files: int) -> None:
1446
1278
  output.success(f"no drift on {inst.tenant} ({inst.base_url}, {files} file(s))")
1447
1279
 
1448
1280
 
1449
- @cli.command("inventory")
1281
+ @survey_group.command("inventory")
1450
1282
  @click.argument(
1451
1283
  "artifact",
1452
1284
  type=click.Path(exists=True, path_type=Path),
@@ -1478,15 +1310,15 @@ def inventory_cmd(
1478
1310
  Existing files skip unless --force.
1479
1311
 
1480
1312
  Not extract (REST → config/ seed). Not state (balances). Never writes
1481
- config/ or state/. Feed inventory/ into `acu reconcile`.
1313
+ config/ or state/. Feed inventory/ into `acu survey reconcile`.
1482
1314
 
1483
1315
  \b
1484
1316
  Exit 0 = clean · 1 = parse/format/version fail · never 2
1485
1317
 
1486
1318
  \b
1487
1319
  Examples
1488
- acu inventory ./export-xml/
1489
- acu inventory Settings.zip --out inventory/ --force
1320
+ acu survey inventory ./export-xml/
1321
+ acu survey inventory Settings.zip --out inventory/ --force
1490
1322
  """
1491
1323
  # V9 long single-op: artifact parse (ZIP/folder IR) via step; banner +
1492
1324
  # per-table write/skip emit stay multi-unit stdout after.
@@ -1500,13 +1332,13 @@ def inventory_cmd(
1500
1332
  inventory.emit(art, dest, force=force, dry_run=dry_run)
1501
1333
 
1502
1334
 
1503
- @cli.command("reconcile")
1335
+ @survey_group.command("reconcile")
1504
1336
  @click.option(
1505
1337
  "--inventory",
1506
1338
  "inventory_dir",
1507
1339
  type=click.Path(file_okay=False, path_type=Path),
1508
1340
  default=None,
1509
- help="Inventory tree from `acu inventory` (default: inventory/)",
1341
+ help="Inventory tree from `acu survey inventory` (default: inventory/)",
1510
1342
  )
1511
1343
  @click.option(
1512
1344
  "--config",
@@ -1549,8 +1381,8 @@ def reconcile_cmd(
1549
1381
 
1550
1382
  \b
1551
1383
  Examples
1552
- acu inventory export.zip && acu reconcile
1553
- acu reconcile --inventory inv/ --config config/ --out findings/
1384
+ acu survey inventory export.zip && acu survey reconcile
1385
+ acu survey reconcile --inventory inv/ --config config/ --out findings/
1554
1386
  """
1555
1387
  inv = (
1556
1388
  inventory_dir
@@ -1628,7 +1460,7 @@ def state_cmd(
1628
1460
  acu --tenant DEV state --assert-unchanged
1629
1461
  acu --tenant DEV state config/views/10-trial-balance.yaml
1630
1462
 
1631
- Related: `run` (moves balances) · `diff` (seed drift) · `extract`.
1463
+ Related: `run` (moves balances) · `diff` (seed drift) · `survey extract`.
1632
1464
  """
1633
1465
  if not files:
1634
1466
  default = data_root() / "config" / "views"
@@ -287,7 +287,7 @@ class FindingsBundle(Model):
287
287
 
288
288
 
289
289
  def load_inventory_tree(path: Path | str) -> InventoryTree:
290
- """Load an ``inventory/`` tree written by ``acu inventory`` (I.data)."""
290
+ """Load an ``inventory/`` tree written by ``acu survey inventory`` (I.data)."""
291
291
  root = Path(path)
292
292
  if not root.is_dir():
293
293
  raise SystemExit(f"{root}: inventory directory not found")
@@ -295,7 +295,7 @@ def load_inventory_tree(path: Path | str) -> InventoryTree:
295
295
  if not summary_path.is_file():
296
296
  raise SystemExit(
297
297
  f"{root}: missing {inventory.SUMMARY_NAME} "
298
- "(run `acu inventory` first, or pass --inventory DIR)"
298
+ "(run `acu survey inventory` first, or pass --inventory DIR)"
299
299
  )
300
300
  try:
301
301
  raw = yaml.safe_load(summary_path.read_text(encoding="utf-8"))
@@ -30,15 +30,14 @@ acu state
30
30
  # warm gate: once-capital only — additive buy/sell moves numeric observations
31
31
  acu run scenario/10-seed-capital.yaml && acu state --assert-unchanged
32
32
 
33
- # 7. Cold lifecycle (SSH + tenant)
34
- # acu check --yes
33
+ # SSH cold rebuild: acu tenant create --login NAME, then apply, run, diff
35
34
 
36
35
  # Optional: re-seed from live (inverse of apply; always under config/)
37
- # acu extract --out . --force
36
+ # acu survey extract --out . --force
38
37
  ```
39
38
 
40
39
  Bare `acu apply` / `acu diff` also prefer `config/` when those trees exist.
41
- `acu extract` hard-cuts emit to `config/{bootstrap,baseline,setup,master}/` (never root SEED_DIRS).
40
+ `acu survey extract` hard-cuts emit to `config/{bootstrap,baseline,setup,master}/` (never root SEED_DIRS).
42
41
 
43
42
  ## Layout
44
43