/** Domaines optionnels. L'éditorial n'en est pas un : c'est le socle. */ declare const OPTIONAL_FEATURES: readonly ["commerce", "auth"]; type OptionalFeature = (typeof OPTIONAL_FEATURES)[number]; declare const PACKAGE_VERSIONS: Record; type HttpMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; interface HttpCall { method: HttpMethod; url: string; body?: unknown; /** Libellé lisible, imprimé en tête de l'appel en dry-run. */ label: string; } interface RequestOptions extends HttpCall { headers?: Record; /** * Valeur rendue en dry-run à la place de la réponse réelle. Sans elle, un * dry-run casserait dès qu'une étape lit l'`uuid` renvoyé par la précédente. */ dryRunResult?: unknown; /** 404 toléré → `undefined` au lieu d'une exception (sondes d'existence). */ allow404?: boolean; /** * Reçoit les en-têtes de la réponse réelle. Seul `x-oauth-scopes` en dépend * — les portées d'un PAT classique ne se lisent nulle part ailleurs. */ captureHeaders?: (headers: Headers) => void; } interface RequesterOptions { dryRun?: boolean; /** Notifié pour CHAQUE appel, émis ou non. Sert au journal et au dry-run. */ onCall?: (call: HttpCall, dryRun: boolean) => void; /** Injection pour les tests ; `globalThis.fetch` par défaut. */ fetchImpl?: typeof fetch; } declare class HttpError extends Error { readonly status: number; readonly url: string; readonly bodyText: string; constructor(status: number, url: string, bodyText: string); } /** * Exécute (ou simule) les appels HTTP JSON. * * En dry-run, AUCUNE requête n'est émise — pas même les GET. C'est délibéré : * un dry-run doit être exécutable sans token valide, donc il ne peut rien * valider côté serveur. C'est le prix d'une simulation qui ne touche rien. */ declare class Requester { private readonly dryRun; private readonly onCall?; private readonly fetchImpl; constructor(opts?: RequesterOptions); get isDryRun(): boolean; request(opts: RequestOptions): Promise; } interface DnsRecordInput { name: string; /** IPv4 du serveur Coolify. */ content: string; proxied: boolean; } declare class CloudflareClient { private readonly zoneId; private readonly token; private readonly http; constructor(zoneId: string, token: string, http: Requester); private call; /** Nom de la zone — sonde de lecture qui valide à la fois le jeton et sa portée. */ zoneName(): Promise; /** * Crée un enregistrement A, ou met à jour celui qui existe déjà. * * Idempotent volontairement : sur un re-run après échec, écraser la cible * d'un record qu'on vient de poser est sans risque, alors qu'échouer * laisserait le provisioning bloqué sur une étape déjà à moitié faite. */ upsertARecord(input: DnsRecordInput): Promise; } /** * Hostname interne d'une base managée Coolify = son UUID. * * Vérifié dans le modèle amont (`StandaloneMariadb::internalDbUrl` → * `mysql://user:pass@{uuid}:3306/db`, idem `StandaloneRedis`). C'est ce qui nous * évite de relire `internal_db_url` : ce champ est dans `$hidden` et n'est * exposé qu'aux tokens `read:sensitive`, alors que l'UUID est renvoyé * directement par le POST de création. `docs/deploy.md` demandait jusqu'ici de * « relever le hostname interne réel » à la main. */ declare function internalHostFor(databaseUuid: string): string; interface CoolifyEnv { key: string; value: string; /** `true` → Coolify n'interprète ni `$` ni `{{ }}` dans la valeur. */ is_literal?: boolean; } interface CreateServiceInput { name: string; projectUuid: string; environmentUuid: string; environmentName: string; serverUuid: string; /** Contenu du compose EN CLAIR — encodé en base64 ici, comme l'exige l'API. */ dockerComposeRaw: string; description?: string; } interface CreateDockerImageAppInput { name: string; projectUuid: string; environmentUuid: string; environmentName: string; serverUuid: string; /** Ex. `ghcr.io/owner/repo/webapp`. */ image: string; imageTag: string; /** Domaines publics, séparés par des virgules, schéma inclus. */ domains: string; /** Port écouté dans le conteneur. */ portsExposes: string; healthCheckPath: string; postDeploymentCommand?: string; description?: string; } declare class CoolifyClient { private readonly token; private readonly http; private readonly base; constructor(baseUrl: string, token: string, http: Requester); private call; /** * Résout un serveur depuis un UUID ou un nom lisible. * * Renvoie aussi son IP, qui est la cible des enregistrements DNS — ça évite * de la demander à l'utilisateur alors que Coolify la connaît déjà. * * En dry-run aucune requête n'est émise, donc la résolution par nom est * impossible : on renvoie l'entrée telle quelle plutôt que d'échouer. */ resolveServer(nameOrUuid: string): Promise<{ uuid: string; ip: string; }>; /** * Crée le projet, en refusant de se greffer sur un homonyme. * * L'API rend un projet existant plutôt que de rejeter le doublon de nom. Sans * ce garde-fou, un run relancé après un échec reprendrait le projet laissé par * le précédent : ses ressources s'empileraient dedans, et la création de * l'environment « production » échouerait en 409 sur celui déjà présent. */ /** UUID du projet portant ce nom, `undefined` s'il n'y en a pas. */ findProject(name: string): Promise; createProject(name: string, description: string): Promise; /** * Rend l'environment `name` du projet, en le créant s'il manque. * * Coolify provisionne un environment « production » à la création du projet : * le poster sans regarder échoue en 409, y compris sur un projet qui vient * d'être créé. On lit donc le projet d'abord. */ resolveEnvironment(projectUuid: string, name: string): Promise; createMariadb(input: { name: string; projectUuid: string; environmentUuid: string; environmentName: string; serverUuid: string; database: string; user: string; password: string; rootPassword: string; }): Promise; createRedis(input: { name: string; projectUuid: string; environmentUuid: string; environmentName: string; serverUuid: string; password: string; }): Promise; /** Ressource « Docker Compose » — le compose est transmis en base64. */ createService(input: CreateServiceInput): Promise; /** Application depuis une image de registry — garde le rolling update natif. */ createDockerImageApplication(input: CreateDockerImageAppInput): Promise; /** * Pose les labels Traefik exacts d'une application, en second appel. * * Pourquoi pas dans le POST de création : Coolify écrase `custom_labels` par * ses labels auto-générés dès que `is_container_label_readonly_enabled` est * vrai — c'est le défaut, et ce réglage n'est PAS exposé par l'API. La même * régénération sur `PATCH` est conditionnée à la présence de `domains` dans * la requête ; un PATCH portant `custom_labels` SEUL passe donc à travers. * D'où cet appel séparé, sans `domains`, après la création. */ setApplicationLabels(applicationUuid: string, labels: string): Promise; setEnvs(kind: "services" | "applications", uuid: string, envs: CoolifyEnv[]): Promise; deploy(uuids: string[]): Promise; } /** * Labels Traefik de la webapp — source de vérité unique. * * Ils étaient jusqu'ici collés à la main dans l'UI Coolify depuis * `docs/traefik-labels.md` ; ce doc est désormais le miroir de cette constante. * `slug` et `appHost` sont substitués ici, donc les labels émis ne contiennent * plus aucune variable à interpoler. * * `priority=1` maintient ce routeur SOUS le routeur `wp-seo` du compose * WordPress, qui sert `/robots.txt` et les sitemaps sur ce même host. */ declare function webappTraefikLabels(slug: string, appHost: string): string; interface RepoRef { owner: string; repo: string; /** Scope les secrets et variables à cet environment plutôt qu'au repo. */ environment?: string; } /** * Chiffre une valeur pour un secret Actions (sealed box libsodium). * * Node n'expose pas `crypto_box_seal` : la primitive combine X25519, blake2b * (dérivation du nonce) et XSalsa20-Poly1305, et OpenSSL ne fournit pas ce * dernier. D'où la dépendance — réimplémenter du scellage crypto à la main * serait un mauvais échange. */ declare function sealSecret(publicKeyBase64: string, value: string): Promise; declare class GitHubClient { private readonly token; private readonly http; constructor(token: string, http: Requester); private call; /** Login du compte porteur du token — sert à détecter owner personnel vs organisation. */ viewerLogin(): Promise; /** * Portées d'un PAT classique, lues dans `x-oauth-scopes`. * * Rend `undefined` pour un jeton fine-grained : GitHub n'expose pas ses * permissions par en-tête, elles ne sont donc pas vérifiables à l'avance. */ tokenScopes(): Promise; repoExists(owner: string, repo: string): Promise; /** * Crée le repo. `POST /user/repos` pour un compte personnel, * `POST /orgs/{org}/repos` sinon — l'API n'a pas d'endpoint unique. */ createRepo(input: { owner: string; repo: string; description: string; private: boolean; isOrg: boolean; }): Promise<{ cloneUrl: string; defaultBranch: string; }>; /** * Racine des secrets et variables Actions. * * Sans `environment`, ils vivent au niveau du repo et servent tous les jobs. * Avec, ils sont scopés à cet environment et l'emportent sur ceux du repo pour * les jobs qui le ciblent — c'est ce qui permet à staging d'avoir ses propres * UUID Coolify sans jamais toucher à ceux de production. Noter que le segment * `/actions` disparaît sur le chemin des environments : l'API ne l'utilise pas. */ private base; /** Crée l'environment, ou le laisse tel quel — le PUT est idempotent côté API. */ ensureEnvironment(ref: RepoRef, name: string): Promise; setSecret(ref: RepoRef, name: string, value: string): Promise; setVariable(ref: RepoRef, name: string, value: string): Promise; } /** * Premier push du repo client. * * C'est cette étape qui déclenche toute la suite : la CI build les deux images, * les pousse sur GHCR, puis appelle elle-même `/api/v1/deploy` (le `curl` déjà * présent dans `build-and-deploy-image.yml`). D'où l'ordre du provisioning — * les ressources Coolify et les secrets `COOLIFY_*_UUID` doivent exister AVANT * ce push, sinon le premier run ne déploie rien. * * Le token voyage par `http.extraHeader`, jamais dans l'URL du remote : une * URL `https://x-access-token:@github.com/…` finirait en clair dans * `.git/config`, sur disque, dans le repo qu'on vient de livrer au client. * * Et ce header est posé par l'ENVIRONNEMENT (`GIT_CONFIG_*`, git ≥ 2.31), pas * par `-c` : tout ce qui passe en argv est lisible dans `ps`, et surtout * `execFileSync` recopie la ligne de commande entière dans le `Command failed:` * qu'il lève — un push refusé imprimait donc le token en clair au terminal. */ declare function pushInitialCommit(input: { dir: string; cloneUrl: string; branch: string; token: string; message: string; /** Faux → le dépôt existe déjà : on ne fait que publier sa branche. */ bootstrap: boolean; dryRun: boolean; log: (line: string) => void; }): void; interface ProvisionOptions { projectName: string; targetDir: string; namespace?: string; /** Domaine du front, ex. `acme.example.com`. WordPress ira sur `wp.`. */ domain: string; /** Propriétaire GitHub du repo client (compte perso ou organisation). */ githubOwner: string; /** Nom du repo. Défaut : le slug du projet. */ repoName?: string; /** Serveur Coolify : UUID ou nom affiché. */ coolifyServer: string; /** * Cible IPv4 des enregistrements DNS. Défaut : l'IP remontée par Coolify. * * Le serveur d'une instance auto-hébergée — celui que Coolify nomme * `localhost` — rapporte `host.docker.internal`, qui décrit la façon dont * Coolify le joint, pas la façon dont Internet le joint. */ serverIp?: string; /** * Environnement provisionné. Décide de la branche poussée, de l'environment * Coolify, du préfixe des ressources et de l'environment GitHub où * atterrissent les valeurs qui lui sont propres. */ targetEnv?: "production" | "staging"; /** * Premier run du projet, quel que soit son environnement : lui seul * scaffolde, crée le repo GitHub et le projet Coolify. Les suivants s'y * greffent et n'ajoutent que leur environnement. * * Le mode est explicite plutôt que déduit de l'état distant : c'est ce qui * permet aux garde-fous de rester utiles dans les deux sens — amorcer sur un * projet existant est une erreur, se greffer sur du vide aussi. */ bootstrap?: boolean; /** Host WordPress. Défaut : `wp.`. */ wpHost?: string; dryRun?: boolean; /** Tokens — lus depuis l'environnement par la CLI, jamais depuis argv. */ tokens: ProvisionTokens; log?: (line: string) => void; fetchImpl?: typeof fetch; } interface ProvisionTokens { github: string; coolifyUrl: string; coolifyToken: string; /** PAT `read:packages` utilisé par la CI pour installer les `@wp-reactor/*`. */ nodeAuthToken: string; cloudflareToken?: string; cloudflareZoneId?: string; } interface ProvisionResult { slug: string; targetDir: string; appHost: string; wpHost: string; repoUrl: string; actionsUrl: string; /** * Mot de passe du compte admin WordPress. Il n'est stocké nulle part ailleurs * que dans l'env Coolify : c'est le seul endroit d'où l'opérateur peut le * récupérer, d'où sa présence dans le résultat. */ wpAdminPassword: string; coolify: { projectUuid: string; environmentUuid: string; mariadbUuid: string; redisUuid: string; wordpressUuid: string; webappUuid: string; }; /** Journal des appels HTTP, dans l'ordre. Alimente le `--dry-run`. */ calls: HttpCall[]; } /** * Contrôles faisables sans réseau, avant toute création. * * Le provisioning n'est pas transactionnel : échouer à mi-parcours laisse un * projet Coolify et un repo GitHub orphelins. Tout ce qui peut être refusé * gratuitement doit l'être ici. */ declare function preflight(opts: ProvisionOptions): string[]; declare function provision(opts: ProvisionOptions): Promise; /** Récapitulatif de fin de course, affiché par la CLI. */ declare function summary(result: ProvisionResult): string; interface ConfigKey { key: string; /** Requis pour lancer le provisioning. */ required: boolean; /** Commentaire imprimé au-dessus de la clé dans le template. */ comment: string; /** Valeur d'exemple, en commentaire — jamais pré-remplie. */ example?: string; /** Regroupement dans le template. */ section: string; } /** * Source de vérité unique : le template généré, la validation et le * récapitulatif en dérivent tous. Ajouter une clé ici suffit à la faire * apparaître partout — c'est ce qui les empêche de diverger. * * Les JETONS gardent leur nom conventionnel, sans préfixe : un * `export GITHUB_TOKEN=…` déjà présent dans le shell doit fonctionner tel quel. * Les PARAMÈTRES sont préfixés `WPR_` pour ne pas entrer en collision avec des * noms génériques — `DOMAIN` traîne dans beaucoup d'environnements. */ declare const CONFIG_KEYS: ConfigKey[]; /** * Parseur `.env` minimal, rendant un objet — il ne touche PAS à `process.env`. * * C'est délibéré et load-bearing : `process.loadEnvFile` n'écrase pas ce qui est * déjà dans l'environnement, donc au second passage de la boucle d'attente il ne * verrait aucune des valeurs que l'utilisateur vient d'écrire (le premier passage * les a déjà posées, vides). Un objet neuf à chaque relecture est la seule façon * de faire fonctionner la boucle. */ declare function parseEnvText(text: string): Record; /** Rend le fichier de configuration commenté, toutes valeurs vides. */ declare function renderConfigTemplate(): string; /** Clés requises absentes ou vides, dans l'ordre du descripteur. */ declare function missingKeys(values: Record): string[]; /** * Incohérences qui ne se voient pas clé par clé. * * `preflight()` (plan.ts) reste le filet final ; ici on attrape ce qu'on peut * expliquer en termes de CLÉS du fichier, pendant que l'utilisateur l'édite. */ declare function configWarnings(values: Record): string[]; interface ResolveInput { /** Valeurs lues dans le fichier de configuration. */ fileValues: Record; /** `process.env`. */ env: Record; /** Valeurs venues de la ligne de commande (undefined si absentes). */ flags: { projectName?: string; dir?: string; namespace?: string; domain?: string; ghOwner?: string; coolifyServer?: string; serverIp?: string; targetEnv?: string; bootstrap?: boolean; repoName?: string; wpHost?: string; dryRun?: boolean; }; } /** * Applique la précédence **flag > shell > fichier**. * * Le shell l'emporte sur le fichier (convention dotenv) pour permettre une * surcharge ponctuelle sans éditer le fichier ; le flag l'emporte sur tout, car * c'est le plus explicite des trois. */ declare function resolveProvisionOptions(input: ResolveInput): Omit; /** * Récapitulatif affiché avant confirmation. * * Aucune valeur de jeton n'y figure — seulement présent/absent. C'est le dernier * point d'arrêt avant des appels non idempotents et sans rollback. */ declare function renderSummary(opts: Omit, slug: string): string; /** * Plancher vérifié à l'exécution par la coque client * (`apps/webapp/src/server/session.ts`), qui refuse de démarrer en dessous. * On génère largement au-dessus ; la constante documente la contrainte. */ declare const SESSION_SECRET_MIN_LENGTH = 32; interface GeneratedSecrets { /** Scellage des cookies de session de la webapp. */ sessionSecret: string; /** Mot de passe applicatif MariaDB (`WORDPRESS_DB_PASSWORD`). */ dbPassword: string; /** Mot de passe root MariaDB (`MYSQL_ROOT_PASSWORD`). */ dbRootPassword: string; /** `WP_REDIS_PASSWORD`. */ redisPassword: string; /** Mot de passe du compte admin WordPress créé par le bootstrap. */ wpAdminPassword: string; /** `CACHE_PURGE_SECRET` — partagé webapp ↔ WordPress pour le webhook de purge. */ purgeSecret: string; } /** Génère l'intégralité des secrets d'une infra. Chaque appel produit des valeurs neuves. */ declare function generateSecrets(): GeneratedSecrets; /** * Remplace toute occurrence d'un secret par `***` dans un texte destiné au * terminal. Le `--dry-run` imprime des payloads complets : sans ce filtre, il * recracherait les mots de passe de prod dans l'historique du shell. * * Le tri par longueur décroissante évite qu'un secret court, contenu dans un * secret long, ne le masque partiellement et ne laisse fuiter le reste. */ declare function redact(text: string, values: readonly string[]): string; declare const TEMPLATES_DIR: string; declare const INFRA_PATHS: string[]; interface ScaffoldOptions { /** * Nom du projet (ex. "Client 2") → slugifié en `client-2` pour les noms de * packages, dossiers, thème, projet Docker Compose et labels Traefik. */ projectName: string; /** Dossier cible (créé). */ targetDir: string; /** * Domaines optionnels activés (« commerce », « auth »). L'éditorial est le * socle et n'en fait pas partie. Omis = tous actifs : le template complet * reste la variante par défaut, et la sandbox de dogfood la construit. */ features?: readonly string[]; /** Namespace des blocs gen-block. Défaut : projectName sans tirets. */ namespace?: string; /** Override du dossier template (tests). */ templatesDir?: string; /** * Override appliqué à TOUTES les deps `@wp-reactor/*` (placeholder * `__PKG_RANGE__`). Par défaut, chaque dep est résolue vers `^` du * package via le manifeste embarqué (`versions.generated.ts`). Le scaffold * dogfood passe `workspace:*` ; `$WP_REACTOR_PKG_RANGE` sert d'override global. */ pkgRange?: string; } interface UpdateOptions { /** Racine du repo client existant. */ targetDir: string; /** Override du dossier template (tests). */ templatesDir?: string; } interface ScaffoldResult { targetDir: string; written: string[]; } /** Une range `@wp-reactor/*` réécrite par `update` dans un package.json client. */ interface DepBump { file: string; dep: string; from: string; to: string; } interface UpdateResult extends ScaffoldResult { /** Ranges réécrites vers la version embarquée dans ce CLI. */ bumps: DepBump[]; /** * Ranges laissées en place parce qu'elles sont DEVANT le manifeste embarqué. * Le manifeste est figé au build de create-wp-reactor : un package publié * après lui y est encore à sa version précédente, et l'écrire reculerait le * client. */ kept: DepBump[]; /** Deps `@wp-reactor/*` absentes du manifeste embarqué — laissées telles quelles. */ unknownDeps: string[]; } /** * Slug injecté dans `__PROJECT_NAME__`. Le token atterrit dans des identifiants * qui n'acceptent PAS d'espaces/majuscules/accents — `name:` du Docker Compose * (`[a-z0-9][a-z0-9_-]*`), noms de packages npm, dossier du thème enfant, labels * Traefik — donc on slugifie au lieu de recopier l'argument CLI tel quel. */ declare function toProjectSlug(input: string): string; declare function scaffold(opts: ScaffoldOptions): ScaffoldResult; /** * Rafraîchit un repo client existant : re-pousse les fichiers d'INFRA depuis le * template embarqué, puis bumpe les deps `@wp-reactor/*` vers les versions * embarquées dans ce CLI (d'où `create-wp-reactor@latest` pour les dernières). */ declare function update(opts: UpdateOptions): UpdateResult; export { CONFIG_KEYS, CloudflareClient, type ConfigKey, CoolifyClient, type DepBump, type GeneratedSecrets, GitHubClient, type HttpCall, HttpError, INFRA_PATHS, OPTIONAL_FEATURES, type OptionalFeature, PACKAGE_VERSIONS, type ProvisionOptions, type ProvisionResult, type ProvisionTokens, Requester, type ResolveInput, SESSION_SECRET_MIN_LENGTH, type ScaffoldOptions, type ScaffoldResult, TEMPLATES_DIR, type UpdateOptions, type UpdateResult, configWarnings, generateSecrets, internalHostFor, missingKeys, parseEnvText, preflight, provision, pushInitialCommit, redact, renderConfigTemplate, renderSummary, resolveProvisionOptions, scaffold, sealSecret, summary, toProjectSlug, update, webappTraefikLabels };