La línea de comandos
remy, la herramienta que toda app de Remy instala con el paquete: los scripts, la estructura que leen, una app nueva, la publicación de paquetes, las traducciones, Cloudflare y los planes.
La herramienta es remy, el bin de @joeblew999/remy-ui (packages/ui/src/cli, un módulo de TypeScript por
comando); una app la obtiene con el paquete (npm install, en la versión del paquete) y la nombra en los
scripts de su package.json, los mismos en todas las apps ("dev:change": "remy change"). Una app indica una sola vez, en
remy.config.ts, dónde está desplegada y cuáles son sus puertos:
import type { RemyProject } from '@joeblew999/remy-ui/project';
export default {
origins: { production: 'https://your-app.your-subdomain.workers.dev', docs: 'https://your-app-docs.your-subdomain.workers.dev' },
ports: { preview: 4173, docsPreview: 4174 }, // the main checkout's; a worktree computes its own from its name
} satisfies RemyProject;La estructura
Todo repositorio de Remy, remy-auth incluido, tiene los mismos archivos en los mismos lugares; lo que hay dentro de los archivos del producto es diferente. La estructura son las rutas que leen los comandos y el paquete, así que un archivo en cualquier otro lugar simplemente no se encuentra; ninguna comprobación propia lo mantiene.
| Ruta | Leído por | Contiene |
|---|---|---|
package.json's scripts, remy.config.ts, .node-version | npm, remy | la entrada: todos los comandos; los orígenes, los puertos y el paquete de release; la versión de Node |
package.json, package-lock.json | npm, remy (workspaces) | las dependencias de la app; workspaces cuando es dueña de paquetes |
wrangler.jsonc | wrangler, el plugin de Vite de Cloudflare, remy cloudflare | el nombre del Worker, los bindings y los assets |
vite.config.ts, tsconfig.json, playwright.config.ts | Vite, remy check types, remy test | el build, los tipos y las comprobaciones de navegador |
fnox.toml, skills-lock.json | fnox, remy skills | los nombres de los secretos; las skills fijadas |
src/routes/, src/routeTree.gen.ts | TanStack Start (el árbol se genera) | las páginas del producto |
src/parts.json | el remyParts() del paquete | las partes compartidas que lista la app (ninguna sin el archivo; la app en blanco lista seo-routes) |
src/api/ | la ruta api.$ de la app | los procedimientos del producto, si tiene API |
tests/ (smoke.spec.ts por nombre) | remy test, el smoke en vivo tras un despliegue | las comprobaciones compartidas con los ajustes de la app, y las propias |
public/ | Vite | favicon, _headers |
dist/ (dist/client/assets) | los despliegues, las comprobaciones | resultado del build, nunca en commit |
docs/ (docs.config.ts, vite.config.ts, tsconfig.json, package.json, content/{users,dev,ui}/ con i18n.json y meta.json, content/questions.json; generados .remy-docs/, dist/) | remy docs, remy i18n (I18N_DOCS_DIR) | la documentación de la app: su identidad y sus páginas; el Worker es del paquete |
packages/<name>/ con README.md | remy plans, remy packages | los paquetes que publica el repositorio, si los hay |
messages/, project.inlang/ | el preset de la app, el script generate de la app, remy i18n | las cadenas propias de la app, si las hay (compiladas en src/paraglide/) |
.plans/ (now.md, done/, parked/) | remy plans | los planes del repositorio |
AGENTS.md, .github/workflows/ | agentes; GitHub | el puntero de los agentes a la documentación; CI |
packages/ui/src/cli/, template/, fixtures/consumer/ | el paquete; giget; remy test consumers | solo remy-auth: la propia línea de comandos, la app en blanco desde la que arranca un repositorio nuevo, y el fixture de consumidor que prueba ambos tal como los recibiría otro repositorio |
Un consumidor nuevo
La receta única. Una app nueva arranca desde la app en blanco, template/
en remy-auth en la tag de la versión: la estructura de arriba con solo los archivos propios del repositorio
dentro (sus nombres, una página de inicio en cada idioma, un índice de documentación por sitio, los
archivos de configuración de la estructura con apenas unas líneas cada uno). Todo lo demás es el paquete,
que un solo cambio de versión actualiza.
- Requisitos: Node según indica
.node-version,gh auth logincon un token que tengaread:packages, Google Chrome (lo usan las comprobaciones) ywrangler loginantes del primer despliegue. npx -y giget@3.3.1 gh:joeblew999/remy-auth/template#vX.Y.Z <name>, luegocd <name>ygit init(las herramientas de traducción y de planes leen el historial de git;npm run setuplo indica si falta).- Nombra la aplicación: sustituye
my-appyMy app(grep -rn -i "my.app" --exclude-dir=node_modules .):package.json,wrangler.jsonc,src/service.ts, los orígenes deremy.config.ts,docs/docs.config.ts(el nombre y la fuente del producto, que también lee el marco de la app) ymessages/en.json. - Node según indica
.node-version, luegoGITHUB_TOKEN=$(gh auth token) npm installuna vez para escribirpackage-lock.json, luegonpm run setup(npm ci, las skills fijadas y la skillremy, el bloque de reglas deAGENTS.md, el registro MCP, el guardián del flujo, las comprobaciones de la instalación). - Haz commit de todo,
package-lock.json,skills-lock.jsonysrc/routeTree.gen.tsincluidos. Las cadenas de la app en blanco ya vienen traducidas; cuando el inglés cambie (messages/en.json, la documentación), haz commit de ello y traduce esto una vez a mano,REMY_FLOW=hand npx remy i18n translate, que escribe y hace commit de los demás idiomas; a partir de entonces lo hacedev:land. - El primer despliegue, a mano:
npx remy cloudflare deploy, ynpx remy cloudflare docs-deploypara la documentación. Una vez que el repositorio está en GitHub (paso 7), el trabajo pasa por el flujo:dev:landpara cada cambio terminado,dev:promotepara producción y la documentación. - CI:
.github/workflows/google.ymlllama al flujo de trabajo compartido de remy-auth en la misma tag (todos los idiomas, las auditorías de Google en cada push amain). Da al repositorio nuevo acceso de lectura en la configuración del paquete@joeblew999/remy-ui("Manage Actions access"), onpm cifalla allí.
Después, la app crece en sus propios archivos: páginas en src/routes/ (una página junto a la ruta de un
padre, como videos.$videoId al lado de videos, es videos_.$videoId.tsx: TanStack anida
videos.$videoId dentro de videos, que entonces necesita un <Outlet />), sus enlaces en
src/remy-app.tsx, cadenas en messages/en.json (m desde src/paraglide/messages.js), una API como
paquete de contrato y src/api/ (todo procedimiento que recibe entrada documenta un error, o apiChecks
falla: la regla api/coverage del paquete), documentación en docs/content/. Pasa a una versión nueva con
npm run upgrade -- <version> (el paquete y la tag del flujo de trabajo de CI, juntos). Dos ejecuciones de
navegador locales a la vez comparten PREVIEW_PORT y la segunda falla al iniciar su servidor: dale a cada
shell el suyo (PREVIEW_PORT=4232 npm run …).
Un repositorio que publica paquetes
Un repositorio también puede publicar paquetes propios (un contrato, un paquete de UI), como hace
remy-auth; eso no cuesta ningún comando propio. Sus paquetes son workspaces de npm (packages/<name>/,
workspaces de package.json), y cada uno que no es private se publica mediante remy packages
(npm run packages -- …):
| Comando | Hace |
|---|---|
check, upgrade | Versiones npm más nuevas de las dependencias propias del repositorio (raíz y workspaces; nunca sus propios paquetes ni @joeblew999/remy-ui), previsualizadas (check) o aplicadas, y luego las comprobaciones de la instalación (upgrade) |
pack | Un tarball de cada paquete publicado, para que una app de prueba lo instale antes de cualquier release |
release | Las puertas de control (traducciones en modo estricto, cada paquete modificado con su versión subida, remy verify, el script release-checks de la app, auditorías de Google, Core Web Vitals), y luego tag; dev:release lo ejecuta |
tag | Sobre un main limpio: etiqueta la versión del paquete de release, hace push y luego publish --release |
publish | Publica cada paquete cuya versión es nueva en su registro; con --release vX.Y.Z también el release de GitHub a partir de CHANGELOG.md (notes). El job de CI de la tag ejecuta el mismo comando |
En remy.config.ts: release.package (el paquete cuya versión da nombre al release) y
release.title. El exports de un paquete nombra cada subruta que sus usuarios importan
("./video": "./src/video.ts"); cualquier otra cosa se rechaza en tiempo de build y en Playwright, cuyo
mensaje nombra el archivo que importa, no el mapa de exports. Las cadenas propias de un paquete son su
propio proyecto inlang, compilado por su build, cuyo runtime sigue el idioma de la plataforma con una sola
llamada, followLocale(runtime) (locale); las tareas de i18n lo leen a través de I18N_INLANG. Las
comprobaciones que el repositorio añade a un release van en su script release-checks (remy-auth:
template:check, ui:verify). remy-auth fija la versión de un release con npm run ui:version -- X.Y.Z,
que la escribe en todos los lugares donde aparece (el paquete y los pines de la app en blanco).
Elegir la versión: publicada, de desarrollo o local
La herramienta y el paquete son una sola cosa: el npm run dev:release de remy-auth publica
@joeblew999/remy-ui X.Y.Z, bin incluido, y etiqueta ese mismo commit como vX.Y.Z. Quien consume el
paquete fija ese único número en su package.json (y en la tag de su flujo de trabajo de CI), y
npm run upgrade -- X.Y.Z cambia ambos. Para trabajar contra el checkout propio de remy-auth,
npm link ../remy-auth/packages/ui (y npm unlink para volver).
La plataforma aporta todos los paquetes npm que necesitan la app y las tareas, en una sola versión: son
dependencias propias de @joeblew999/remy-ui (el framework, la cadena de herramientas, las herramientas de
las comprobaciones), así que el package.json de una app nombra el paquete, sus propios paquetes y lo que
su producto añada. the check's single-copies part (dentro de remy check) falla cuando un paquete que se
rompe si se instala por duplicado (React, TanStack Router y Query, el stringifier de MDX; la lista
remy.singleCopy del paquete) está presente: quita el pin propio de la app. Los comandos llaman a los
ganchos propios de una app donde los tiene, como scripts de su package.json, que se ejecutan si existen:
generate (código generado antes de comprobar los tipos; el de la app en blanco compila su propio
catálogo), local (el estado local que una app necesita antes de ejecutarse, que dev, build y
dev:start ejecutan primero; remy-auth escribe ahí su .dev.vars y migra su D1 local), release-checks y
test:consumers.
El flujo de desarrollo es código, packages/ui/src/cli/flow.ts, con cuatro pasos y un guardián
(cómo trabajamos):
| Paso | Script |
|---|---|
| Primero | dev:status (main con los veredictos de GitHub, los despliegues, los worktrees por delante y por detrás, las traducciones, las pull requests; un hook SessionStart lo ejecuta para los agentes) |
| Un trabajo | dev:start -- <name> (un worktree propio desde main, npm ci, puertos propios, el guardián); dev:done lo elimina cuando el trabajo termina y queda fusionado |
| Cada cambio | dev:change (remy change, cada parte una función: planes, copias únicas, rutas cuando cambia un archivo de ruta, tipos, las comprobaciones unitarias, las traducciones como aviso, la documentación cuando cambia) |
| Trabajo terminado | dev:land -- "<message>" (la comprobación, commit, main, push, el despliegue en staging, y luego la traducción cuando está desactualizada; GitHub ejecuta entonces npm run test -- every, google y consumers en paralelo, y una ejecución en rojo comenta en el commit. Las mismas tres en local, a propósito, por una persona: REMY_FLOW=hand npm run test -- <level>) |
| Producción | dev:promote (se niega ante un commit que GitHub no ha aprobado; luego el despliegue a producción, el despliegue de la documentación, lo que ejecuta cada despliegue) |
| Un release | dev:release (remy packages release, que ejecuta remy verify) |
| Un área | npm run test -- only <words> |
| El guardián | npx remy guard lo registra (npm run setup lo hace); npx remy guard --check lo verifica (las comprobaciones de la instalación lo hacen) |
npm run test -- every es cada una de nuestras comprobaciones en todos los idiomas (dentro de remy verify).
remy verify y CI terminan con remy test consumers, los demás repositorios a los que este sirve,
probados tal como los reciben: el script test:consumers de la app, nada por defecto; en remy-auth, el
fixture de consumidor, que construye la app en
blanco y un paquete propio a partir de la plataforma de este commit (el paquete desde un tarball con
versión propia, las tareas desde una copia fuera de cualquier node_modules, el .npmrc de la plantilla)
y ejecuta la instalación, las skills (instaladas una vez por lista fijada y luego reutilizadas), los
invariantes de las herramientas, la comprobación, las comprobaciones rápidas de navegador, la documentación y una publicación
de prueba (dry-run) (~1,5 a 3 min, sobre todo la red). Un cambio que rompería otro repositorio falla ahí, no
en ese repositorio. Las pruebas se comprueban con tipos junto con la app (tsconfig.json incluye
tests/). El nivel de Google es npm run test -- google (auditorías de Lighthouse, local; CI en cada push y
tag) y npm run test -- cwv (Core Web Vitals en un Worker de Cloudflare desechable); el release
ejecuta ambas.
Dónde se ejecutan las comprobaciones: el comportamiento propio del paquete compartido se comprueba una vez, en remy-auth, antes de cada
release. Una app construida sobre el paquete ejecuta un conjunto de contrato (sus páginas renderizan, las páginas de sitio y de app se mantienen
separadas, sus propias funcionalidades funcionan) más el nivel de Google en sus propias páginas de sitio, no toda la suite del paquete
de nuevo. Mantén las comprobaciones baratas en lugar de quitarlas (el reloj de Playwright en lugar de esperas
reales, una página de navegador por comprobación, workers en paralelo). Las comprobaciones recorren checkedLocales de
@joeblew999/remy-ui/checks, que respeta CHECK_LOCALES. Cada paso indica cuánto tardó.
Traducciones
Una misma estructura en cada app, para que los mismos comandos remy i18n funcionen en todas partes (la regla sobre quién traduce es
un solo redactor):
| Qué | Inglés | Traducción |
|---|---|---|
| Docs | docs/content/<site>/<page>.md o .mdx (estructura de Fumadocs) | a su lado: <page>.<locale>.md o .mdx |
| Catálogos de UI | el catálogo del idioma base de cada proyecto inlang (<dir>/project.inlang) | el catálogo de cada idioma, según el pathPattern propio del proyecto (p. ej. messages/<locale>.json) |
Dos flujos, uno para cada tipo de texto, con el mismo patrón en ambos: detectar sin conexión con git, traducir
con el Claude Code de la máquina (headless, sin herramientas) y hacer commit. Ambos son TypeScript en el paquete
(packages/ui/src/cli/i18n.ts, translate.ts: remy i18n check, remy i18n translate); las tareas de
abajo les reenvían.
- Mensajes de UI (Paraglide): los catálogos, los idiomas y el idioma base vienen del propio
settings.jsonde inlang (I18N_INLANG, o si no, el único proyecto que git conoce). Se detectan: catálogos y claves que faltan (jq),{placeholders}perdidos o inventados (@lingual/i18n-check), claves cuyo inglés cambió desde el último commit del catálogo (git,jq), y las categorías de plural que necesita el idioma (Intl.PluralRules,tasks/i18n/messages/plurals.mjs: ninguna herramienta externa las comprueba). - Docs (Fumadocs): la carpeta es
I18N_DOCS_DIR(por defectodocs/content); cada sitio con uni18n.jsontraduce cada página en inglés a cada uno de sus idiomas. Se detectan: páginas que faltan y desactualizadas (git), huérfanas, y el propio texto de Fumadocs UI (ui/<lang>.jsonfrente aui/en.json). - El traductor es el Claude Code en modo headless de la máquina (
claude -p), sin herramientas, sin servidores MCP, skills ni ajustes de proyecto, y con salida estructurada: las claves de un catálogo vuelven como JSON y se combinan en el orden de claves del inglés; una página vuelve completa, y solo se escribe cuando es una página (page.ts). Los prompts están enpackages/ui/src/cli/prompts.ts. Una llamada por idioma (mensajes) o por página (docs),I18N_JOBSa la vez. - Las comprobaciones no necesitan clave, ni red, pero sí el historial completo de git (un clon superficial se rechaza). Traducir usa el login de Claude de la máquina.
- Una app sin proyecto inlang ni sitios de documentación recibe "nothing to check" y sale con 0.
| Comando | Hace |
|---|---|
remy i18n check | Ambas comprobaciones; un WARNING y salida 0 mientras se programa (la comprobación la ejecuta), salida 1 con I18N_STRICT=1 (el release) |
remy i18n check messages | Los huecos de los catálogos, y los idiomas que difieren de los de la plataforma (de unlisted); --list una línea por hueco (es missing nav_more) |
remy i18n check docs | Los huecos de la documentación; --list una línea por hueco (es stale docs/content/dev/gui.es.md) |
remy i18n translate | En main: mensajes y luego documentación, un commit para cada uno; dev:land lo ejecuta, al final; a mano, con REMY_FLOW=hand delante |
remy i18n translate messages | Los huecos de los catálogos, un idioma por llamada al agente |
remy i18n translate docs [files] | Los huecos de la documentación, una página por llamada al agente; los archivos de traducción indicados se rehacen por completo |
Ajustes: I18N_MODEL (por defecto sonnet), I18N_JOBS (4), I18N_COMMIT=0 (deja el cambio
sin commit para revisarlo), I18N_BRANCH (main).
Cloudflare
npx remy cloudflare <command>; cada uno indica en su comentario de cabecera si es local o remoto (y de producción).
| Comando | Hace |
|---|---|
deploy | Construye con DEPLOY_ORIGIN, sube, espera a la nueva versión (the wait for the new version) y luego ejecuta su comprobación smoke en vivo. Sin pruebas antes: dev:promote lo ejecuta después de las comprobaciones del flujo. Se niega cuando un binding de D1, KV o R2 nombra un recurso que no existe: Wrangler crearía uno durante el despliegue, y crear recursos corresponde al propietario (preview se niega del mismo modo) |
preview [alias] [--keep] | Despliega este commit como un Worker desechable <worker>-check-<commit> (producción intacta), ejecuta el nivel 1 contra él y lo elimina; --keep lo conserva |
preview-delete [name] | Lista los Workers de comprobación, o elimina uno por nombre; nunca el Worker de producción |
urls [origin] | Imprime las páginas y /healthz del origen de producción (o de uno dado), para informes |
staging | deploy para el entorno de staging: construye con CLOUDFLARE_ENV=staging (env.staging en la configuración de Wrangler) y sube su propio Worker en origins.staging; producción queda intacta. Lo que staging permite más allá de producción está en la tabla de entornos de la app |
versions [origins…] | Pregunta a cada despliegue (los orígenes de remy.config.ts, o los orígenes dados) qué está ejecutando: servicio, entorno, commit, cuán lejos está eso de este checkout, cuándo se desplegó. Se pregunta, nunca se recuerda |
docs-deploy | El Worker de la documentación: construye, despliega y publica sus páginas en el índice de Ask AI; dev:promote lo ejecuta |
npm run test -- remote [origin] | El nivel 1 contra un despliegue, por una persona (REMY_FLOW=hand): los despliegues del flujo terminan con el smoke en vivo, y GitHub ejecuta la suite |
npm run observe -- events|ai-usage|ai-check|gateway | Workers Logs almacenados, uso de AI Gateway, comprobación de la configuración de IA, los ajustes del gateway (herramientas) |
npm run upgrade -- <version> | Mueve una app a una misma publicación compartida: la versión del paquete y la tag del flujo de trabajo de CI juntas, y luego las comprobaciones de la instalación |
Las credenciales vienen del login de Wrangler, o del llavero a través de fnox (herramientas).
Planes
La regla de los planes (cómo trabajamos) como comandos (npm run plans -- …), igual en
todos los proyectos: .plans/now.md es la única lista ordenada, .plans/*.md los pocos archivos de plan abiertos,
.plans/done/ y .plans/parked/ el resto, .plans/stability-log.md opcional. Un proyecto sin
.plans/ pasa plans check y no tiene nada que listar.
| Comando | Hace |
|---|---|
status | Los elementos abiertos de now.md por sección, los archivos de plan abiertos con su primera línea Status:/Closed/Parked y su último commit (STALE a los 14 días), los planes aparcados. --json para agentes |
check | now.md existe, cada enlace relativo a un .md bajo .plans/ resuelve, ningún .md fuera de .plans/, done/ y parked/, cada archivo de plan abierto enlazado desde now.md. Instantánea, sin red; la comprobación la ejecuta primero |
close <plan> "<lo publicado>" | Añade Closed <hoy>: ... bajo el título, lo mueve a done/ (desde .plans/ o parked/; git mv si está versionado), reapunta los enlaces relativos hacia él en .plans/, docs/, packages/*/README.md y los *.md de la raíz, y sus propios enlaces desde la carpeta nueva, y tacha su elemento en now.md |
park <plan> "<por qué>" | Lo mismo hacia parked/ con Parked <hoy>: ... |
open <plan> "<por qué ahora>" | Un plan aparcado que se retoma: lo mismo desde parked/ de vuelta a .plans/ con Opened <hoy>: ...; añade su elemento a now.md tú mismo |
close y park cambian archivos y preparan el movimiento, pero nunca hacen commit: lee git diff y luego npm run dev:change.
Inicio de sesión y permisos
Cómo una app de Remy obtiene el inicio de sesión, los roles y los permisos basados en relaciones de remy-auth y su paquete: el guard, el motor de relaciones, el componente Allowed, los entornos, el correo y las comprobaciones que aporta cada uno.
Escribir la documentación
Cómo se organiza y se escribe la documentación de una app de Remy con Fumadocs: carpetas, frontmatter, medios, componentes, traducciones y la CLI.