@imagina-build/ias-cli 0.0.0 → 0.1.0
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.
- package/README.md +406 -2
- package/bin/ias.mjs +19 -0
- package/package.json +31 -12
- package/schema/ias.schema.json +535 -0
- package/shared/credentials/cms.mjs +105 -0
- package/shared/credentials/index.mjs +354 -0
- package/shared/credentials/plist.mjs +93 -0
- package/shared/vault/approvers-vectors.mjs +188 -0
- package/shared/vault/approvers.mjs +234 -0
- package/shared/vault/index.mjs +583 -0
- package/shared/vault/plaintext.mjs +115 -0
- package/shared/vault/test-vectors/ias-vault-2/approvers.json +2122 -0
- package/shared/vault/test-vectors/ias-vault-2/keys.json +43 -0
- package/shared/vault/test-vectors/ias-vault-2/malformed.json +1739 -0
- package/shared/vault/test-vectors/ias-vault-2/plaintexts.json +149 -0
- package/shared/vault/test-vectors/ias-vault-2/runners.json +580 -0
- package/shared/vault/test-vectors/ias-vault-2/sealed.json +567 -0
- package/shared/vault/test-vectors/ias-vault-2/tampered.json +989 -0
- package/shared/vault/test-vectors/keys.json +31 -0
- package/shared/vault/test-vectors/malformed.json +1341 -0
- package/shared/vault/test-vectors/sealed.json +384 -0
- package/shared/vault/test-vectors/tampered.json +610 -0
- package/shared/vault/vectors.mjs +720 -0
- package/src/cli.mjs +231 -0
- package/src/commands/apps/index.mjs +36 -0
- package/src/commands/auth/index.mjs +141 -0
- package/src/commands/build/follow.mjs +168 -0
- package/src/commands/build/index.mjs +110 -0
- package/src/commands/build/local.mjs +69 -0
- package/src/commands/build/profile.mjs +50 -0
- package/src/commands/build/source.mjs +71 -0
- package/src/commands/builds/index.mjs +148 -0
- package/src/commands/credentials/index.mjs +256 -0
- package/src/commands/credentials/material.mjs +159 -0
- package/src/commands/devices/asc.mjs +116 -0
- package/src/commands/devices/index.mjs +65 -0
- package/src/commands/devices/sync.mjs +223 -0
- package/src/commands/dist/index.mjs +108 -0
- package/src/commands/doctor/index.mjs +158 -0
- package/src/commands/env/index.mjs +163 -0
- package/src/commands/github/index.mjs +53 -0
- package/src/commands/init/index.mjs +224 -0
- package/src/commands/migrate/credentials.mjs +213 -0
- package/src/commands/migrate/eas-cli.mjs +153 -0
- package/src/commands/migrate/index.mjs +73 -0
- package/src/commands/migrate/numbers.mjs +85 -0
- package/src/commands/migrate/profiles.mjs +174 -0
- package/src/commands/migrate/project.mjs +249 -0
- package/src/commands/migrate/survey.mjs +205 -0
- package/src/commands/migrate/variables.mjs +183 -0
- package/src/commands/push/index.mjs +334 -0
- package/src/commands/sdk/index.mjs +81 -0
- package/src/commands/submit/index.mjs +85 -0
- package/src/commands/update/index.mjs +526 -0
- package/src/commands/usage/index.mjs +57 -0
- package/src/commands/vault/approvers.mjs +129 -0
- package/src/commands/vault/index.mjs +159 -0
- package/src/commands/vault/runners.mjs +115 -0
- package/src/commands/vault/team.mjs +111 -0
- package/src/commands/version/index.mjs +71 -0
- package/src/commands/webhooks/index.mjs +104 -0
- package/src/commands/workflows/index.mjs +88 -0
- package/src/config.mjs +261 -0
- package/src/dist.mjs +99 -0
- package/src/errors.mjs +158 -0
- package/src/http.mjs +45 -0
- package/src/legacy.mjs +73 -0
- package/src/manage.mjs +120 -0
- package/src/ota.mjs +122 -0
- package/src/output.mjs +36 -0
- package/src/process.mjs +45 -0
- package/src/schema.mjs +131 -0
- package/src/sdk.mjs +120 -0
- package/src/secret-input.mjs +27 -0
- package/src/secrets.mjs +37 -0
- package/src/shared.mjs +5 -0
- package/src/team-vault.mjs +152 -0
- package/src/token-store.mjs +93 -0
package/README.md
CHANGED
|
@@ -1,3 +1,407 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `ias`: el CLI de Imagina Application Services
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`ias` da a cada app acceso a IAS desde la terminal. Es el paquete `@imagina/ias-cli` (pista 1B de [PLAN-APPLICATION-SERVICES.md](../../docs/plans/PLAN-APPLICATION-SERVICES.md)):
|
|
4
|
+
|
|
5
|
+
- Node 22 o posterior, sin dependencias: `util.parseArgs`, `fetch`, `node:crypto` y `node:test`;
|
|
6
|
+
- la configuración de cada app en `ias.json`;
|
|
7
|
+
- las actualizaciones OTA firmadas (`ias update`) y la distribución interna (`ias dist`), que antes eran `tools/expo-ota` y `tools/expo-dist`;
|
|
8
|
+
- el acceso a la API de gestión (`ias login`, pista 1D-2) y la bóveda sellada: las claves del equipo (`ias vault`), las credenciales de firma (`ias credentials`) y las variables (`ias env`), con el formato `ias-vault-2` de [docs/technical/vault.md](../../docs/technical/vault.md);
|
|
9
|
+
- las builds en los runners de IAS o en este Mac (`ias build`, `ias builds`, `ias version`), los envíos a las tiendas (`ias submit`) y lo que el equipo firma para que los runners se fíen (`ias vault approvers|grant|revoke`), con el contrato de [docs/technical/runner-api.md](../../docs/technical/runner-api.md) (pista 2C);
|
|
10
|
+
- salir de EAS paso a paso (`ias migrate eas`, pista 4D).
|
|
11
|
+
|
|
12
|
+
## 1. Instalación
|
|
13
|
+
|
|
14
|
+
No se publica en npm. Desde este repositorio:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node packages/cli/bin/ias.mjs --help
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Para tenerlo como `ias` en el Mac:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install --global ./packages/cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Sin el repositorio, desde npm, donde cada push a `main` que toca `packages/cli`, `packages/sdk` o `packages/shared` publica la versión de su `package.json` si aún no está ([`.github/workflows/packages.yml`](../../.github/workflows/packages.yml) y [`scripts/publish.mjs`](scripts/publish.mjs)). La organización de npm es `imagina-build`, así que se publican como `@imagina-build/ias-cli` y `@imagina-build/ias` (`REGISTRY_SCOPE` en `src/sdk.mjs`); las apps instalan el SDK con un alias, como `@imagina/ias`. Instalar no pide token:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install --global @imagina-build/ias-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
El workflow entra en npm con *trusted publishing* (OIDC, sin token guardado). npmjs.com solo deja que un paquete confíe en un workflow cuando ya existe, así que cada uno se crea antes, a mano y una vez, como un 0.0.0 vacío (`npm login` y `node packages/cli/scripts/publish.mjs --placeholder`); después, en npmjs.com, cada paquete › Settings › Trusted publisher › GitHub Actions: este repositorio y `packages.yml`.
|
|
33
|
+
|
|
34
|
+
`npm pack` a secas no sirve: no se lleva `packages/shared`, que `src/shared.mjs` importa por su ruta (decisión D5). [`scripts/pack.mjs`](scripts/pack.mjs) prepara una copia con `shared/` dentro (y el SDK compilado), la que publica `scripts/publish.mjs` (con `--dry-run`, sin publicar) y la que deja en `.tgz`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm run pack:release --prefix packages/cli -- ./release
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Fuera de un clon, `ias build --local` necesita `IAS_RUNNER_MODULE`.
|
|
41
|
+
|
|
42
|
+
## 2. Comandos
|
|
43
|
+
|
|
44
|
+
| Comando | Qué hace |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `ias init [--app <código>] [--force] [--dry-run]` | Escribe `ias.json` a partir de `app.json`, `eas.json`, `ota.config.json` y `dist.config.json`. Dice de dónde sale cada parte y qué se queda fuera. |
|
|
47
|
+
| `ias doctor` | Comprueba Node, `git`, `op`, Xcode y sus herramientas (`plutil`, `security`, `sips`, `unzip`), `aapt2` del SDK de Android y la configuración de la app. En una app con `package.json`, también el SDK `@imagina/ias`: instalado, de una versión compatible con la de `ias` (la misma *major* y, mientras es 0, la misma *minor*) y con su plugin en `app.json`; si la app no lo usa, es un aviso. No abre ningún secreto. |
|
|
48
|
+
| `ias sdk install [--from <origen>] [--dry-run]` | Instala `@imagina/ias` con el gestor de paquetes de la app (`packageManager` de `package.json` o su lockfile: npm, yarn, pnpm o bun), añade su plugin al final de los `plugins` de `app.json` sin tocar lo demás (con `app.config.*` no lo toca y dice qué añadir) y dice qué falta: `ias init`, `expo-updates`, `expo-notifications` y `expo-task-manager`, y un prebuild o una build nueva. El paquete sale de `--from` (un `.tgz`, su URL, una carpeta o una versión), de `IAS_SDK_SOURCE`, del `packages/sdk` del repositorio si `ias` corre desde un clon (compilado), o de npm como alias de `@imagina-build/ias@^<versión de ias>`. |
|
|
49
|
+
| `ias update publish\|republish\|rollback-to-embedded\|retire\|restore\|list\|check` | Las actualizaciones OTA, con el comportamiento de `tools/expo-ota` ([README](../../tools/expo-ota/README.md)). `rollback` vale por `rollback-to-embedded`. `--channel` firma con la clave de ese canal y `--rollout <1-100>` empieza con ese porcentaje de las instalaciones (§3.1). |
|
|
50
|
+
| `ias update promote <grupo> --to <canal>` | Publica un grupo en otro canal: comprueba sus bytes con el certificado de su canal y vuelve a firmar el contenido en este Mac con la clave del de destino, con un `id` y un `createdAt` nuevos. Nunca copia una firma (API.md §36.11). |
|
|
51
|
+
| `ias update rollout <grupo> <porcentaje>\|pause\|complete` | Sube o baja el porcentaje de un despliegue (de 1 a 99; 100 lo completa), lo pausa o lo completa (API.md §36.9). |
|
|
52
|
+
| `ias update channels` | Los canales del proyecto con su keyid y su certificado, y si este Mac firma para cada uno. |
|
|
53
|
+
| `ias dist upload\|list\|retire\|restore` | La distribución interna, con el comportamiento de `tools/expo-dist` ([README](../../tools/expo-dist/README.md)). |
|
|
54
|
+
| `ias login [--team <código>]` | Autoriza este ordenador con una passkey (autorización de dispositivo, RFC 8628) y guarda su token personal (`iasp_…`), que es de un equipo (§4). |
|
|
55
|
+
| `ias logout` | Revoca el token guardado (`DELETE /me/token`) y lo borra. No toca `IAS_TOKEN`. |
|
|
56
|
+
| `ias whoami` | De quién es el token, de qué equipo, de dónde sale y sus alcances. |
|
|
57
|
+
| `ias teams` | Los equipos de la persona, con su rol en cada uno, y cuál es el del token (`GET /teams`). |
|
|
58
|
+
| `ias apps [<código>]` | Las aplicaciones que ve el token, o una en detalle. |
|
|
59
|
+
| `ias usage [--month AAAA-MM]` | Lo que el equipo del token ha usado en un mes (minutos de build, artefactos, OTA, notificaciones, sincronización, dispositivos), por aplicación y con sus límites; con un límite agotado, las builds, las notificaciones o los dispositivos que cubre se rechazan con `limit_exceeded` (`GET /team/usage`, manage-api §17). |
|
|
60
|
+
| `ias vault init\|status` | Crea la clave de recuperación y la de firma del equipo en 1Password y registra sus públicas, con una aprobación con passkey; o compara las del servidor con las de 1Password y verifica la lista firmada de runners (§5). |
|
|
61
|
+
| `ias vault approvers` | Firma con la clave del equipo las passkeys de los `owner` que pueden aprobar una build de tienda (`ias-approvers-1`), después de enseñarlas con su huella y lo que cambia desde la versión firmada, y la sube (§5.2). |
|
|
62
|
+
| `ias vault grant\|revoke <runner>` | Deja a un runner abrir las credenciales del equipo (la lista firmada y una envoltura en cada sobre) o se lo quita cuando el panel ya lo revocó (§5.2). |
|
|
63
|
+
| `ias credentials import\|list\|show\|rotate\|retire` | Sella en este ordenador y sube las credenciales de firma, con sus metadatos sin secreto; las lista, enseña quién puede abrirlas y quién las firmó, las sustituye por una versión nueva y las retira (§5). |
|
|
64
|
+
| `ias env list\|set\|pull\|delete` | Las variables de cada entorno. `EXPO_PUBLIC_*` siempre en claro; las demás, selladas salvo `--plain`. `pull` escribe `.env.local` sin los secretos. |
|
|
65
|
+
| `ias build [--platform ios\|android\|all] [--profile <perfil>] [--message "…"] [--wait] [--local]` | Manda el commit de un árbol limpio a un runner de IAS con el perfil de `ias.json`, abre la aprobación de una build de tienda y sigue su registro hasta que acaba; con `--wait` baja los artefactos. Con `--local`, la hace en este Mac con `@imagina/ias-runner` (§6). |
|
|
66
|
+
| `ias builds list\|view\|logs\|cancel\|download` | Las builds de la aplicación: la lista, una en detalle, su registro (`--follow`), cancelarla y bajar sus artefactos comprobados (§6). |
|
|
67
|
+
| `ias submit <build> [--track <pista>] [--wait]` | Manda una build de tienda a App Store Connect o a Google Play, siempre como borrador, después de que un `owner` lo apruebe con su passkey (§6). |
|
|
68
|
+
| `ias version get\|set` | Los números de build que guarda el servidor por plataforma, los que toma `autoIncrement`; `set` los trae de EAS o de `app.json` y solo los deja subir (§6). |
|
|
69
|
+
| `ias push send\|messages\|cancel\|devices\|credentials` | Las notificaciones push: envía una ahora, la programa con `--at` o cuenta a quién llegaría con `--dry-run`; el historial con sus cifras; los dispositivos, con solo el principio y el final de su token; y las credenciales de APNs y FCM, que aprueba un propietario con su passkey (§5.1). |
|
|
70
|
+
| `ias devices list [--status <estado>]` | Los iPhone y iPad de quien prueba la app, con su estado (pendiente, aprobado, en Apple, rechazado), la versión del perfil que los lista y el enlace de registro (§6.1). |
|
|
71
|
+
| `ias devices sync [--yes] [--no-resign] [--detach]` | Da de alta en Apple los dispositivos aprobados, regenera el perfil ad hoc con todos ellos, lo sella y lo firma en la bóveda, y pide que un runner firme de nuevo la última build interna de iOS, sin compilarla (§6.1). |
|
|
72
|
+
| `ias webhooks list\|create\|delete\|test` | Los webhooks salientes de la aplicación: la lista, uno nuevo con sus eventos (`--events build.failed,device.pending` o `all`; imprime su secreto, una vez), borrarlo y mandarle una prueba. Cada evento llega como un POST firmado con `IAS-Signature` (docs/technical/manage-api.md §16.1). |
|
|
73
|
+
| `ias github link <owner/name>` | Vincula el repositorio de GitHub de la aplicación: sus etiquetas, ramas y pull requests lanzan los flujos de `ias.json`. La primera vez imprime lo que hay que poner en el webhook de GitHub (el secreto, una vez); el token de GitHub lo carga un propietario en el panel, con una passkey (§16.2). |
|
|
74
|
+
| `ias workflows list\|run` | Los flujos de `ias.json` en un commit del repositorio, y lanzar uno a mano (`ias workflows run release --ref main [--wait]`) (§16.3). |
|
|
75
|
+
| `ias migrate eas [profiles\|prepare\|import <carpeta>\|version\|env\|checklist]` | Sale de EAS paso a paso: los perfiles de `eas.json` a `ias.json`, una carpeta fuera del proyecto para `eas credentials`, las credenciales selladas en la bóveda, los números de build y las variables de EAS en el servidor, y lo que queda por hacer en expo.dev y en las tiendas. Sin paso, dice en qué punto está (§8). |
|
|
76
|
+
|
|
77
|
+
Cada comando tiene su ayuda: `ias update --help` o `ias update publish --help`. `--project <carpeta>` elige la app; si no, la carpeta actual.
|
|
78
|
+
|
|
79
|
+
**Salida.**
|
|
80
|
+
- Para personas: líneas según avanza.
|
|
81
|
+
- Con `--json`: un solo documento JSON en stdout al final, y el progreso en stderr.
|
|
82
|
+
- Los errores dicen qué ha pasado y cómo arreglarlo: `ias: no ias.json in …: run ias init there…`. Con `--json` salen como `{ "error": { "message", "fix", "code" } }`.
|
|
83
|
+
- Si el servidor responde con un error que se puede arreglar (un token que no es el del proyecto, un grupo que no existe…), una segunda línea da la pista.
|
|
84
|
+
|
|
85
|
+
**Códigos de salida**: `0` si funciona, `1` si falla y `2` si la línea de órdenes no se entiende (un comando o una opción que no existen, o falta un argumento).
|
|
86
|
+
|
|
87
|
+
## 3. `ias.json`
|
|
88
|
+
|
|
89
|
+
Va en la raíz de la app, junto a `app.json`, y en git: no lleva secretos. Su esquema es [`schema/ias.schema.json`](schema/ias.schema.json) (JSON Schema 2020-12), que también usarán el runner y el SDK. `src/schema.mjs` lo valida sin dependencias.
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"app": "jscheduler",
|
|
94
|
+
"server": "https://sync.domibi.com",
|
|
95
|
+
"build": {
|
|
96
|
+
"production": { "distribution": "store", "environment": "production", "channel": "production", "autoIncrement": true, "android": { "format": "aab" } },
|
|
97
|
+
"preview": { "distribution": "internal", "environment": "preview", "channel": "preview", "android": { "format": "apk" } }
|
|
98
|
+
},
|
|
99
|
+
"updates": {
|
|
100
|
+
"signing": { "keyId": "jscheduler-ota-1", "privateKey": "op://Private/jScheduler clave firma OTA jscheduler-ota-1/private-key.pem" },
|
|
101
|
+
"token": "op://Private/jScheduler token OTA/credential",
|
|
102
|
+
"envFile": ".env.production.local",
|
|
103
|
+
"bundleEnv": ["EXPO_PUBLIC_SIGNUP_CODE"]
|
|
104
|
+
},
|
|
105
|
+
"distribution": { "token": "op://Private/… token instalaciones/credential" },
|
|
106
|
+
"submit": {
|
|
107
|
+
"production": { "ios": { "ascAppId": "6822051274" }, "android": { "track": "production", "releaseStatus": "draft" } }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| Campo | Qué es |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| `app` | El código de la aplicación en IAS (`apps:create`). También es el del proyecto de distribución, salvo que `distribution.project` diga otro. |
|
|
115
|
+
| `server` | El origen de IAS. Por defecto, `https://ias.imagina.build`, que todavía no responde. Las OTA van al servidor de `updates.url` de `app.json`, que es al que preguntan las builds; sin ella, al de este campo (§3.2). |
|
|
116
|
+
| `build`, `submit` | Los perfiles de `eas.json`, sin credenciales. La Fase 2 (`ias build`) los completa. |
|
|
117
|
+
| `updates` | La firma del canal por defecto (`keyId`, `certificate` y `privateKey`), el `token` del proyecto OTA, `envFile` y `bundleEnv`. El servidor, el proyecto, el certificado y el keyid salen de `app.json` o, si no los tiene, de `ias.json`, como los escribe el plugin de `@imagina/ias` (§3.2). `channels`, la firma de los demás canales (§3.1). |
|
|
118
|
+
| `distribution` | El `token` del proyecto de distribución y, si no es el de `app`, su `project`. |
|
|
119
|
+
| `workflows` | Lo que lanza GitHub (una etiqueta, una rama o un pull request) y en qué pasos: `build` y `submit` (§3.0). |
|
|
120
|
+
| `vault` | `item`: el ítem de 1Password de la bóveda (`op://<bóveda>/<ítem>`), si no es `op://Private/IAS vault <host del servidor>`. |
|
|
121
|
+
|
|
122
|
+
### 3.0 Flujos (`workflows`)
|
|
123
|
+
|
|
124
|
+
Lo que hace GitHub al recibir una etiqueta, una rama o un pull request del repositorio vinculado (`ias github link`), en pasos que van en orden y de los que el primero que falla para el flujo (docs/technical/manage-api.md §16.3):
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
"workflows": {
|
|
128
|
+
"release": { "on": { "tag": "v*" }, "steps": [ { "build": { "profile": "production", "platform": "all" } }, { "submit": { "ios": { "target": "app_store_connect" }, "android": { "target": "google_play", "track": "production" } } } ] },
|
|
129
|
+
"preview": { "on": { "pull_request": true }, "steps": [ { "build": { "profile": "preview", "platform": "all" } } ] }
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- `on`: `tag` o `branch` (con `*`) o `pull_request: true`. Los pasos son `build` (un perfil de `build` y `platform`: `ios`, `android` o `all`) y `submit` (envía a las tiendas las builds del `build` anterior, siempre como borrador).
|
|
134
|
+
- No hay paso de publicar una OTA: la clave de firma vive en el Mac.
|
|
135
|
+
- Las builds de tienda y los envíos esperan la aprobación de un propietario con su passkey, aunque los lance GitHub. Un pull request solo usa perfiles internos y nunca compila un commit de un fork.
|
|
136
|
+
- El servidor lee `ias.json` y `app.json` del commit: el perfil con sus `extends` resueltos, y la versión y el número de build de `app.json`.
|
|
137
|
+
|
|
138
|
+
### 3.1 Canales y despliegues graduales
|
|
139
|
+
|
|
140
|
+
Cada canal del proyecto OTA tiene su clave (API.md §36.8). El de `app.json` firma como siempre; los demás van en `updates.channels`, con su `keyId`, la referencia `op://` de su clave privada y el certificado que llevan sus builds (un fichero del repositorio):
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
"updates": {
|
|
144
|
+
"signing": { "keyId": "jscheduler-ota-1", "privateKey": "op://Private/jScheduler clave firma OTA jscheduler-ota-1/private-key.pem" },
|
|
145
|
+
"channels": {
|
|
146
|
+
"staging": { "keyId": "jscheduler-staging-1", "privateKey": "op://Private/jScheduler clave firma OTA staging/private-key.pem", "certificate": "certs/ota-staging-certificate.pem" }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- `ias update publish --channel staging` firma con la clave de `staging`. Antes de firmar, compara el keyid y el certificado del canal en el servidor (`GET …/channels`) con los suyos y para si no coinciden: las builds de ese canal rechazarían la actualización.
|
|
152
|
+
- `ias update promote <grupo> --to production` comprueba el grupo con el certificado de su canal que tiene el Mac (el de `app.json` o el de `updates.channels`), nunca con el que dice el servidor, y lo vuelve a firmar con la clave del de destino.
|
|
153
|
+
- `--rollout <1-100>` en `publish`, `republish`, `rollback` y `promote`: la publicación empieza en ese porcentaje. Al comprobar lo publicado, el CLI pide el manifiesto como una instalación de dentro del porcentaje (calcula un `EAS-Client-ID` que cae dentro, como el servidor).
|
|
154
|
+
- `ias update rollout <grupo> 25`, `pause` o `complete` (100 también completa). El servidor lo pausa solo si la nueva falla al arrancar más que la anterior (API.md §36.10); se reanuda con un porcentaje.
|
|
155
|
+
|
|
156
|
+
**Secretos: solo `op://` o variables de entorno.** `ias` se niega a cargar un `ias.json` con un secreto escrito:
|
|
157
|
+
|
|
158
|
+
- un `token` o una `privateKey` que no sean una referencia `op://`;
|
|
159
|
+
- cualquier valor que parezca un token (`ota_…`, `dist_…`, `iasp_…`, `iasr_…`) o una clave PEM.
|
|
160
|
+
|
|
161
|
+
La clave privada tampoco puede ser la ruta de un fichero. Para usar un fichero, se pone su ruta en `IAS_UPDATES_PRIVATE_KEY`; tiene que estar fuera del proyecto y con permisos `600`.
|
|
162
|
+
|
|
163
|
+
**Variables de entorno**, que mandan sobre `ias.json`:
|
|
164
|
+
|
|
165
|
+
| Variable | Qué sustituye |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `IAS_TOKEN` | El token: `updates.token` en `ias update`, `distribution.token` en `ias dist` y el de `ias login` en los demás. Una referencia `op://` o el token. En la CI, un token de robot (`iasr_…`). |
|
|
168
|
+
| `IAS_TOKEN_STORE` | Dónde guarda `ias login` el token: `keychain` (por defecto en macOS), `file` (por defecto en los demás) o `none` (por defecto si `CI` está puesta). |
|
|
169
|
+
| `IAS_VAULT_ITEM` | `vault.item`: el ítem de 1Password de la bóveda. |
|
|
170
|
+
| `BROWSER` | Con qué se abre el panel (`none` para no abrirlo); por defecto `open` en macOS y `xdg-open` en Linux. |
|
|
171
|
+
| `IAS_UPDATES_PRIVATE_KEY` | `updates.signing.privateKey`: una referencia `op://` o un fichero. |
|
|
172
|
+
| `IAS_SERVER` | `server`, salvo en la URL de las OTA que sale de él (§3.2). |
|
|
173
|
+
| `OTA_TOKEN`, `OTA_PRIVATE_KEY`, `DIST_TOKEN`, `DIST_SERVER` | Las de `tools/`, que siguen valiendo y mandan sobre las de `IAS_`. |
|
|
174
|
+
|
|
175
|
+
**Autenticación.** `ias update` y `ias dist` usan, por este orden, `OTA_TOKEN` o `DIST_TOKEN`, `IAS_TOKEN`, el token de `ias.json` y, si no hay ninguno, el que guardó `ias login`. Valen el token de robot del proyecto (`ota_…`, `dist_…`), uno de robot de la aplicación (`iasr_…`) o el personal (`iasp_…`), si tienen el alcance. Si le llega el del otro servicio, lo dice antes de llamar al servidor. Los demás comandos hablan con la API de gestión (docs/technical/manage-api.md), que no acepta `ota_` ni `dist_`: usan `IAS_TOKEN` o el token de `ias login`.
|
|
176
|
+
|
|
177
|
+
### 3.2 Lo que `app.json` deja a `ias.json`
|
|
178
|
+
|
|
179
|
+
Con el plugin de `@imagina/ias` en los `plugins` de `app.json` (`packages/sdk/README.md` §10), la configuración de las OTA puede ir solo en `ias.json`. `ias update` la lee con las mismas reglas con que el plugin la escribe en las builds:
|
|
180
|
+
|
|
181
|
+
| En `app.json` | Si falta, de `ias.json` |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| `updates.url` | `<server>/api/v1/ota/<app>/manifest`, con el `server` de `ias.json` (o `https://ias.imagina.build`). `IAS_SERVER` no cuenta: el plugin no la lee, así que las builds no preguntan ahí. |
|
|
184
|
+
| `updates.codeSigningCertificate` | `updates.signing.certificate`: el certificado del canal por defecto, un fichero del repositorio. |
|
|
185
|
+
| `updates.codeSigningMetadata.keyid` | `updates.signing.keyId`. |
|
|
186
|
+
|
|
187
|
+
- Lo que está en los dos tiene que coincidir: el mismo fichero (aunque se escriba distinto) y el mismo keyid. Si no, es un error, como en el plugin.
|
|
188
|
+
- Si a `app.json` le falta alguno y sus `plugins` no tienen `@imagina/ias` (ni `@imagina/ias/plugin`), `ias update` para: ninguna build llevaría ese valor. Con un `app.config.*`, que el CLI no lee, da por hecho que lo añade.
|
|
189
|
+
- Una URL de IAS que `app.json` ya tiene se queda: es la que piden las builds publicadas (jScheduler, `sync.domibi.com`).
|
|
190
|
+
- Las builds de un canal con clave propia (§3.1) llevan su certificado porque el runner exporta `IAS_BUILD_PROFILE` (el perfil del trabajo) a todos sus pasos, y el plugin elige el canal con él. El runner comprueba después que la build lleva el certificado de `expo config --json`.
|
|
191
|
+
|
|
192
|
+
## 4. `ias login`
|
|
193
|
+
|
|
194
|
+
1. Pide un código (`POST /auth/device`) y lo enseña: `Your code: WDJB-MJHT`.
|
|
195
|
+
2. Abre el panel (Autorizar el CLI). La persona comprueba que enseña el mismo código y lo aprueba con una passkey.
|
|
196
|
+
3. Pregunta cada `interval` segundos (5 más con cada `slow_down`) hasta que llega el token, se rechaza o caduca a los 10 minutos.
|
|
197
|
+
4. Guarda el token (90 días) para el servidor (`IAS_SERVER`, `server` de `ias.json` o `ias.imagina.build`):
|
|
198
|
+
- en macOS, en el llavero, con el servicio `ias` y la cuenta = el origen del servidor. El token le llega a `security -i` por stdin, nunca como argumento de un proceso;
|
|
199
|
+
- fuera de macOS, en `$XDG_CONFIG_HOME/ias/tokens.json` (`~/.config` por defecto), con permisos 600 y la carpeta 700. Si el fichero lo pueden leer otros, `ias` se niega a usarlo;
|
|
200
|
+
- en la CI no se guarda: el token es `IAS_TOKEN`, y `ias login` lo dice.
|
|
201
|
+
|
|
202
|
+
`ias logout` revoca el token y lo borra; si ya no valía, solo lo borra.
|
|
203
|
+
|
|
204
|
+
**Un token es de un equipo** (docs/technical/phase-5.md §1.4): sirve para las aplicaciones de ese equipo y con el rol que la persona tiene en él.
|
|
205
|
+
- `ias login --team <código>` pide ese equipo (`team` en `POST /auth/device`) y el panel lo deja preseleccionado. Quien está en varios equipos elige uno al aprobar; quien está en uno, no elige nada. `ias login` sin `--team` deja la elección al panel.
|
|
206
|
+
- El token que llega dice su equipo (`team` en la respuesta y en `ias whoami`).
|
|
207
|
+
- Para trabajar en otro equipo, `ias login --team <otro>` emite otro token, que sustituye al guardado para ese servidor. `ias teams` lista los equipos de la persona y marca el del token.
|
|
208
|
+
- La bóveda de cada equipo es suya. Quien está en dos equipos guarda sus claves en dos ítems de 1Password: el de `ias.json` (`vault.item`) de cada aplicación dice cuál.
|
|
209
|
+
|
|
210
|
+
## 5. La bóveda, las credenciales y las variables
|
|
211
|
+
|
|
212
|
+
El material de firma se sella **en este ordenador** con `ias-vault-2` ([docs/technical/vault.md](../../docs/technical/vault.md)): para la clave de recuperación del equipo y para los runners de la lista que firmó el equipo, y firmado con la clave de firma del equipo. El servidor guarda el sobre y los metadatos, y no puede abrirlo ni firmarlo.
|
|
213
|
+
|
|
214
|
+
**Las claves del equipo** viven en un ítem de 1Password (`vault.item`, `IAS_VAULT_ITEM` o `--item`; por defecto `op://Private/IAS vault <host>`), con los campos de docs/technical/vault.md §2.2.
|
|
215
|
+
- `ias vault init` (un `owner`) las crea con `node:crypto`, crea el ítem con `op item create` pasándole la plantilla por stdin, y registra las públicas (`POST /vault/keys`). El servidor responde `202 approval_required`: el CLI abre la aprobación, enseña los dos `kid` para compararlos con los del panel y pregunta cada 3 s hasta que se aprueba. Si el ítem ya existe (un intento anterior sin aprobar), registra sus claves.
|
|
216
|
+
- `ias vault status` lee solo las públicas de 1Password, las compara con las del servidor y verifica la lista firmada de runners. Sale con 1 si algo no cuadra.
|
|
217
|
+
|
|
218
|
+
**Antes de sellar**, cada comando lee `GET /vault` y para si las claves del servidor no son las del ítem, si la bóveda no tiene claves o si la lista de runners no está firmada por el equipo. Los runners que el servidor enseña fuera de la lista firmada se quedan fuera, con un aviso.
|
|
219
|
+
|
|
220
|
+
**`ias credentials import`**:
|
|
221
|
+
|
|
222
|
+
| Tipo | Opciones |
|
|
223
|
+
|---|---|
|
|
224
|
+
| `apple_distribution_certificate` (del equipo) | `--p12 <fichero>`, `--password <ref>` |
|
|
225
|
+
| `apple_provisioning_profile` | `--profile <fichero>` |
|
|
226
|
+
| `android_upload_keystore` | `--keystore <fichero>`, `--store-password <ref>`, `--key-alias <alias>` y, si es otra, `--key-password <ref>` |
|
|
227
|
+
| `apns_key` (del equipo) | `--p8 <fichero>` (`AuthKey_<id>.p8` dice su id; si no, `--key-id`), `--team-id <id>` |
|
|
228
|
+
| `asc_api_key` (del equipo) | Lo mismo, más `--issuer-id <uuid>` |
|
|
229
|
+
| `google_service_account` | `--service-account <fichero>` |
|
|
230
|
+
| Todo lo de un `credentials.json` de EAS | `--eas <carpeta>`: el certificado, el perfil y el keystore, comprobados contra el bundle id y el equipo de la aplicación (`--force` para seguir con problemas) |
|
|
231
|
+
|
|
232
|
+
- **Ficheros**: una ruta o una referencia `op://`.
|
|
233
|
+
- **Contraseñas**: `op://…`, `env:NOMBRE` (una variable del entorno) o `-` (stdin). **Nunca la contraseña misma**: `ias` la rechaza, porque `ps` y el historial de la shell la verían.
|
|
234
|
+
- El tipo se puede omitir cuando las opciones lo dicen. `--name` da el nombre; si no, sale del material.
|
|
235
|
+
- El CLI saca los metadatos con `@imagina/ias-shared/credentials` (`metadataOf`), genera el `id` (un UUID), sella el texto en claro del tipo (docs/technical/vault.md §3.5) y sube `{ id, type, name, metadata, envelope }` a `/apps/<app>/credentials` o, si es del equipo, a `/team/credentials`.
|
|
236
|
+
- Una credencial que ya está (el mismo certificado, perfil o clave) se salta; `ias credentials rotate <id>` la sustituye por la versión siguiente. `--dry-run` lo hace todo menos subirla.
|
|
237
|
+
|
|
238
|
+
`ias credentials list` (con `--all`, también las retiradas), `show <id>` (con `--verify`, comprueba la firma con la clave de 1Password), `rotate <id>` y `retire <id>`.
|
|
239
|
+
|
|
240
|
+
**`ias env`**:
|
|
241
|
+
- `ias env set NOMBRE [valor] --environment <entorno>`: `EXPO_PUBLIC_*` siempre en claro (y `--secret` da error). Cualquier otra se sella como `environment_variable` salvo `--plain`, y su valor llega con `--from op://…|env:NOMBRE|-`, nunca en la línea de órdenes. Una secreta que ya existe conserva su `id` y sube de versión.
|
|
242
|
+
- `ias env list [--environment <entorno>]` enseña los valores en claro y, de las secretas, solo su versión.
|
|
243
|
+
- `ias env pull [--environment development] [--file .env.local]` escribe las variables en claro, con permisos 600, y deja las secretas como comentario. No escribe sobre un fichero que no escribió él salvo con `--force`.
|
|
244
|
+
- `ias env delete NOMBRE --environment <entorno>`.
|
|
245
|
+
|
|
246
|
+
### 5.1 Las notificaciones push
|
|
247
|
+
|
|
248
|
+
Con el alcance `push:send` ([docs/technical/manage-api.md](../../docs/technical/manage-api.md) §14). Las apps dan de alta sus dispositivos solas (API.md §38).
|
|
249
|
+
|
|
250
|
+
- `ias push send --title … [--body …] [--to …] [--platform ios] [--language es] [--data url=/avisos/42] [--ttl 1d] [--at <fecha>] [--dry-run] [--wait]`:
|
|
251
|
+
- `--to all` (por defecto), o `--to topic:avisos`, `user:<id>`, `organization:<código>`, `space:<id>`, `group:<id>` o `device:<id>`, repetido para más del mismo tipo;
|
|
252
|
+
- `--platform`, `--app-version`, `--runtime`, `--channel` y `--language` la acotan (un segmento);
|
|
253
|
+
- las traducciones y lo demás del §14.2 van en un JSON con `--from aviso.json`; las opciones mandan sobre él;
|
|
254
|
+
- `--dry-run` cuenta a cuántos dispositivos llegaría, por plataforma e idioma, sin enviar nada; `--wait` espera las cifras.
|
|
255
|
+
- `ias push messages [<id>]` y `ias push cancel <id>`: el historial con sus cifras, y cancelar una programada.
|
|
256
|
+
- `ias push devices [--platform] [--user] [--topic] [--status active|retired|all]`.
|
|
257
|
+
- `ias push credentials` enseña sus metadatos; `ias push credentials ios <AuthKey_XXXXXXXXXX.p8>` o `android <cuenta.json>` (una ruta o una referencia `op://`) carga una. Es la excepción de la bóveda: el servidor envía con ella, así que la recibe tal cual (por https), la comprueba y la guarda cifrada con `PUSH_CREDENTIALS_KEY` cuando un propietario la aprueba con su passkey. Nunca se imprime.
|
|
258
|
+
|
|
259
|
+
### 5.2 Lo que el equipo firma para los runners
|
|
260
|
+
|
|
261
|
+
Un runner solo se fía de la clave de firma del equipo y del panel que fijó al registrarse ([docs/technical/runner-api.md](../../docs/technical/runner-api.md) §0). Lo demás se lo da el servidor firmado por el equipo, y estos comandos lo firman en el ordenador de un miembro con la clave de 1Password, después de comprobar que las claves del servidor son las del ítem.
|
|
262
|
+
|
|
263
|
+
- **`ias vault approvers`** (un `owner`): lee las passkeys de los `owner` (`GET /team/approvers`) y la versión firmada que ya hay (`GET /team/approvers-document`), que verifica con la clave del equipo. Enseña cada passkey con su miembro, su nombre, su fecha y su huella (el SHA-256 de su clave pública, calculado aquí: no se fía de la del servidor), y marca las nuevas, las que cambian y las que se van. Después de confirmar (un terminal, o `--yes`), firma la versión siguiente de `ias-approvers-1` ([docs/technical/vault.md](../../docs/technical/vault.md) §2.4) y la sube (`PUT /team/approvers-document`). Si ya está firmada tal cual, no firma nada. Si la del servidor no verifica, para; `--replace` firma encima después de mirar por qué.
|
|
264
|
+
- **`ias vault grant <runner>`** (el id, el nombre o el `kid` del runner): enseña su `kid` de bóveda para compararlo con el que imprimió `ias-runner register` en su Mac, y después de confirmar:
|
|
265
|
+
1. firma la lista de runners con él y el `serial` siguiente y la sube (`PUT /vault/runner-list`);
|
|
266
|
+
2. abre cada sobre del equipo (las credenciales no retiradas de cada aplicación y del equipo, y las variables secretas) con la clave de recuperación, lo envuelve para el runner y lo firma de nuevo (`PATCH /credentials/{id}/recipients`). Un sobre que no verifica con la clave del equipo, o que es de otra credencial, no se firma: lo dice y sale con 1 al final.
|
|
267
|
+
- **`ias vault revoke <runner>`**: primero se revoca en el panel (Sistema › Runners), que deja de darle trabajos y sobres al momento. Después, esto firma una lista sin él y quita su envoltura de cada sobre, firmando de nuevo, y lista lo que pudo leer: es lo que hay que renovar si su Mac pudo estar comprometido (docs/technical/vault.md §10).
|
|
268
|
+
|
|
269
|
+
Ninguno imprime ni escribe una clave privada: la de firma y la de recuperación se leen con `op read` a memoria.
|
|
270
|
+
|
|
271
|
+
## 6. Las builds y los envíos a las tiendas
|
|
272
|
+
|
|
273
|
+
Con el contrato de [docs/technical/runner-api.md](../../docs/technical/runner-api.md) §4 y §6. Una build de tienda necesita un token personal (`ias login`) y la aprobación de un `owner` con su passkey; una interna vale también con un token de robot con `builds:create`.
|
|
274
|
+
|
|
275
|
+
**`ias build`**:
|
|
276
|
+
1. Exige que la app esté en la raíz de su repositorio de git y que el árbol esté limpio (ni cambios ni ficheros sin seguimiento): la build se hace del commit, y lo que no está en él no estaría en la build.
|
|
277
|
+
2. Resuelve el perfil de `ias.json` (`--profile`, `production` por defecto): sigue `extends` y deja explícitos `distribution` y `environment`. Un perfil con `developmentClient` o `ios.simulator` solo se hace con `--local`.
|
|
278
|
+
3. Hace el tarball del commit con `git archive` y calcula su SHA-256 y su tamaño. La versión y el número de build salen del `app.json` del commit; con `autoIncrement`, el servidor da el siguiente (`ias version`).
|
|
279
|
+
4. Por cada plataforma (`--platform`, `all` por defecto, iOS primero) crea la build (`POST /apps/<app>/builds`), sube el código a la URL prefirmada (que no se imprime ni lleva el token) y la completa (`POST /builds/<id>/source/complete`). Una de tienda queda esperando la aprobación: abre su página y enseña el hash que enseña el panel.
|
|
280
|
+
5. Sigue el estado cada 5 s y el registro según llega, hasta que acaba (Ctrl-C deja de seguirla, no la cancela; `--detach` no la sigue). Sale con 1 si no acaba bien.
|
|
281
|
+
6. Con `--wait`, baja sus artefactos a `~/ias-builds/<app>/<plataforma>-<versión>-<número>` (o `--out`) y comprueba el tamaño y el SHA-256 de cada uno.
|
|
282
|
+
|
|
283
|
+
**`ias build --local`** hace lo mismo en este Mac, importando `@imagina/ias-runner` (`packages/runner`, o `IAS_RUNNER_MODULE`): una plataforma detrás de otra, a baja prioridad, con el cerrojo de la máquina (`--wait` espera a otra build que lo tenga). El material de firma es `--credentials op://<bóveda>/<elemento>` o una carpeta 700, o `IAS_IOS_CREDENTIALS` e `IAS_ANDROID_CREDENTIALS`. No habla con el servidor.
|
|
284
|
+
|
|
285
|
+
**`ias builds`**: `list [--status <estado>] [--platform]`, `view <id>`, `logs <id> [--follow]`, `cancel <id>` (quien la lanzó o un `owner`) y `download <id> [--kind ipa,dsyms] [--out <carpeta>]`. El registro es el que mandó el runner, ya limpio de secretos.
|
|
286
|
+
|
|
287
|
+
**`ias submit <build>`**: una build de tienda que acabó bien va a App Store Connect (con el Apple ID de `--asc-app-id` o de `submit.<perfil>.ios.ascAppId`) o a Google Play (la pista de `--track`, de `submit.<perfil>.android.track` o `internal`), **siempre como borrador** (D7): la revisión se pide en la tienda. La aprueba un `owner` con su passkey y la manda el runner con las claves de la bóveda. `--wait` espera a que acabe y dice qué respondió la tienda.
|
|
288
|
+
|
|
289
|
+
**`ias version get|set`**: los últimos números de build por plataforma. `set --ios <n> --android <n>` o `set --from-app-json` (un `owner`) los fija, por ejemplo al traerlos de EAS; solo suben.
|
|
290
|
+
|
|
291
|
+
### 6.1 Los dispositivos de quien prueba y las nuevas firmas
|
|
292
|
+
|
|
293
|
+
Con el contrato de [docs/technical/manage-api.md](../../docs/technical/manage-api.md) §15 y [docs/technical/runner-api.md](../../docs/technical/runner-api.md) §11. Es lo que hacen `eas device:create` y `eas build:resign`.
|
|
294
|
+
|
|
295
|
+
1. **Quien prueba** abre en Safari el enlace de registro de la aplicación (el panel: la aplicación › Distribución › Dispositivos) e instala el perfil que descarga. iOS lo marca como «No firmado»: solo envía el UDID, el modelo y el nombre.
|
|
296
|
+
2. **Un miembro** lo aprueba en el panel con una passkey reciente.
|
|
297
|
+
3. **`ias devices sync`**, en el ordenador de un miembro, en la carpeta de la app:
|
|
298
|
+
- lee de 1Password las claves del equipo y las compara con las del servidor;
|
|
299
|
+
- abre en memoria, con la clave de recuperación, la clave de API de App Store Connect de la bóveda (`ias credentials import asc_api_key`);
|
|
300
|
+
- enseña qué va a dar de alta en Apple y qué dispositivos lista el perfil nuevo, y pide confirmación (`--yes` en un script);
|
|
301
|
+
- registra los aprobados en Apple, crea el perfil ad hoc con el certificado del equipo y con todos los dispositivos que deben estar (los del perfil actual que siguen activos y los aprobados, sin los rechazados);
|
|
302
|
+
- lo sella para la recuperación y los runners de la lista firmada, lo firma con la clave del equipo y lo sube como versión nueva de la credencial del perfil;
|
|
303
|
+
- pide la nueva firma de la última build interna de iOS y la sigue hasta que está en Distribución, donde el enlace estable la sirve (`--detach` no la espera; `--no-resign` no la pide).
|
|
304
|
+
|
|
305
|
+
Si el perfil ya lista todos los aprobados, no hace nada (`--force` lo regenera igualmente). El perfil anterior se queda en Apple Developer. La clave de App Store Connect y el JWT con el que firma cada petición nunca se imprimen ni se escriben en disco. `IAS_APPLE_API_URL` cambia la dirección de la API, solo para las pruebas.
|
|
306
|
+
|
|
307
|
+
## 7. La transición desde `tools/`
|
|
308
|
+
|
|
309
|
+
- **`tools/expo-ota/publish.mjs` y `tools/expo-dist/upload.mjs` siguen funcionando**, para que jscheduler-app y jero-invoices-app no tengan que cambiar nada.
|
|
310
|
+
- Son envoltorios finos de `ias update` y `ias dist` (`src/legacy.mjs`).
|
|
311
|
+
- Tienen los mismos argumentos, también las opciones que ignoraban, y los mismos ficheros de configuración.
|
|
312
|
+
- Dan las mismas salidas, con sus prefijos `expo-ota:` y `expo-dist:`, y los mismos errores y códigos de salida.
|
|
313
|
+
- Se comprobó byte a byte contra las herramientas de antes en 56 casos el 20 de octubre de 2026: uso, `--help`, negativas, errores del servidor y respuestas grabadas.
|
|
314
|
+
- Solo cambia lo que antes no pasaba:
|
|
315
|
+
- una app con `ias.json`;
|
|
316
|
+
- `IAS_TOKEN`, que también vale;
|
|
317
|
+
- el token del otro servicio, que se rechaza antes de llamar al servidor;
|
|
318
|
+
- una respuesta correcta que no es JSON, como la página de un proxy, que ahora da un error claro también en `tools/expo-ota`.
|
|
319
|
+
- **Qué fichero se lee**: `ias.json` si existe, también desde `tools/`; si no, `ota.config.json` y `dist.config.json`, con sus reglas de siempre.
|
|
320
|
+
- **Para pasar una app a `ias`**:
|
|
321
|
+
1. `ias init` en su carpeta, y revisar lo que dice que se queda fuera.
|
|
322
|
+
2. `ias doctor`.
|
|
323
|
+
3. Commit de `ias.json`.
|
|
324
|
+
4. Cambiar los scripts y la documentación de la app:
|
|
325
|
+
|
|
326
|
+
| Antes | Ahora |
|
|
327
|
+
| --- | --- |
|
|
328
|
+
| `node ../application-services/tools/expo-ota/publish.mjs publish --message "…"` | `ias update publish --message "…"` |
|
|
329
|
+
| `… publish.mjs republish <grupo>` | `ias update republish <grupo>` |
|
|
330
|
+
| `… publish.mjs rollback-to-embedded` | `ias update rollback` |
|
|
331
|
+
| `node ../application-services/tools/expo-dist/upload.mjs upload <fichero>` | `ias dist upload <fichero>` |
|
|
332
|
+
|
|
333
|
+
5. Cuando nada llame ya a `tools/`, se pueden borrar `ota.config.json`, `dist.config.json` y, al final, `tools/`.
|
|
334
|
+
- `eas.json` se queda mientras las builds se hagan con EAS o con `store-build.mjs`. `ias init` no lo toca; `ias migrate eas checklist` dice cuándo quitarlo (§8).
|
|
335
|
+
|
|
336
|
+
## 8. Salir de EAS (`ias migrate eas`)
|
|
337
|
+
|
|
338
|
+
Lo que jscheduler-app hizo a mano el 19 de octubre de 2026 (su `docs/local-builds.md`, con `scripts/store-credentials.mjs`), para cualquier app que siga en EAS (pista 4D). Es un asistente por pasos: cada uno se puede repetir sin estropear nada y dice qué ha hecho y qué falta. Sin paso, `ias migrate eas` dice en qué punto está la app y cuál es el siguiente.
|
|
339
|
+
|
|
340
|
+
| Paso | Qué hace |
|
|
341
|
+
| --- | --- |
|
|
342
|
+
| `profiles [--app <código>] [--server <origen>] [--force] [--dry-run]` | Pasa los perfiles de build y de envío de `eas.json` a `ias.json`, con la conversión de `ias init`. Un `ias.json` nuevo se escribe entero; uno que ya existe solo gana los perfiles que le faltan (`--force` toma los de `eas.json` cuando difieren). Dice lo que no tiene equivalente y qué hacer en su lugar: las variables de cada perfil, `resourceClass`, las claves de envío, los ganchos `eas-build-*` de `package.json`, un `releaseStatus` que no es `draft`… |
|
|
343
|
+
| `prepare [--eas-project <id>] [--owner <cuenta>]` | Una carpeta en `$TMPDIR`, fuera del proyecto y con permisos 700, con `app.json` (nombre, identificadores y el `projectId` de EAS), `eas.json` (sus perfiles de build, sin `env`) y `node_modules` enlazado al de la app. Enseña las órdenes de `eas credentials`, que **ejecutas tú**: manejan secretos y preguntan. Si `app.json` ya no nombra el proyecto, `--eas-project` y `--owner`. Repetido, reutiliza la misma carpeta. |
|
|
344
|
+
| `import <carpeta> [--dry-run] [--force]` | Lee `credentials.json` y `credentials/` con `inspectEasCredentials` (`@imagina/ias-shared/credentials`), enseña sus metadatos sin secretos y lo que no cuadra con `app.json`, e importa cada credencial por el mismo camino que `ias credentials import --eas`: sellada y firmada por el equipo en este Mac, con el `id` hecho antes de sellar. Lo que ya está en la bóveda se salta. Después **borra lo bajado**, también si falla a medias o se interrumpe con Ctrl-C, y dice qué se importó y qué falta. Con `--dry-run` no envía nada y deja la descarga. |
|
|
345
|
+
| `version [--ios <n>] [--android <n>] [--profile <perfil>] [--dry-run]` | Los últimos números de build de EAS (`eas build:version:get --json`), los de `app.json` si `appVersionSource` no es `remote`, o los de las opciones, a `PUT /apps/<app>/build-numbers` ([docs/technical/runner-api.md](../../docs/technical/runner-api.md) §4.1). Gana el mayor, y el del servidor **nunca baja**: solo se envía la plataforma que sube. |
|
|
346
|
+
| `env [--environment <entorno>] [--env-file <fichero>] [--force] [--dry-run]` | Las variables de cada entorno de EAS (`eas env:list`, o el `.env` de `eas env:pull` con `--env-file`): `EXPO_PUBLIC_*` en claro y las demás selladas, como `ias env set`. Las secretas de EAS solo las leen sus builders: las lista para `ias env set … --from op://…`. Una variable que ya está en IAS se queda salvo `--force`, que sella la versión siguiente con su mismo `id`. Si el `.env` está en la carpeta de `prepare`, lo borra al acabar. |
|
|
347
|
+
| `checklist` | Lo que queda en IAS, en la app, en expo.dev y en las tiendas, adaptado a lo que encuentra, como el §7 de `docs/local-builds.md`: comparar la huella de la clave de subida con Play Console, cambiar `updates.url` en una build nueva, la cuenta de servicio en la carpeta del proyecto, quitar las claves de envío de EAS, las de push, borrar el proyecto de EAS, revisar las credenciales de la cuenta (pueden servir a otras apps del equipo) y borrar la carpeta de `prepare`. **No borra nada.** |
|
|
348
|
+
|
|
349
|
+
**El orden.** `profiles` va primero, porque los demás pasos toman de `ias.json` la aplicación y el servidor. Para una app en EAS:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
ias migrate eas profiles --app verifire --server https://sync.domibi.com
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Después, `ias migrate eas prepare`; en la carpeta que imprime, `EAS_NO_VCS=1 eas credentials -p ios` y `-p android` (perfil `production` › «credentials.json: Upload/Download…» › «Download credentials from EAS to credentials.json»; nunca «Download existing keystore», que imprime las contraseñas); y desde la app, `import <carpeta> --dry-run`, `import <carpeta>`, `version`, `env` y `checklist`. Cada descarga sustituye a la anterior: las credenciales de otro perfil (las ad hoc de `preview`) se importan después de bajarlas.
|
|
356
|
+
|
|
357
|
+
**Lo que ias ejecuta de eas-cli, y lo que no.**
|
|
358
|
+
- `eas build:version:get` y `eas env:list` solo leen y no preguntan: ias los ejecuta sin terminal, en la carpeta de la app si su `app.json` nombra el proyecto de EAS (si no, en la de `prepare`, con `EAS_NO_VCS=1`), sin las variables `IAS_*` en su entorno, y lee su salida sin imprimirla. Si fallan (eas-cli no está o no hay sesión en Expo), el error dice cómo hacerlo a mano: `--ios` y `--android`, o `eas env:pull` y `--env-file`.
|
|
359
|
+
- `eas credentials` y `eas env:pull` manejan secretos y preguntan: los ejecuta la persona, en la carpeta de `prepare`.
|
|
360
|
+
- eas-cli 24.14 no tiene `--json` en `env:list`: ias lee su formato `--format long`, y JSON si una versión posterior lo da. Se comprobó contra su código, no contra EAS.
|
|
361
|
+
|
|
362
|
+
**Secretos.** Ningún paso imprime una contraseña, una clave ni el valor de una variable que no sea `EXPO_PUBLIC_*`. La carpeta de `prepare` acaba como la dejó `prepare`, sin secretos, y se borra al final con `rm -rf` (su `node_modules` es un enlace: el de la app no se toca). `import` se niega a leer una carpeta dentro del proyecto o preparada para otra app, sin tocarla, y `checklist` avisa si `eas credentials` dejó `credentials.json` en el proyecto.
|
|
363
|
+
|
|
364
|
+
## 9. Estructura
|
|
365
|
+
|
|
366
|
+
- `bin/ias.mjs`: el ejecutable.
|
|
367
|
+
- `src/cli.mjs`: el enrutador.
|
|
368
|
+
- Cada comando es una carpeta de `src/commands/` con un `index.mjs`, así que uno nuevo no toca el enrutador.
|
|
369
|
+
- `vault`, `credentials` y `env` son de la pista 1D; `push`, de la 3B; `migrate`, de la 4D (`project.mjs`: la app y la carpeta de `prepare`; `eas-cli.mjs`: lo que se ejecuta y se lee de eas-cli; un fichero por paso); `build`, `builds`, `submit`, `version` y `vault approvers|grant|revoke`, de la 2C; `devices`, de la 4B (`asc.mjs`: la API de App Store Connect; `sync.mjs`: `ias devices sync`). `build/follow.mjs` (seguir y bajar), `build/source.mjs` (el commit y su tarball) y `build/profile.mjs` (el perfil resuelto) los comparten `build`, `builds` y `submit`; `vault/team.mjs`, los tres de confianza.
|
|
370
|
+
- `src/config.mjs`: carga `ias.json` o los ficheros viejos.
|
|
371
|
+
- `src/schema.mjs` y `schema/ias.schema.json`: el esquema y su validador.
|
|
372
|
+
- `src/ota.mjs` y `src/dist.mjs`: lo que no necesita red. `src/http.mjs`: las peticiones.
|
|
373
|
+
- `src/legacy.mjs`: las líneas de órdenes de `tools/`.
|
|
374
|
+
- `src/manage.mjs`: la API de gestión (el servidor, el token, las peticiones y las aprobaciones). `src/token-store.mjs`: dónde guarda el token `ias login`.
|
|
375
|
+
- `src/team-vault.mjs`: el ítem de 1Password de la bóveda y lo que hace falta para sellar, comprobado contra él. `src/secret-input.mjs`: de dónde se lee un secreto (`op://`, `env:`, `-`).
|
|
376
|
+
- `src/shared.mjs`: lo que se toma de `packages/shared` (`../../shared/…`). `npm pack` no lo lleva consigo: `scripts/pack.mjs` empaqueta una copia con `shared/` dentro (§1).
|
|
377
|
+
- `src/sdk.mjs`: el SDK `@imagina/ias` en una app (el gestor de paquetes, de dónde sale, su versión y su plugin en `app.json`), que comparten `ias sdk install` y `ias doctor`.
|
|
378
|
+
|
|
379
|
+
## 10. Pruebas
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
npm test --prefix packages/cli
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
En Node 22, `node --test` no acepta una carpeta: desde la raíz, `node --test 'packages/cli/test/*.test.mjs'`.
|
|
386
|
+
|
|
387
|
+
No usan red real, claves reales ni 1Password: el certificado lo crea `openssl` en una carpeta temporal. Prueban:
|
|
388
|
+
|
|
389
|
+
- `ias update` y `ias dist`, con las pruebas de `tools/` portadas;
|
|
390
|
+
- `ias.json`: el esquema, el cargador y sus negativas, y lo que `ias update` toma de él cuando `app.json` lo deja al plugin de `@imagina/ias` (`test/config.test.mjs`);
|
|
391
|
+
- `ias init` con la configuración de jscheduler-app y jero-invoices-app;
|
|
392
|
+
- `ias doctor` con programas simulados;
|
|
393
|
+
- `ias sdk install` y el SDK en `ias doctor` (`test/sdk.test.mjs`): el gestor de cada lockfile, de dónde sale el paquete, el plugin en `app.json` sin perder lo que había, lo que falta y cada estado del SDK, con un gestor de paquetes simulado;
|
|
394
|
+
- el `.tgz` del CLI y la copia que se publica (`test/pack.test.mjs`): lleva `shared/`, funciona sin el repositorio al lado y sale como `@imagina-build/ias-cli`, pública y sin `private`;
|
|
395
|
+
- el enrutador, `--json` y los códigos de salida;
|
|
396
|
+
- los envoltorios de `tools/`;
|
|
397
|
+
- **el contrato de la API de gestión** (`test/manage.test.mjs`): un servidor falso responde con las conversaciones de `tests/Fixtures/manage-v1` (escritas a mano o grabadas: el cuerpo vale como valor JSON o como texto), con un `op` y un `security` falsos en el `PATH` (`test/fakes.mjs`), material desechable hecho con `openssl` y `keytool`, y las claves públicas de prueba de `packages/shared/vault/test-vectors/ias-vault-2`. Comprueba lo que pide cada comando, cómo lee cada respuesta y que **ningún secreto** (contraseñas, claves privadas, tokens, el código de dispositivo) sale en stdout, en stderr ni en los argumentos de `op` o `security`;
|
|
398
|
+
- `ias build`, `builds`, `submit` y `version` (`test/builds.test.mjs`) contra la conversación grabada `tests/Fixtures/manage-v1/builds.json`, con repositorios de git desechables, el tarball comprobado byte a byte, la URL de subida y la de descarga sin token, y `--local` con un runner simulado que comprueba que nunca van dos builds a la vez;
|
|
399
|
+
- `ias devices list` y `sync` (`test/devices.test.mjs`) contra la conversación grabada `tests/Fixtures/manage-v1/devices.json` y un App Store Connect falso en 127.0.0.1, que comprueba cada token ES256 con la clave de prueba de `tests/Feature/Distribution/device-envelopes.json` y hace perfiles ad hoc firmados en CMS con un certificado desechable; el sobre que sella el CLI se abre después con la clave de recuperación de prueba;
|
|
400
|
+
- `ias vault approvers`, `grant` y `revoke` (`test/vault-trust.test.mjs`) contra la misma conversación y los vectores de `packages/shared/vault/test-vectors/ias-vault-2` (`approvers.json`, `runners.json`, `sealed.json` y `tampered.json`): cada documento, lista y sobre firmados se verifican como los verifica el runner, el runner autorizado abre lo que selló el equipo, y nunca se firma de nuevo un sobre manipulado;
|
|
401
|
+
- `ias push` (`test/push.test.mjs`), contra la conversación grabada `tests/Fixtures/manage-v1/push.json`, con una clave `.p8` y una cuenta de servicio desechables que no salen en ninguna salida;
|
|
402
|
+
- `ias migrate eas` (`test/migrate.test.mjs`): el asistente entero, de `profiles` a `checklist`, con un `eas` falso en el `PATH` que responde como eas-cli 24.14 (`credentials` escribe `credentials.json` y `credentials/` con material desechable, `build:version:get --json`, `env:list --format long` y `env:pull`), con `op` y `security` falsos y contra las conversaciones `credentials`, `builds`, `environment` y `vault-and-approvals`. Comprueba que la descarga se borra también cuando la importación falla a medias o se interrumpe con Ctrl-C, que no se toca lo que no se debe leer, que los números nunca bajan, y que ningún secreto sale en una salida, en los argumentos de `op` o de `eas` ni en un fichero que quede;
|
|
403
|
+
- **el contrato** (`test/contract.test.mjs`):
|
|
404
|
+
- un servidor falso responde con las conversaciones grabadas en `tests/Fixtures/v1/conversations/ota.json` y `distribution.json`;
|
|
405
|
+
- la prueba comprueba que el CLI pide lo mismo que la grabación (método, ruta, cabeceras y cuerpo) y que lee bien cada respuesta: manifiestos firmados, directivas, redirecciones al bucket y errores.
|
|
406
|
+
|
|
407
|
+
Las pruebas de `tools/` (`node --test tools/expo-ota/publish.test.mjs tools/expo-dist/upload.test.mjs`) siguen pasando sin cambios contra los envoltorios.
|
package/bin/ias.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ias: Imagina Application Services from the terminal. ias --help lists the commands; packages/cli/README.md explains them.
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { main } from '../src/cli.mjs';
|
|
5
|
+
import { cleanup } from '../src/process.mjs';
|
|
6
|
+
|
|
7
|
+
const [major] = process.versions.node.split('.').map(Number);
|
|
8
|
+
if (major < 22) {
|
|
9
|
+
process.stderr.write(`ias: Node ${process.versions.node} is too old: ias needs Node 22 or later\n`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
process.on('SIGINT', () => {
|
|
14
|
+
process.stderr.write('\nias: interrupted, cleaning up\n');
|
|
15
|
+
cleanup();
|
|
16
|
+
process.exit(130);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
process.exitCode = await main(process.argv.slice(2));
|
package/package.json
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
"name": "@imagina-build/ias-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The command line of Imagina Application Services (IAS): ias.json, OTA updates, internal distribution and the checks of a project.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ias": "bin/ias.mjs"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/cli.mjs",
|
|
11
|
+
"./schema/ias.schema.json": "./schema/ias.schema.json",
|
|
12
|
+
"./schema": "./src/schema.mjs",
|
|
13
|
+
"./config": "./src/config.mjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"schema",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md",
|
|
20
|
+
"shared"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"registry": "https://registry.npmjs.org",
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/efrain-salas/application-services.git"
|
|
32
|
+
}
|
|
14
33
|
}
|