acumatica-cli 0.3.0__tar.gz → 0.4.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 (33) hide show
  1. acumatica_cli-0.4.0/PKG-INFO +188 -0
  2. acumatica_cli-0.4.0/README.md +171 -0
  3. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/pyproject.toml +1 -1
  4. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/cli.py +120 -5
  5. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/client.py +14 -0
  6. acumatica_cli-0.4.0/src/acumatica_cli/extract.py +503 -0
  7. acumatica_cli-0.4.0/src/acumatica_cli/extract_manifest.yaml +151 -0
  8. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/output.py +3 -1
  9. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/seed.py +38 -15
  10. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/baseline/60-ledger-company.yaml +8 -6
  11. acumatica_cli-0.3.0/PKG-INFO +0 -239
  12. acumatica_cli-0.3.0/README.md +0 -222
  13. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/__init__.py +0 -0
  14. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/bootstrap.py +0 -0
  15. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
  16. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/bootstrap_project.xml +0 -0
  17. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/config.py +0 -0
  18. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/firstlogin.py +0 -0
  19. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/models.py +0 -0
  20. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/baseline/10-subaccounts.yaml +0 -0
  21. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/baseline/20-accounts.yaml +0 -0
  22. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/baseline/40-ledger.yaml +0 -0
  23. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/baseline/50-gl-preferences.yaml +0 -0
  24. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/baseline/90-uoms.yaml +0 -0
  25. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/bootstrap/company.yaml +0 -0
  26. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/bootstrap/credit-terms.yaml +0 -0
  27. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/bootstrap/features.yaml +0 -0
  28. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/env +0 -0
  29. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/gitignore +0 -0
  30. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/setup/10-financial-year.yaml +0 -0
  31. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/setup/20-master-calendar.yaml +0 -0
  32. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/templates/setup/30-open-periods.yaml +0 -0
  33. {acumatica_cli-0.3.0 → acumatica_cli-0.4.0}/src/acumatica_cli/tenant.py +0 -0
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.3
2
+ Name: acumatica-cli
3
+ Version: 0.4.0
4
+ Summary: Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data
5
+ Author: Konstantin Borovik
6
+ Author-email: Konstantin Borovik <kb@lab5.ca>
7
+ Requires-Dist: click>=8.1
8
+ Requires-Dist: httpx>=0.27
9
+ Requires-Dist: pydantic>=2.7
10
+ Requires-Dist: pydantic-settings>=2.5
11
+ Requires-Dist: pyyaml>=6.0
12
+ Requires-Dist: rich>=13
13
+ Requires-Python: >=3.12
14
+ Project-URL: Homepage, https://lab5.ca
15
+ Project-URL: Repository, https://github.com/kborovik/acumatica-cli
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Acumatica Config-as-Code
19
+
20
+ **`acu`** configures Acumatica ERP from YAML files in a git repo.
21
+
22
+ **No UI clicks, no Configuration Wizard.**
23
+
24
+ ## How it works
25
+
26
+ 1. **Read** the target Acumatica deployment and translate its configuration into YAML — config as code.
27
+ 2. **Develop** changes in the YAML, in git: edit, review, version.
28
+ 3. **Re-deploy** with `acu apply`, then prove the live tenant matches the code with `acu diff`.
29
+
30
+ Three commands do the work, and every one is safe to re-run:
31
+
32
+ - **`acu tenant create`** — creates a tenant and bootstraps it in one step, ready for `apply`. Re-running it against an existing tenant republishes the bootstrap package instead of failing.
33
+ - **`acu apply`** — pushes your YAML into the tenant as keyed upserts. Running it twice changes nothing.
34
+ - **`acu diff`** — compares your YAML against the live tenant and exits with code 2 on drift.
35
+
36
+ > **Tested against** Acumatica ERP **26.101.0225** on Windows Server 2025,
37
+ > contract REST endpoint **25.200.001**. Other versions will likely work,
38
+ > but only this combination is verified.
39
+
40
+ ## Why
41
+
42
+ Acumatica configuration normally lives in the web UI: wizards, screens, and manual data entry that nobody can review, version, or reproduce.
43
+
44
+ `acu` moves that configuration into YAML files in a git repo, so a tenant can be rebuilt from scratch, audited in a pull request, and checked for drift like any other infrastructure.
45
+
46
+ ## Quick start
47
+
48
+ ```sh
49
+ uv tool install acumatica-cli
50
+
51
+ acu config init --host erp.example.com my-erp
52
+ cd my-erp # edit .env: set ACU_PASSWORD
53
+
54
+ acu config check # read-only preflight
55
+ acu tenant create --id 3 --login DEV # create the tenant + bootstrap it
56
+ acu --tenant DEV apply # seed bootstrap/, baseline/, setup/
57
+ acu --tenant DEV diff # prove zero drift (exit 2 on drift)
58
+ ```
59
+
60
+ ## CLI map
61
+
62
+ ```text
63
+ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
64
+ [--username U] [--password P]
65
+
66
+ ├── tenant tenant CRUD (ac.exe over SSH — control plane)
67
+ │ ├── list CompanyID, sign-in name, internal CD, type
68
+ │ ├── create --id N --login NAME create + bootstrap; re-run to republish
69
+ │ │ [--type SalesDemo] [--parent N] [--hidden] [--no-init]
70
+ │ └── delete --id N [--yes] delete the tenant and its data, recycle app pool
71
+
72
+ ├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
73
+ ├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
74
+ ├── schema [--out DIR] dump the endpoint's OpenAPI schema (swagger.json)
75
+
76
+ └── config configuration ops
77
+ ├── init [--host HOST] [DIR] scaffold a data repo (.env plus example YAML)
78
+ ├── show print the resolved config as a complete .env
79
+ └── check read-only preflight: discovery, secrets, REST, SSH
80
+ ```
81
+
82
+ `apply` and `diff` called without FILES default to the scaffolded directories, in order: `bootstrap/`, then `baseline/`, then `setup/`. Run `acu <command> --help` for details on any command.
83
+
84
+ ## The data repo
85
+
86
+ Your configuration lives in its own git repo, which `acu config init` scaffolds:
87
+
88
+ | Path | What it holds |
89
+ | ------------ | -------------------------------------------------------------------------- |
90
+ | `bootstrap/` | what makes a virgin tenant configurable: features, company, credit terms |
91
+ | `baseline/` | reference data: subaccounts, chart of accounts, ledger, units of measure |
92
+ | `setup/` | one-time actions: financial year, master calendar, open periods |
93
+ | `.env` | where to apply and who signs in, every key an `ACU_*` variable |
94
+
95
+ Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-`, and so on) encode dependency order. The scaffolded `.gitignore` keeps `.env` out of git — store it encrypted (for example as `.env.gpg`) and decrypt once per clone.
96
+
97
+ ## Installation
98
+
99
+ Requires Python 3.12 or newer.
100
+
101
+ ```sh
102
+ uv tool install acumatica-cli
103
+ ```
104
+
105
+ `pipx install acumatica-cli` and `pip install acumatica-cli` work too. For the latest development version straight from the main branch:
106
+
107
+ ```sh
108
+ uv tool install git+https://github.com/kborovik/acumatica-cli.git
109
+ ```
110
+
111
+ Verify with `acu --version`.
112
+
113
+ ## Configuration
114
+
115
+ Everything lives in one `.env` file: *where* to apply and *who* signs in. Three values are required; everything else has a code default matching a stock Acumatica install:
116
+
117
+ ```sh
118
+ ACU_BASE_URL=http://acu-dev1.vm.internal/AcumaticaERP # required: REST root
119
+ ACU_TENANT=LAB5 # sign-in name of the tenant API sessions use
120
+ ACU_SSH=Administrator@acu-dev1.vm.internal # required: control-plane user@host
121
+ ACU_USER=admin # optional, defaults to admin
122
+ ACU_PASSWORD=... # required
123
+ ```
124
+
125
+ Worth knowing:
126
+
127
+ - The file is found by walking up from the current directory, so any subdirectory of the data repo works. Without a `.env`, global flags plus the process environment supply the full configuration.
128
+ - Nothing is derived: split-horizon DNS, port forwards, and jump hosts are all handled by writing the address you actually want into the two address keys.
129
+ - `acu config show` prints the fully resolved configuration as a complete, valid `.env` — every knob visible, the password excluded. Redirect it to turn resolved state into a working config: `acu config show > .env`.
130
+
131
+ Verify before touching anything live:
132
+
133
+ ```sh
134
+ acu config check # read-only preflight: discovery, secrets, REST, SSH
135
+ acu apply --dry-run # show what would be written, write nothing
136
+ ```
137
+
138
+ ## Control and Data Planes
139
+
140
+ `acu` talks to an instance over two independent channels:
141
+
142
+ - **Control plane (SSH):** `acu tenant` runs `ac.exe -cm:CompanyConfig` and `sqlcmd` on the Windows guest — see [`docs/ac-exe.md`](docs/ac-exe.md).
143
+ - **Data plane (REST):** `acu apply`, `diff`, and `schema` use the contract-based API (`/entity/Default/25.200.001/`), where `PUT` is a keyed upsert — see [`docs/rest-api.md`](docs/rest-api.md).
144
+
145
+ If you only apply and diff YAML, you never need SSH. SSH setup is required only for `acu tenant`.
146
+
147
+ ## SSH setup (control plane)
148
+
149
+ `acu tenant` runs commands on the Windows guest through plain `ssh`. Two things about this setup are not obvious, and both are hard requirements.
150
+
151
+ **1. The default SSH shell on the Windows guest must be PowerShell.**
152
+ `acu` sends PowerShell syntax over the wire, and every one of those commands fails under `cmd.exe`, the Windows OpenSSH default. Switch it once, in an elevated PowerShell on the guest:
153
+
154
+ ```powershell
155
+ New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
156
+ -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" `
157
+ -PropertyType String -Force
158
+ ```
159
+
160
+ **2. Authentication must be key-based and non-interactive.**
161
+ `acu` connects with `BatchMode=yes`, so it will never answer a password prompt.
162
+ Because the default user is `Administrator` (an administrators-group member), Windows OpenSSH reads the key from the *machine-wide* file `C:\ProgramData\ssh\administrators_authorized_keys` — **not** from `~\.ssh\authorized_keys` like on Linux. On the guest:
163
+
164
+ Install + start the server (once):
165
+
166
+ ```powershell
167
+ Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
168
+ Set-Service sshd -StartupType Automatic
169
+ Start-Service sshd
170
+ ```
171
+
172
+ Authorize your public key for administrators:
173
+
174
+ ```powershell
175
+ Add-Content -Path C:\ProgramData\ssh\administrators_authorized_keys -Value "ssh-ed25519 AAAA... you@laptop"
176
+ ```
177
+
178
+ The file must be readable by SYSTEM/Administrators only, or sshd ignores it:
179
+
180
+ ```powershell
181
+ icacls C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"
182
+ ```
183
+
184
+ Then verify from your workstation — this one test proves both requirements at once (key auth works, and the shell is PowerShell):
185
+
186
+ ```sh
187
+ ssh -o BatchMode=yes Administrator@acu-dev1.vm.internal '$PSVersionTable.PSVersion'
188
+ ```
@@ -0,0 +1,171 @@
1
+ # Acumatica Config-as-Code
2
+
3
+ **`acu`** configures Acumatica ERP from YAML files in a git repo.
4
+
5
+ **No UI clicks, no Configuration Wizard.**
6
+
7
+ ## How it works
8
+
9
+ 1. **Read** the target Acumatica deployment and translate its configuration into YAML — config as code.
10
+ 2. **Develop** changes in the YAML, in git: edit, review, version.
11
+ 3. **Re-deploy** with `acu apply`, then prove the live tenant matches the code with `acu diff`.
12
+
13
+ Three commands do the work, and every one is safe to re-run:
14
+
15
+ - **`acu tenant create`** — creates a tenant and bootstraps it in one step, ready for `apply`. Re-running it against an existing tenant republishes the bootstrap package instead of failing.
16
+ - **`acu apply`** — pushes your YAML into the tenant as keyed upserts. Running it twice changes nothing.
17
+ - **`acu diff`** — compares your YAML against the live tenant and exits with code 2 on drift.
18
+
19
+ > **Tested against** Acumatica ERP **26.101.0225** on Windows Server 2025,
20
+ > contract REST endpoint **25.200.001**. Other versions will likely work,
21
+ > but only this combination is verified.
22
+
23
+ ## Why
24
+
25
+ Acumatica configuration normally lives in the web UI: wizards, screens, and manual data entry that nobody can review, version, or reproduce.
26
+
27
+ `acu` moves that configuration into YAML files in a git repo, so a tenant can be rebuilt from scratch, audited in a pull request, and checked for drift like any other infrastructure.
28
+
29
+ ## Quick start
30
+
31
+ ```sh
32
+ uv tool install acumatica-cli
33
+
34
+ acu config init --host erp.example.com my-erp
35
+ cd my-erp # edit .env: set ACU_PASSWORD
36
+
37
+ acu config check # read-only preflight
38
+ acu tenant create --id 3 --login DEV # create the tenant + bootstrap it
39
+ acu --tenant DEV apply # seed bootstrap/, baseline/, setup/
40
+ acu --tenant DEV diff # prove zero drift (exit 2 on drift)
41
+ ```
42
+
43
+ ## CLI map
44
+
45
+ ```text
46
+ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
47
+ [--username U] [--password P]
48
+
49
+ ├── tenant tenant CRUD (ac.exe over SSH — control plane)
50
+ │ ├── list CompanyID, sign-in name, internal CD, type
51
+ │ ├── create --id N --login NAME create + bootstrap; re-run to republish
52
+ │ │ [--type SalesDemo] [--parent N] [--hidden] [--no-init]
53
+ │ └── delete --id N [--yes] delete the tenant and its data, recycle app pool
54
+
55
+ ├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
56
+ ├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
57
+ ├── schema [--out DIR] dump the endpoint's OpenAPI schema (swagger.json)
58
+
59
+ └── config configuration ops
60
+ ├── init [--host HOST] [DIR] scaffold a data repo (.env plus example YAML)
61
+ ├── show print the resolved config as a complete .env
62
+ └── check read-only preflight: discovery, secrets, REST, SSH
63
+ ```
64
+
65
+ `apply` and `diff` called without FILES default to the scaffolded directories, in order: `bootstrap/`, then `baseline/`, then `setup/`. Run `acu <command> --help` for details on any command.
66
+
67
+ ## The data repo
68
+
69
+ Your configuration lives in its own git repo, which `acu config init` scaffolds:
70
+
71
+ | Path | What it holds |
72
+ | ------------ | -------------------------------------------------------------------------- |
73
+ | `bootstrap/` | what makes a virgin tenant configurable: features, company, credit terms |
74
+ | `baseline/` | reference data: subaccounts, chart of accounts, ledger, units of measure |
75
+ | `setup/` | one-time actions: financial year, master calendar, open periods |
76
+ | `.env` | where to apply and who signs in, every key an `ACU_*` variable |
77
+
78
+ Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-`, and so on) encode dependency order. The scaffolded `.gitignore` keeps `.env` out of git — store it encrypted (for example as `.env.gpg`) and decrypt once per clone.
79
+
80
+ ## Installation
81
+
82
+ Requires Python 3.12 or newer.
83
+
84
+ ```sh
85
+ uv tool install acumatica-cli
86
+ ```
87
+
88
+ `pipx install acumatica-cli` and `pip install acumatica-cli` work too. For the latest development version straight from the main branch:
89
+
90
+ ```sh
91
+ uv tool install git+https://github.com/kborovik/acumatica-cli.git
92
+ ```
93
+
94
+ Verify with `acu --version`.
95
+
96
+ ## Configuration
97
+
98
+ Everything lives in one `.env` file: *where* to apply and *who* signs in. Three values are required; everything else has a code default matching a stock Acumatica install:
99
+
100
+ ```sh
101
+ ACU_BASE_URL=http://acu-dev1.vm.internal/AcumaticaERP # required: REST root
102
+ ACU_TENANT=LAB5 # sign-in name of the tenant API sessions use
103
+ ACU_SSH=Administrator@acu-dev1.vm.internal # required: control-plane user@host
104
+ ACU_USER=admin # optional, defaults to admin
105
+ ACU_PASSWORD=... # required
106
+ ```
107
+
108
+ Worth knowing:
109
+
110
+ - The file is found by walking up from the current directory, so any subdirectory of the data repo works. Without a `.env`, global flags plus the process environment supply the full configuration.
111
+ - Nothing is derived: split-horizon DNS, port forwards, and jump hosts are all handled by writing the address you actually want into the two address keys.
112
+ - `acu config show` prints the fully resolved configuration as a complete, valid `.env` — every knob visible, the password excluded. Redirect it to turn resolved state into a working config: `acu config show > .env`.
113
+
114
+ Verify before touching anything live:
115
+
116
+ ```sh
117
+ acu config check # read-only preflight: discovery, secrets, REST, SSH
118
+ acu apply --dry-run # show what would be written, write nothing
119
+ ```
120
+
121
+ ## Control and Data Planes
122
+
123
+ `acu` talks to an instance over two independent channels:
124
+
125
+ - **Control plane (SSH):** `acu tenant` runs `ac.exe -cm:CompanyConfig` and `sqlcmd` on the Windows guest — see [`docs/ac-exe.md`](docs/ac-exe.md).
126
+ - **Data plane (REST):** `acu apply`, `diff`, and `schema` use the contract-based API (`/entity/Default/25.200.001/`), where `PUT` is a keyed upsert — see [`docs/rest-api.md`](docs/rest-api.md).
127
+
128
+ If you only apply and diff YAML, you never need SSH. SSH setup is required only for `acu tenant`.
129
+
130
+ ## SSH setup (control plane)
131
+
132
+ `acu tenant` runs commands on the Windows guest through plain `ssh`. Two things about this setup are not obvious, and both are hard requirements.
133
+
134
+ **1. The default SSH shell on the Windows guest must be PowerShell.**
135
+ `acu` sends PowerShell syntax over the wire, and every one of those commands fails under `cmd.exe`, the Windows OpenSSH default. Switch it once, in an elevated PowerShell on the guest:
136
+
137
+ ```powershell
138
+ New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
139
+ -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" `
140
+ -PropertyType String -Force
141
+ ```
142
+
143
+ **2. Authentication must be key-based and non-interactive.**
144
+ `acu` connects with `BatchMode=yes`, so it will never answer a password prompt.
145
+ Because the default user is `Administrator` (an administrators-group member), Windows OpenSSH reads the key from the *machine-wide* file `C:\ProgramData\ssh\administrators_authorized_keys` — **not** from `~\.ssh\authorized_keys` like on Linux. On the guest:
146
+
147
+ Install + start the server (once):
148
+
149
+ ```powershell
150
+ Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
151
+ Set-Service sshd -StartupType Automatic
152
+ Start-Service sshd
153
+ ```
154
+
155
+ Authorize your public key for administrators:
156
+
157
+ ```powershell
158
+ Add-Content -Path C:\ProgramData\ssh\administrators_authorized_keys -Value "ssh-ed25519 AAAA... you@laptop"
159
+ ```
160
+
161
+ The file must be readable by SYSTEM/Administrators only, or sshd ignores it:
162
+
163
+ ```powershell
164
+ icacls C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"
165
+ ```
166
+
167
+ Then verify from your workstation — this one test proves both requirements at once (key auth works, and the shell is PowerShell):
168
+
169
+ ```sh
170
+ ssh -o BatchMode=yes Administrator@acu-dev1.vm.internal '$PSVersionTable.PSVersion'
171
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acumatica-cli"
3
- version = "0.3.0"
3
+ version = "0.4.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
  readme = "README.md"
