operx-cli 0.1.0__tar.gz → 0.2.1__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.
- {operx_cli-0.1.0 → operx_cli-0.2.1}/PKG-INFO +88 -15
- {operx_cli-0.1.0 → operx_cli-0.2.1}/README.md +86 -14
- operx_cli-0.2.1/operx_cli/AGENTES.md +134 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/__init__.py +1 -1
- operx_cli-0.2.1/operx_cli/agent_guide.py +57 -0
- operx_cli-0.2.1/operx_cli/app.py +149 -0
- operx_cli-0.2.1/operx_cli/browser_login.py +91 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/client.py +13 -11
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/commands/account.py +50 -14
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/commands/common.py +16 -2
- operx_cli-0.2.1/operx_cli/shell.py +254 -0
- operx_cli-0.2.1/operx_cli/welcome.py +110 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli.egg-info/PKG-INFO +88 -15
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli.egg-info/SOURCES.txt +10 -1
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli.egg-info/requires.txt +1 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/pyproject.toml +7 -1
- operx_cli-0.2.1/tests/test_agentes.py +44 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/tests/test_app.py +44 -12
- {operx_cli-0.1.0 → operx_cli-0.2.1}/tests/test_client.py +31 -1
- operx_cli-0.2.1/tests/test_login.py +196 -0
- operx_cli-0.2.1/tests/test_shell.py +248 -0
- operx_cli-0.2.1/tests/test_welcome.py +131 -0
- operx_cli-0.1.0/operx_cli/app.py +0 -87
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/commands/__init__.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/commands/inventario.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/commands/lecturas.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/commands/ordenes.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/commands/personas.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/commands/raw.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/commands/registros.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/commands/search.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/lookup.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/output.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/settings.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/spreadsheet.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli/values.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli.egg-info/dependency_links.txt +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli.egg-info/entry_points.txt +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/operx_cli.egg-info/top_level.txt +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/setup.cfg +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/tests/test_inventario_lecturas.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/tests/test_lookup.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/tests/test_ordenes.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/tests/test_output.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/tests/test_personas.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/tests/test_registros.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/tests/test_settings.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/tests/test_spreadsheet.py +0 -0
- {operx_cli-0.1.0 → operx_cli-0.2.1}/tests/test_values.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: operx-cli
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: La línea de comandos de OperX: tu empresa desde la terminal, con tu token de API.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -8,6 +8,7 @@ Requires-Dist: typer>=0.27
|
|
|
8
8
|
Requires-Dist: httpx>=0.28
|
|
9
9
|
Requires-Dist: keyring>=25
|
|
10
10
|
Requires-Dist: openpyxl>=3.1
|
|
11
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
11
12
|
Provides-Extra: test
|
|
12
13
|
Requires-Dist: pytest>=8; extra == "test"
|
|
13
14
|
|
|
@@ -19,38 +20,75 @@ tú, con tu rol, y solo en tu empresa.
|
|
|
19
20
|
|
|
20
21
|
## Instalar
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
Windows (PowerShell):
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"; uv tool install operx-cli
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Mac y Linux:
|
|
23
30
|
|
|
24
31
|
```sh
|
|
25
|
-
|
|
26
|
-
operx --version
|
|
32
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh && uv tool install operx-cli
|
|
27
33
|
```
|
|
28
34
|
|
|
35
|
+
`uv` instala su propio Python, así que no necesitas tener uno. Si ya tienes
|
|
36
|
+
Python 3.10 o más nuevo, también sirve `pipx install operx-cli`. Abre una
|
|
37
|
+
terminal nueva y comprueba con `operx --version`.
|
|
38
|
+
|
|
39
|
+
Para actualizar: `uv tool upgrade operx-cli`.
|
|
40
|
+
|
|
29
41
|
## Entrar
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
```sh
|
|
44
|
+
operx login
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
1. La terminal muestra un código y abre el navegador en la página de OperX.
|
|
48
|
+
2. Entras como siempre. La página dice qué terminal pide acceso: apruébalo solo
|
|
49
|
+
si acabas de escribir `operx login` en tu propio computador y el código es
|
|
50
|
+
el mismo. Escribe el código de 6 dígitos de tu app de autenticación (o tu
|
|
51
|
+
contraseña, si no usas una) y elige *Solo lectura* o *Lectura y
|
|
52
|
+
escritura*.
|
|
53
|
+
3. Vuelve a la terminal: ya entraste. Nunca ves ni copias un token.
|
|
34
54
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
```
|
|
55
|
+
Tu rol necesita el permiso «Acceso por API» (Ajustes › Usuarios y acceso ›
|
|
56
|
+
Roles); un administrador lo da. El acceso dura 90 días y lo ves, y puedes
|
|
57
|
+
revocarlo, en el menú de tu cuenta › Acceso por API.
|
|
39
58
|
|
|
40
59
|
El token queda en el llavero del sistema (en Windows, el Administrador de
|
|
41
|
-
credenciales), nunca en un archivo. En un servidor sin
|
|
42
|
-
|
|
60
|
+
credenciales), nunca en un archivo. En un servidor sin navegador ni llavero:
|
|
61
|
+
`operx login --token` te deja pegar un token creado en Acceso por API, o puedes
|
|
62
|
+
usar variables de entorno: `OPERX_TOKEN` (el token) y, si no es producción,
|
|
63
|
+
`OPERX_DIRECCION`.
|
|
43
64
|
|
|
44
65
|
Para otra dirección u otra empresa, usa un perfil:
|
|
45
66
|
|
|
46
67
|
```sh
|
|
47
|
-
operx --perfil pruebas
|
|
68
|
+
operx --perfil pruebas login --direccion https://…
|
|
48
69
|
operx --perfil pruebas ordenes listar
|
|
49
70
|
```
|
|
50
71
|
|
|
51
|
-
`operx
|
|
72
|
+
`operx logout` borra el token de este equipo; para que deje de funcionar,
|
|
52
73
|
revócalo en Acceso por API.
|
|
53
74
|
|
|
75
|
+
## Modo interactivo
|
|
76
|
+
|
|
77
|
+
Escribe `operx` solo, en una terminal, y se abre la pantalla de bienvenida
|
|
78
|
+
(quién eres y los números de hoy: órdenes abiertas, vencidas, artículos bajo
|
|
79
|
+
el mínimo) con un prompt. Ahí escribes los comandos sin el `operx`, con
|
|
80
|
+
Tab para completar y ↑ para repetir lo anterior:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
operx › ordenes listar --vencidas
|
|
84
|
+
operx › buscar "bomba"
|
|
85
|
+
operx › ayuda
|
|
86
|
+
operx › salir
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`salir` (o Ctrl+D) termina la sesión sin borrar tu token. En un script o una
|
|
90
|
+
tubería, `operx` solo muestra la ayuda y no espera a nadie.
|
|
91
|
+
|
|
54
92
|
## Comandos
|
|
55
93
|
|
|
56
94
|
```sh
|
|
@@ -124,6 +162,13 @@ operx api POST work-orders/1234/attachments/ -a file=foto.jpg -d '{"file_type":
|
|
|
124
162
|
En Git Bash, una dirección que empieza por `/` se vuelve una ruta de Windows:
|
|
125
163
|
escríbela sin la barra inicial, como arriba.
|
|
126
164
|
|
|
165
|
+
## Para agentes de IA
|
|
166
|
+
|
|
167
|
+
`operx agentes` imprime una guía para un agente de IA (Claude, ChatGPT,
|
|
168
|
+
Copilot, Codex…): las reglas para usar `operx` bien (leer con `--json`,
|
|
169
|
+
preguntar antes de escribir, no tocar tokens) y cada comando con sus opciones.
|
|
170
|
+
Dile a tu agente: «Ejecuta `operx agentes` y sigue esa guía».
|
|
171
|
+
|
|
127
172
|
## Para desarrollar
|
|
128
173
|
|
|
129
174
|
```sh
|
|
@@ -134,3 +179,31 @@ python -m venv .venv && .venv/Scripts/python -m pip install -e ".[test]"
|
|
|
134
179
|
|
|
135
180
|
`operx_cli/client.py` es lo único que habla con la red; el servidor MCP lo
|
|
136
181
|
usará igual.
|
|
182
|
+
|
|
183
|
+
### Publicar una versión
|
|
184
|
+
|
|
185
|
+
El paquete es `operx-cli` en PyPI. Una versión publicada no se puede reemplazar:
|
|
186
|
+
si algo falla, se sube la siguiente.
|
|
187
|
+
|
|
188
|
+
1. Sube el número en `pyproject.toml` y en `operx_cli/__init__.py`.
|
|
189
|
+
2. Las funciones nuevas del servidor (como `operx login`) tienen que estar ya
|
|
190
|
+
desplegadas en producción antes de publicar: el paquete nuevo las llama.
|
|
191
|
+
3. Desde `cli/`:
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
uv build --clear
|
|
195
|
+
uv publish
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`uv publish` toma el token de PyPI de la variable de entorno
|
|
199
|
+
`UV_PUBLISH_TOKEN`. Usa un token solo del proyecto `operx-cli` (PyPI › Account
|
|
200
|
+
settings › API tokens). Se guarda una vez, en tu usuario de Windows, desde
|
|
201
|
+
PowerShell; lo pegas donde lo pide y no se ve:
|
|
202
|
+
|
|
203
|
+
```powershell
|
|
204
|
+
$token = Read-Host "Token de PyPI" -AsSecureString
|
|
205
|
+
[Environment]::SetEnvironmentVariable("UV_PUBLISH_TOKEN", [Net.NetworkCredential]::new("", $token).Password, "User")
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Las terminales que abras después ya lo tienen. El token es secreto: nunca va
|
|
209
|
+
en un archivo del repositorio ni en un chat.
|
|
@@ -6,38 +6,75 @@ tú, con tu rol, y solo en tu empresa.
|
|
|
6
6
|
|
|
7
7
|
## Instalar
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Windows (PowerShell):
|
|
10
|
+
|
|
11
|
+
```powershell
|
|
12
|
+
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"; uv tool install operx-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Mac y Linux:
|
|
10
16
|
|
|
11
17
|
```sh
|
|
12
|
-
|
|
13
|
-
operx --version
|
|
18
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh && uv tool install operx-cli
|
|
14
19
|
```
|
|
15
20
|
|
|
21
|
+
`uv` instala su propio Python, así que no necesitas tener uno. Si ya tienes
|
|
22
|
+
Python 3.10 o más nuevo, también sirve `pipx install operx-cli`. Abre una
|
|
23
|
+
terminal nueva y comprueba con `operx --version`.
|
|
24
|
+
|
|
25
|
+
Para actualizar: `uv tool upgrade operx-cli`.
|
|
26
|
+
|
|
16
27
|
## Entrar
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
```sh
|
|
30
|
+
operx login
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
1. La terminal muestra un código y abre el navegador en la página de OperX.
|
|
34
|
+
2. Entras como siempre. La página dice qué terminal pide acceso: apruébalo solo
|
|
35
|
+
si acabas de escribir `operx login` en tu propio computador y el código es
|
|
36
|
+
el mismo. Escribe el código de 6 dígitos de tu app de autenticación (o tu
|
|
37
|
+
contraseña, si no usas una) y elige *Solo lectura* o *Lectura y
|
|
38
|
+
escritura*.
|
|
39
|
+
3. Vuelve a la terminal: ya entraste. Nunca ves ni copias un token.
|
|
21
40
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```
|
|
41
|
+
Tu rol necesita el permiso «Acceso por API» (Ajustes › Usuarios y acceso ›
|
|
42
|
+
Roles); un administrador lo da. El acceso dura 90 días y lo ves, y puedes
|
|
43
|
+
revocarlo, en el menú de tu cuenta › Acceso por API.
|
|
26
44
|
|
|
27
45
|
El token queda en el llavero del sistema (en Windows, el Administrador de
|
|
28
|
-
credenciales), nunca en un archivo. En un servidor sin
|
|
29
|
-
|
|
46
|
+
credenciales), nunca en un archivo. En un servidor sin navegador ni llavero:
|
|
47
|
+
`operx login --token` te deja pegar un token creado en Acceso por API, o puedes
|
|
48
|
+
usar variables de entorno: `OPERX_TOKEN` (el token) y, si no es producción,
|
|
49
|
+
`OPERX_DIRECCION`.
|
|
30
50
|
|
|
31
51
|
Para otra dirección u otra empresa, usa un perfil:
|
|
32
52
|
|
|
33
53
|
```sh
|
|
34
|
-
operx --perfil pruebas
|
|
54
|
+
operx --perfil pruebas login --direccion https://…
|
|
35
55
|
operx --perfil pruebas ordenes listar
|
|
36
56
|
```
|
|
37
57
|
|
|
38
|
-
`operx
|
|
58
|
+
`operx logout` borra el token de este equipo; para que deje de funcionar,
|
|
39
59
|
revócalo en Acceso por API.
|
|
40
60
|
|
|
61
|
+
## Modo interactivo
|
|
62
|
+
|
|
63
|
+
Escribe `operx` solo, en una terminal, y se abre la pantalla de bienvenida
|
|
64
|
+
(quién eres y los números de hoy: órdenes abiertas, vencidas, artículos bajo
|
|
65
|
+
el mínimo) con un prompt. Ahí escribes los comandos sin el `operx`, con
|
|
66
|
+
Tab para completar y ↑ para repetir lo anterior:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
operx › ordenes listar --vencidas
|
|
70
|
+
operx › buscar "bomba"
|
|
71
|
+
operx › ayuda
|
|
72
|
+
operx › salir
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`salir` (o Ctrl+D) termina la sesión sin borrar tu token. En un script o una
|
|
76
|
+
tubería, `operx` solo muestra la ayuda y no espera a nadie.
|
|
77
|
+
|
|
41
78
|
## Comandos
|
|
42
79
|
|
|
43
80
|
```sh
|
|
@@ -111,6 +148,13 @@ operx api POST work-orders/1234/attachments/ -a file=foto.jpg -d '{"file_type":
|
|
|
111
148
|
En Git Bash, una dirección que empieza por `/` se vuelve una ruta de Windows:
|
|
112
149
|
escríbela sin la barra inicial, como arriba.
|
|
113
150
|
|
|
151
|
+
## Para agentes de IA
|
|
152
|
+
|
|
153
|
+
`operx agentes` imprime una guía para un agente de IA (Claude, ChatGPT,
|
|
154
|
+
Copilot, Codex…): las reglas para usar `operx` bien (leer con `--json`,
|
|
155
|
+
preguntar antes de escribir, no tocar tokens) y cada comando con sus opciones.
|
|
156
|
+
Dile a tu agente: «Ejecuta `operx agentes` y sigue esa guía».
|
|
157
|
+
|
|
114
158
|
## Para desarrollar
|
|
115
159
|
|
|
116
160
|
```sh
|
|
@@ -121,3 +165,31 @@ python -m venv .venv && .venv/Scripts/python -m pip install -e ".[test]"
|
|
|
121
165
|
|
|
122
166
|
`operx_cli/client.py` es lo único que habla con la red; el servidor MCP lo
|
|
123
167
|
usará igual.
|
|
168
|
+
|
|
169
|
+
### Publicar una versión
|
|
170
|
+
|
|
171
|
+
El paquete es `operx-cli` en PyPI. Una versión publicada no se puede reemplazar:
|
|
172
|
+
si algo falla, se sube la siguiente.
|
|
173
|
+
|
|
174
|
+
1. Sube el número en `pyproject.toml` y en `operx_cli/__init__.py`.
|
|
175
|
+
2. Las funciones nuevas del servidor (como `operx login`) tienen que estar ya
|
|
176
|
+
desplegadas en producción antes de publicar: el paquete nuevo las llama.
|
|
177
|
+
3. Desde `cli/`:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
uv build --clear
|
|
181
|
+
uv publish
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`uv publish` toma el token de PyPI de la variable de entorno
|
|
185
|
+
`UV_PUBLISH_TOKEN`. Usa un token solo del proyecto `operx-cli` (PyPI › Account
|
|
186
|
+
settings › API tokens). Se guarda una vez, en tu usuario de Windows, desde
|
|
187
|
+
PowerShell; lo pegas donde lo pide y no se ve:
|
|
188
|
+
|
|
189
|
+
```powershell
|
|
190
|
+
$token = Read-Host "Token de PyPI" -AsSecureString
|
|
191
|
+
[Environment]::SetEnvironmentVariable("UV_PUBLISH_TOKEN", [Net.NetworkCredential]::new("", $token).Password, "User")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Las terminales que abras después ya lo tienen. El token es secreto: nunca va
|
|
195
|
+
en un archivo del repositorio ni en un chat.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# operx para agentes de IA
|
|
2
|
+
|
|
3
|
+
Esta guía es para un agente de IA (Claude, ChatGPT, Copilot, Codex…) que usa
|
|
4
|
+
`operx` en nombre de una persona. `operx` llama a la API de OperX con el token
|
|
5
|
+
de esa persona: actúa como ella, con su rol, y solo en su empresa. Cada
|
|
6
|
+
llamada queda en el historial del token, marcada como hecha desde la CLI.
|
|
7
|
+
|
|
8
|
+
## Reglas
|
|
9
|
+
|
|
10
|
+
1. Escribe siempre el comando completo (`operx ordenes listar …`). No abras el
|
|
11
|
+
modo interactivo: `operx` solo, fuera de una terminal, solo muestra la ayuda.
|
|
12
|
+
2. Lee con `--json`: es lo que responde la API, completo. Las tablas son para
|
|
13
|
+
personas.
|
|
14
|
+
3. Antes de crear o cambiar algo, dile a la persona qué vas a hacer y espera
|
|
15
|
+
su sí. Con su sí, agrega `--si`: sin él, toda escritura falla fuera de una
|
|
16
|
+
terminal.
|
|
17
|
+
4. Nunca pidas, muestres ni guardes un token o una contraseña. Si no hay
|
|
18
|
+
sesión, pídele a la persona que ejecute `operx login`.
|
|
19
|
+
5. Si una escritura dice que la API no respondió a tiempo, no la repitas:
|
|
20
|
+
consulta primero si quedó hecha.
|
|
21
|
+
6. Si un nombre coincide con varios registros, `operx` lo dice con sus ids:
|
|
22
|
+
usa el id, no adivines.
|
|
23
|
+
7. Usa un comando propio cuando exista; `operx api` es para lo demás.
|
|
24
|
+
|
|
25
|
+
## Entrar
|
|
26
|
+
|
|
27
|
+
- Empieza con `operx cuenta --json`: quién es la persona, su empresa, su rol
|
|
28
|
+
y los productos de la empresa (`products`).
|
|
29
|
+
- Si responde «No has entrado», la persona ejecuta `operx login`: se abre su
|
|
30
|
+
navegador, aprueba con su código y la terminal queda conectada 90 días.
|
|
31
|
+
Puedes ejecutarlo tú si la persona está frente al computador: espera hasta
|
|
32
|
+
10 minutos a que apruebe.
|
|
33
|
+
- En un servidor sin navegador, la persona pone el token en la variable de
|
|
34
|
+
entorno `OPERX_TOKEN` (y `OPERX_DIRECCION` si no es producción).
|
|
35
|
+
- `--perfil NOMBRE` (o `OPERX_PERFIL`) usa otra empresa u otra dirección.
|
|
36
|
+
|
|
37
|
+
## Leer
|
|
38
|
+
|
|
39
|
+
- Las listas traen la primera página y avisan en stderr cuántas filas hay en
|
|
40
|
+
total. `--todas` las trae todas.
|
|
41
|
+
- `--buscar TEXTO` filtra en el servidor. `operx buscar TEXTO --json` busca en
|
|
42
|
+
todo (órdenes, equipos, clientes, lugares y personas), desde 2 letras.
|
|
43
|
+
- `ver` da un registro completo: `operx ordenes ver 1234 --json`.
|
|
44
|
+
- En `--json` los valores van como los manda la API, en inglés: el `status`
|
|
45
|
+
de una orden es `open`, `in_progress`, `on_hold`, `completed`, `verified` o
|
|
46
|
+
`cancelled`, y su `priority` es `low`, `medium`, `high` o `critical`. Las
|
|
47
|
+
opciones de los comandos van en español: `--estado en-progreso`.
|
|
48
|
+
- Los resultados van a stdout y los avisos a stderr:
|
|
49
|
+
`operx equipos listar --todas --json > equipos.json` guarda solo los datos.
|
|
50
|
+
- `--csv` da un CSV para Excel (separado por punto y coma, con decimales con
|
|
51
|
+
coma).
|
|
52
|
+
|
|
53
|
+
## Escribir
|
|
54
|
+
|
|
55
|
+
- Escriben: `ordenes crear`, `ordenes estado`, `ordenes asignar`,
|
|
56
|
+
`ordenes comentar`, `equipos crear`, `clientes crear`, `inventario entrada`,
|
|
57
|
+
`inventario salida`, `lecturas registrar`, `personas importar --confirmar`,
|
|
58
|
+
y `operx api` con POST, PUT, PATCH o DELETE. Todos llevan `--si`.
|
|
59
|
+
- `operx personas importar ARCHIVO` sin `--confirmar` solo revisa el archivo
|
|
60
|
+
y no guarda nada: hazlo primero y muéstrale el resultado a la persona.
|
|
61
|
+
- Un acceso de solo lectura no escribe: la API responde 403.
|
|
62
|
+
- Una orden se verifica en la app, nunca por la API. Un token nunca firma ni
|
|
63
|
+
aprueba, y la API nunca abre resultados médicos, nómina, quejas de
|
|
64
|
+
convivencia ni los archivos del SG-SST. Si algo responde 403, dile a la
|
|
65
|
+
persona que lo haga en la app (operx.pro).
|
|
66
|
+
|
|
67
|
+
## Nombres, números y fechas
|
|
68
|
+
|
|
69
|
+
- Equipos, lugares, clientes, artículos y personas se nombran por su código o
|
|
70
|
+
su nombre exacto (sin importar mayúsculas), o por su id. Un nombre a medias
|
|
71
|
+
no sirve: busca primero con `listar --buscar` o `operx buscar`.
|
|
72
|
+
- `yo` es la persona dueña del token: `--asignada-a yo`, `--asignar-a yo`.
|
|
73
|
+
`operx ordenes asignar 1234 nadie` quita la asignación.
|
|
74
|
+
- Números como en la app: `73,4` o `1.500,50` (también `73.4`).
|
|
75
|
+
- Fechas con el día primero: `30/10/2026`; con hora: `22/10/2026 08:00`.
|
|
76
|
+
También sirven `2026-10-30` y `2026-10-22T08:00`. La hora es la del
|
|
77
|
+
computador.
|
|
78
|
+
|
|
79
|
+
## Errores
|
|
80
|
+
|
|
81
|
+
| Código de salida | Qué pasó |
|
|
82
|
+
|---|---|
|
|
83
|
+
| 0 | Funcionó. |
|
|
84
|
+
| 1 | `operx` o la API lo rechazó. El motivo va en stderr, en una línea que empieza por `Error:`. |
|
|
85
|
+
| 2 | El comando está mal escrito: una opción que no existe o un dato que falta. Mira `operx COMANDO --help`. |
|
|
86
|
+
|
|
87
|
+
Lo que responde la API:
|
|
88
|
+
|
|
89
|
+
- **401**: el acceso venció o lo revocaron. La persona ejecuta `operx login`.
|
|
90
|
+
- **403**: el rol de la persona no lo permite, el acceso es de solo lectura,
|
|
91
|
+
la empresa no tiene ese producto, o un token no puede hacer eso.
|
|
92
|
+
- **404**: no existe, o la persona no lo puede ver.
|
|
93
|
+
- **400**: un dato no sirve. El mensaje dice cuál y por qué.
|
|
94
|
+
- **429**: demasiadas llamadas (120 por minuto por token, 600 por empresa).
|
|
95
|
+
`operx` espera y reintenta solo, hasta 3 veces.
|
|
96
|
+
|
|
97
|
+
## Cualquier otra llamada: `operx api`
|
|
98
|
+
|
|
99
|
+
`operx api MÉTODO RUTA` hace cualquier llamada que el token pueda hacer y
|
|
100
|
+
responde el JSON de la API. Las rutas están en la app, en Acceso por API ›
|
|
101
|
+
Documentación; escríbelas sin `/api/v1/` y sin barra inicial.
|
|
102
|
+
|
|
103
|
+
- `-q nombre=valor`: un filtro. Se repite.
|
|
104
|
+
- `-d '{"campo": "valor"}'` o `-d @archivo.json`: el cuerpo de la llamada.
|
|
105
|
+
- `-a campo=ruta`: un archivo a subir. Se repite.
|
|
106
|
+
- `--todas`: todas las páginas de un GET.
|
|
107
|
+
- `-o archivo`: guarda una respuesta que es un archivo (CSV, PDF).
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
operx api GET pm/schedules/ -q search=compresor
|
|
111
|
+
operx api POST contacts/ -d @contacto.json --si
|
|
112
|
+
operx api GET work-orders/export/ -o ordenes.csv
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Recetas
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
# Las órdenes vencidas de la persona
|
|
119
|
+
operx ordenes listar --vencidas --asignada-a yo --todas --json
|
|
120
|
+
|
|
121
|
+
# Pasar una orden a completada, después de su sí
|
|
122
|
+
operx ordenes estado 1234 completada --si
|
|
123
|
+
|
|
124
|
+
# Registrar una lectura: primero el número del punto
|
|
125
|
+
operx lecturas puntos BOM-04 --json
|
|
126
|
+
operx lecturas registrar --punto 12 --valor 73,4 --si
|
|
127
|
+
|
|
128
|
+
# Sacar stock: primero el código y el lugar exactos
|
|
129
|
+
operx inventario existencias --buscar filtro --json
|
|
130
|
+
operx inventario salida --articulo FIL-010 --lugar "Bodega central" --cantidad 2 --motivo Consumo --si
|
|
131
|
+
|
|
132
|
+
# Los equipos en un CSV para Excel
|
|
133
|
+
operx equipos listar --todas --csv > equipos.csv
|
|
134
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""`operx agentes`: the guide for AI agents (AGENTES.md), then every command
|
|
2
|
+
with its options, read from the commands themselves so the list matches the
|
|
3
|
+
installed version."""
|
|
4
|
+
|
|
5
|
+
from importlib.resources import files
|
|
6
|
+
|
|
7
|
+
ROOT_TITLE = "operx (opciones de cualquier comando)"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def guide_text(root):
|
|
11
|
+
rules = files("operx_cli").joinpath("AGENTES.md").read_text(encoding="utf-8")
|
|
12
|
+
return rules.rstrip("\n") + "\n\n" + reference(root)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def reference(root):
|
|
16
|
+
lines = ["## Todos los comandos", "", "Cada uno también tiene `--help`.", ""]
|
|
17
|
+
lines += _command_section(ROOT_TITLE, root)
|
|
18
|
+
for path, command in _commands(root, ["operx"]):
|
|
19
|
+
lines += _command_section(" ".join(path), command)
|
|
20
|
+
return "\n".join(lines)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _commands(group, path):
|
|
24
|
+
"""The commands a person can type, group by group, without the hidden ones."""
|
|
25
|
+
for name, command in group.commands.items():
|
|
26
|
+
if getattr(command, "hidden", False):
|
|
27
|
+
continue
|
|
28
|
+
if hasattr(command, "commands"):
|
|
29
|
+
yield from _commands(command, path + [name])
|
|
30
|
+
else:
|
|
31
|
+
yield path + [name], command
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _command_section(title, command):
|
|
35
|
+
lines = [f"### {title}", ""]
|
|
36
|
+
if command.help and title != ROOT_TITLE:
|
|
37
|
+
lines += [command.help.strip(), ""]
|
|
38
|
+
params = [param for param in command.params if not getattr(param, "hidden", False)]
|
|
39
|
+
lines += [f"- {_param_line(param)}" for param in params]
|
|
40
|
+
return lines + ([""] if params else [])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _param_line(param):
|
|
44
|
+
if param.param_type_name == "argument":
|
|
45
|
+
line = f"`{param.name.upper()}`"
|
|
46
|
+
else:
|
|
47
|
+
line = ", ".join(f"`{opt}`" for opt in param.opts)
|
|
48
|
+
choices = getattr(param.type, "choices", None)
|
|
49
|
+
if choices:
|
|
50
|
+
line = f"`{' '.join(param.opts)} [{'|'.join(str(choice) for choice in choices)}]`"
|
|
51
|
+
elif not getattr(param, "is_flag", False):
|
|
52
|
+
line += " VALOR"
|
|
53
|
+
marks = [mark for mark, on in (("obligatorio", param.required), ("se repite", getattr(param, "multiple", False))) if on]
|
|
54
|
+
if marks:
|
|
55
|
+
line += f" ({', '.join(marks)})"
|
|
56
|
+
help_text = (getattr(param, "help", None) or "").strip()
|
|
57
|
+
return f"{line}: {help_text}" if help_text else line
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""`operx`: the root of the commands, and what turns a refused call into
|
|
2
|
+
one line instead of a traceback."""
|
|
3
|
+
|
|
4
|
+
import errno
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Annotated, Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from typer.core import TyperGroup
|
|
11
|
+
|
|
12
|
+
from . import __version__, settings, shell
|
|
13
|
+
from .agent_guide import guide_text
|
|
14
|
+
from .client import ApiError
|
|
15
|
+
from .commands import account, inventario, lecturas, ordenes, personas, raw, registros, search
|
|
16
|
+
from .commands.common import CliError, Session
|
|
17
|
+
from .lookup import NoSingleMatch
|
|
18
|
+
from .spreadsheet import SpreadsheetError
|
|
19
|
+
|
|
20
|
+
# What a person can fix: shown as "Error: …", exit code 1.
|
|
21
|
+
EXPECTED_ERRORS = (ApiError, CliError, NoSingleMatch, SpreadsheetError, settings.SettingsError)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_closed_pipe(error):
|
|
25
|
+
"""What writing to a pipe whose reader left raises: BrokenPipeError (EPIPE),
|
|
26
|
+
and on Windows a bare EINVAL. That EINVAL names no file: a bad file name
|
|
27
|
+
raises the same errno with the name in it, and must not be hidden."""
|
|
28
|
+
if isinstance(error, BrokenPipeError) or error.errno == errno.EPIPE:
|
|
29
|
+
return True
|
|
30
|
+
return error.errno == errno.EINVAL and error.filename is None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _silence_stdout():
|
|
34
|
+
"""Python flushes stdout once more on the way out, which would print
|
|
35
|
+
"Exception ignored…" over the quiet exit: point it at nothing."""
|
|
36
|
+
try:
|
|
37
|
+
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
|
|
38
|
+
except (OSError, ValueError, AttributeError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ReportedErrors(TyperGroup):
|
|
43
|
+
def invoke(self, ctx):
|
|
44
|
+
try:
|
|
45
|
+
return super().invoke(ctx)
|
|
46
|
+
except EXPECTED_ERRORS as error:
|
|
47
|
+
typer.echo(f"Error: {error}", err=True)
|
|
48
|
+
raise typer.Exit(1) from None
|
|
49
|
+
except (typer.Abort, KeyboardInterrupt):
|
|
50
|
+
# Ctrl+C, or a question whose answer never came.
|
|
51
|
+
typer.echo("Se canceló.", err=True)
|
|
52
|
+
raise typer.Exit(1) from None
|
|
53
|
+
except OSError as error:
|
|
54
|
+
if not _is_closed_pipe(error):
|
|
55
|
+
raise
|
|
56
|
+
# `operx … | head`: whoever reads left before the end. Not an error.
|
|
57
|
+
_silence_stdout()
|
|
58
|
+
raise typer.Exit(0) from None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
app = typer.Typer(
|
|
62
|
+
cls=ReportedErrors,
|
|
63
|
+
help="OperX desde la terminal: tu empresa con tu token de API.",
|
|
64
|
+
# `operx` alone is the welcome and the prompt (root below), not the help.
|
|
65
|
+
no_args_is_help=False,
|
|
66
|
+
add_completion=False,
|
|
67
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
68
|
+
# A crash must never print the token, which lives in the client's locals.
|
|
69
|
+
pretty_exceptions_show_locals=False,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _show_version(value):
|
|
74
|
+
if value:
|
|
75
|
+
typer.echo(f"operx {__version__}")
|
|
76
|
+
raise typer.Exit()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def is_interactive():
|
|
80
|
+
"""A person at a terminal, not a script or a pipe: only then does a prompt
|
|
81
|
+
make sense."""
|
|
82
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.callback(invoke_without_command=True)
|
|
86
|
+
def root(
|
|
87
|
+
ctx: typer.Context,
|
|
88
|
+
perfil: Annotated[
|
|
89
|
+
Optional[str],
|
|
90
|
+
typer.Option("--perfil", "-p", help="El perfil: otra API u otra empresa. Sin él, «principal» (o OPERX_PERFIL)."),
|
|
91
|
+
] = None,
|
|
92
|
+
version: Annotated[
|
|
93
|
+
bool, typer.Option("--version", callback=_show_version, is_eager=True, help="La versión instalada.")
|
|
94
|
+
] = False,
|
|
95
|
+
):
|
|
96
|
+
if ctx.obj is None:
|
|
97
|
+
ctx.obj = Session()
|
|
98
|
+
ctx.obj.profile = settings.profile_name(perfil)
|
|
99
|
+
elif perfil:
|
|
100
|
+
ctx.obj.profile = perfil
|
|
101
|
+
# Else the interactive mode's session, which already has its profile:
|
|
102
|
+
# every line typed runs this callback again and must not reset it.
|
|
103
|
+
if ctx.invoked_subcommand is None:
|
|
104
|
+
open_the_prompt(ctx)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def open_the_prompt(ctx):
|
|
108
|
+
"""`operx` alone: at a terminal, the welcome and the prompt; in a script
|
|
109
|
+
or a pipe, the help (a prompt would hang it)."""
|
|
110
|
+
if ctx.obj.in_shell:
|
|
111
|
+
# Typed at the prompt (`--perfil staging`): it already did what it
|
|
112
|
+
# says; a second prompt must not open inside the first.
|
|
113
|
+
return
|
|
114
|
+
if not is_interactive() and ctx.obj.reader is None:
|
|
115
|
+
typer.echo(ctx.get_help())
|
|
116
|
+
raise typer.Exit()
|
|
117
|
+
shell.enter(ctx.obj, ctx.command)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
app.command()(account.login)
|
|
121
|
+
app.command()(account.logout)
|
|
122
|
+
# The first version's names, for whoever typed them: out of the help.
|
|
123
|
+
app.command(name="entrar", hidden=True)(account.entrar)
|
|
124
|
+
app.command(name="salir", hidden=True)(account.logout)
|
|
125
|
+
app.command()(account.cuenta)
|
|
126
|
+
app.command()(search.buscar)
|
|
127
|
+
app.command()(raw.api)
|
|
128
|
+
app.add_typer(ordenes.app, name="ordenes")
|
|
129
|
+
app.add_typer(registros.equipos, name="equipos")
|
|
130
|
+
app.add_typer(registros.clientes, name="clientes")
|
|
131
|
+
app.add_typer(registros.lugares, name="lugares")
|
|
132
|
+
app.add_typer(inventario.app, name="inventario")
|
|
133
|
+
app.add_typer(lecturas.app, name="lecturas")
|
|
134
|
+
app.add_typer(personas.app, name="personas")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@app.command()
|
|
138
|
+
def agentes(ctx: typer.Context):
|
|
139
|
+
"""La guía para agentes de IA (Claude, ChatGPT, Copilot): cómo usar operx bien, con cada comando y sus opciones."""
|
|
140
|
+
typer.echo(guide_text(ctx.find_root().command))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main():
|
|
144
|
+
# Redirected to a file, Windows would write the results in its own code
|
|
145
|
+
# page; the CSV and the JSON are UTF-8.
|
|
146
|
+
for stream in (sys.stdout, sys.stderr):
|
|
147
|
+
if hasattr(stream, "reconfigure"):
|
|
148
|
+
stream.reconfigure(encoding="utf-8")
|
|
149
|
+
app()
|