@@ -10,8 +10,9 @@ from typing import Concatenate
10
10
 
11
11
  import click
12
12
  import httpx
13
+ from click.shell_completion import get_completion_class
13
14
 
14
- from . import bootstrap, firstlogin, output, seed
15
+ from . import bootstrap, extract, firstlogin, output, seed
15
16
  from .client import AcumaticaClient
16
17
  from .config import (
17
18
  Instance,
@@ -43,8 +44,53 @@ def _version() -> str:
43
44
  return dist.version
44
45
 
45
46
 
47
+ # flag_value sentinel: a bare --completion means "detect from $SHELL"
48
+ _DETECT_SHELL = "auto"
49
+
50
+
51
+ def _completion_script(shell: str) -> str:
52
+ """The click completion script for one supported shell (I.cmd --completion).
53
+
54
+ The _DETECT_SHELL sentinel resolves the shell from $SHELL's basename,
55
+ so a bare --completion works in the shell it runs in; an unsupported
56
+ or undetectable shell errors naming the supported set (V9: exit 1).
57
+ Emission is local-only (V23): click renders the script text, nothing
58
+ live is touched - enabling is the user's job (source the output).
59
+ """
60
+ if shell == _DETECT_SHELL:
61
+ shell = Path(os.environ.get("SHELL", "")).name
62
+ completion_cls = get_completion_class(shell)
63
+ if completion_cls is None:
64
+ raise SystemExit(
65
+ f"cannot emit completion for shell {shell!r} (supported: bash, zsh, fish)"
66
+ )
67
+ return completion_cls(cli, {}, "acu", "_ACU_COMPLETE").source()
68
+
69
+
70
+ def _emit_completion(
71
+ ctx: click.Context, _param: click.Parameter, shell: str | None
72
+ ) -> None:
73
+ """Print the completion script and exit - eager, like --version (V16)."""
74
+ if shell is None or ctx.resilient_parsing:
75
+ return
76
+ output.data(_completion_script(shell))
77
+ ctx.exit()
78
+
79
+
46
80
  @click.group(help=__doc__)
47
81
  @click.version_option(version=_version(), prog_name="acu")
82
+ @click.option(
83
+ "--completion",
84
+ is_flag=False,
85
+ flag_value=_DETECT_SHELL,
86
+ default=None,
87
+ expose_value=False,
88
+ is_eager=True,
89
+ callback=_emit_completion,
90
+ metavar="[bash|zsh|fish]",
91
+ help="Print the shell completion script and exit; a bare --completion "
92
+ "detects the shell from $SHELL. Enable by sourcing the output.",
93
+ )
48
94
  @click.option(
49
95
  "--tenant",
50
96
  default=None,
@@ -160,8 +206,12 @@ def tenant_list(inst: Instance) -> None:
160
206
  @click.option(
161
207
  "--type",
162
208
  "company_type",
163
- default="",
164
- help="Inserted data set: '' = clean, SalesDemo = demo",
209
+ # the V12-verified dataset folders on the box (docs/ac-exe.md); System
210
+ # is the system-tenant dataset, deliberately not offered
211
+ type=click.Choice(["SalesDemo", "T100", "U100"]),
212
+ default=None,
213
+ help="Data set inserted at creation (omit for a clean tenant); "
214
+ "T100/U100 are the Acumatica University training sets",
165
215
  )
166
216
  @click.option("--parent", "parent_id", type=int, default=1, show_default=True)
167
217
  @click.option("--hidden", is_flag=True, help="Do not show on the sign-in page")
@@ -175,7 +225,7 @@ def tenant_create(
175
225
  inst: Instance,
176
226
  company_id: int,
177
227
  login_name: str,
178
- company_type: str,
228
+ company_type: str | None,
179
229
  parent_id: int,
180
230
  hidden: bool,
181
231
  no_init: bool,
@@ -211,8 +261,9 @@ def tenant_create(
211
261
  with output.step(
212
262
  f"creating tenant {company_id} ({login_name}) on {inst.base_url}"
213
263
  ):
264
+ # None = --type omitted = clean tenant (ac.exe's empty CompanyType)
214
265
  raw = mgr.create(
215
- company_id, login_name, parent_id, not hidden, company_type
266
+ company_id, login_name, parent_id, not hidden, company_type or ""
216
267
  )
217
268
  output.data(raw.splitlines()[-1] if raw.strip() else "created")
218
269
  if no_init:
@@ -506,6 +557,70 @@ def diff_cmd(inst: Instance, files: tuple[Path, ...]) -> None:
506
557
  _exit_on_drift(inst, drifts, len(paths))
507
558
 
508
559
 
560
+ def _complete_only(
561
+ _ctx: click.Context, _param: click.Parameter, incomplete: str
562
+ ) -> list[str]:
563
+ """--only value completion: entity names off the packaged manifest.
564
+
565
+ Fires per keystroke, so it stays local-only (V23): the manifest is
566
+ package data - never REST, never SSH, never a live instance.
567
+ """
568
+ return [
569
+ spec.entity
570
+ for spec in extract.load_manifest().entities
571
+ if spec.entity.startswith(incomplete)
572
+ ]
573
+
574
+
575
+ @cli.command("extract")
576
+ @click.option(
577
+ "--out",
578
+ "out_dir",
579
+ type=click.Path(file_okay=False, path_type=Path),
580
+ default=None,
581
+ help="Output directory (default: current directory)",
582
+ )
583
+ @click.option(
584
+ "--only",
585
+ multiple=True,
586
+ shell_complete=_complete_only,
587
+ help="Limit to matching manifest rows (entity name or file stem); repeatable",
588
+ )
589
+ @click.option("--force", is_flag=True, help="Overwrite existing files")
590
+ @click.option(
591
+ "--dry-run", is_flag=True, help="Show what would be written without writing"
592
+ )
593
+ @pass_instance
594
+ def extract_cmd(
595
+ inst: Instance,
596
+ out_dir: Path | None,
597
+ only: tuple[str, ...],
598
+ force: bool,
599
+ dry_run: bool,
600
+ ) -> None:
601
+ """Extract live tenant state into seed YAML files (the inverse of apply).
602
+
603
+ Manifest-driven (the packaged extract manifest carries the verified
604
+ entity set): each entity is read from the live tenant and written as a
605
+ seed file under bootstrap/ or baseline/ that apply and diff consume
606
+ unchanged. Existing files are skipped unless --force; an entity with
607
+ no live records produces no file. A failing row is reported and the
608
+ run continues to the next (a virgin tenant extracts whole). Exit 0
609
+ when every row wrote or skipped clean, 1 when any row failed - drift
610
+ detection stays with diff.
611
+ """
612
+ with AcumaticaClient(inst) as client:
613
+ failed = extract.run(
614
+ client,
615
+ out_dir or Path("."),
616
+ only=frozenset(only),
617
+ force=force,
618
+ dry_run=dry_run,
619
+ )
620
+ if failed:
621
+ raise SystemExit(1)
622
+
623
+
509
624
  def _exit_on_drift(inst: Instance, drifts: list[str], files: int) -> None:
510
625
  """Report drift lines and exit 2 (the load-bearing diff contract, V9)."""
511
626
  if drifts:
@@ -25,6 +25,20 @@ def unwrap(entity: dict[str, Any]) -> dict[str, Any]:
25
25
  }
26
26
 
27
27
 
28
+ # The list GET's optimized-export failure (B9): the contract API's list GET
29
+ # 500s with this marker when any field in scope maps to a BQL-delegate view.
30
+ # The one error read paths retry around (seed.diff via the key-URL GET,
31
+ # extract via a $select-narrowed list GET); any other error still raises.
32
+ OPTIMIZATION_500 = "Optimization cannot be performed"
33
+
34
+ # The empty-state 500 extract classifies as a clean skip (V24): a virgin
35
+ # tenant answers PXSetupNotEnteredException on most entities until bootstrap
36
+ # lands, and the exceptionMessage names the missing form — "The required
37
+ # configuration data is not entered on the <Form> form." (verified vs
38
+ # 26.101.0225, docs/rest-api.md).
39
+ SETUP_NOT_ENTERED_500 = "configuration data is not entered"
40
+
41
+
28
42
  class AcumaticaClient:
29
43
  """Cookie-session client for the contract-based endpoint.
30
44