@timekast/factory 1.28.1 → 2.0.1
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/dist/commands/backlog.js +52 -15
- package/dist/commands/doctor.js +210 -29
- package/dist/commands/env.js +154 -18
- package/dist/commands/invite-admin.js +62 -15
- package/dist/commands/provision.js +5815 -763
- package/dist/commands/status.js +12 -2
- package/dist/commands/ticket.js +4 -4
- package/dist/commands/update.js +97 -12
- package/dist/commands/vault.js +2627 -0
- package/dist/index.js +97 -33
- package/dist/lib/backlog-mapping.js +111 -18
- package/dist/lib/backlog-parse.js +1 -1
- package/dist/lib/claude-md.js +320 -0
- package/dist/lib/cloudflare-api.js +213 -37
- package/dist/lib/constants.js +70 -6
- package/dist/lib/env-file.js +45 -7
- package/dist/lib/env-wizard.js +66 -2
- package/dist/lib/initial-migration.js +3 -2
- package/dist/lib/neon-api.js +145 -10
- package/dist/lib/package-json.js +37 -16
- package/dist/lib/preflight.js +92 -38
- package/dist/lib/provision-preflight.js +158 -100
- package/dist/lib/provision-state.js +652 -20
- package/dist/lib/publish-engine.js +1 -1
- package/dist/lib/rail.js +400 -0
- package/dist/lib/railway-api.js +851 -0
- package/dist/lib/vault-adopt.js +1495 -0
- package/dist/lib/vault-api.js +1106 -0
- package/dist/lib/vault-config.js +21 -0
- package/dist/lib/vault-deploys.js +898 -0
- package/dist/lib/vercel-env.js +251 -31
- package/dist/lib/wrapper-wiring.js +160 -0
- package/dist/tui/env-wizard-flow.js +17 -3
- package/dist/tui/menu.js +4 -22
- package/dist/tui/provision-flow.js +68 -13
- package/package.json +1 -1
- package/dist/commands/secrets.js +0 -71
- package/dist/lib/env-merge.js +0 -238
package/dist/index.js
CHANGED
|
@@ -20,12 +20,14 @@ import { parseDoctorFlags, runDoctor, runLauncherDoctor } from './commands/docto
|
|
|
20
20
|
import { parseNewFlags, runNew } from './commands/new.js';
|
|
21
21
|
import { parseProvisionFlags, runProvision } from './commands/provision.js';
|
|
22
22
|
import { parsePublishArgs, runPublish } from './commands/publish.js';
|
|
23
|
-
import { secretsCommand } from './commands/secrets.js';
|
|
24
23
|
import { parseStatusFlags, runStatus } from './commands/status.js';
|
|
25
24
|
import { runTicketCommand } from './commands/ticket.js';
|
|
26
25
|
import { parseUnpublishTargets, runUnpublish } from './commands/unpublish.js';
|
|
27
26
|
import { maybeOfferAdopt, parseUpdateFlags, runUpdate } from './commands/update.js';
|
|
27
|
+
import { runVaultCommand } from './commands/vault.js';
|
|
28
|
+
import { loginCommand } from './lib/rail.js';
|
|
28
29
|
import { detectRepo } from './lib/repo-detection.js';
|
|
30
|
+
import { VAULT_CONFIG } from './lib/vault-config.js';
|
|
29
31
|
import { runTuiMenu } from './tui/index.js';
|
|
30
32
|
const HELP = `
|
|
31
33
|
@timekast/factory — bootstrap y mantenimiento de proyectos derivados del Factory.
|
|
@@ -43,12 +45,19 @@ Comandos:
|
|
|
43
45
|
(--launcher: verifica el entorno — Node ≥ 22, gh, gh auth, membresía org)
|
|
44
46
|
publish <qué> Publica el entregable (proposal|mockup|both) a proposals.timekast.mx
|
|
45
47
|
unpublish [qué] Baja del hub un entregable publicado (proposal|mockup|both; default both)
|
|
46
|
-
provision Aprovisiona la infra de un derivado (Neon
|
|
48
|
+
provision Aprovisiona la infra de un derivado (Neon, GitHub, DNS, backlog y su destino de
|
|
49
|
+
despliegue: Vercel o Railway) — flag-driven, --json
|
|
47
50
|
backlog <sub> push: refleja el backlog local en backlog.timekast.mx · sync: status + self-healing
|
|
48
51
|
ticket <sub> push: entrega un factory-ticket de project/factory/ al repo del Factory
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
env <sub> push: sube product vars a Vercel (day-2) · wizard: configura las variables
|
|
53
|
+
(en la bóveda si el repo la tiene; sin bóveda, en .env.local)
|
|
51
54
|
invite-admin Crea una invitación de super_admin para un derivado (sin password en texto plano)
|
|
55
|
+
vault <sub> adopt: pasa a la bóveda un repo que nació antes de ella (sin --apply sólo
|
|
56
|
+
enseña el plan, por nombre y huella — nunca valores; con varios proyectos
|
|
57
|
+
de Vercel, --vercel-project <id> elige el principal) · sync railway: crea
|
|
58
|
+
el sync de un entorno de la bóveda a un servicio de Railway · deploys: siembra el
|
|
59
|
+
proyecto de cada despliegue de un repo multi-cliente desde la plantilla
|
|
60
|
+
(--apply, --yes para confirmar sin terminal los valores que cambia)
|
|
52
61
|
|
|
53
62
|
Sobre \`new\`:
|
|
54
63
|
Pregunta el perfil de forma interactiva. Los flags \`--full\` / \`--core\` lo
|
|
@@ -85,28 +94,50 @@ Sobre \`update\`:
|
|
|
85
94
|
Sobre \`provision\`:
|
|
86
95
|
Aprovisiona la infra de un proyecto derivado. Flag-driven (sin prompts) y
|
|
87
96
|
headless-safe; la interacción la pone la skill \`tk-provision\`. Preflight
|
|
88
|
-
fail-closed (gh + tokens del gate + \`VERCEL_TOKEN\` con
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
fail-closed (gh + tokens del gate + — con destino Vercel — \`VERCEL_TOKEN\` con
|
|
98
|
+
acceso al team);
|
|
99
|
+
\`neonctl\` es opcional (ausente → warning). Por default crea primero el proyecto
|
|
100
|
+
del repo en la bóveda (\`vault\`) y cada paso escribe ahí lo que acuña — sin
|
|
101
|
+
\`.env.local\`. No se ofrece en perfil \`core\`, ni se agrega sola a un repo ya
|
|
102
|
+
aprovisionado sin bóveda (para eso: \`factory vault adopt\`). Flags:
|
|
103
|
+
--services=a,b Servicios a crear (vault,neon,github,vercel,railway,dns,backlog). \`backlog\`
|
|
104
|
+
registra el proyecto en backlog.timekast.mx y guarda
|
|
105
|
+
BACKLOG_API_KEY + BACKLOG_PROJECT_ID en el entorno \`local\` de la
|
|
106
|
+
bóveda (sin bóveda: en .env.local) (requiere BACKLOG_ADMIN_TOKEN en
|
|
107
|
+
el rail; ausente en un bundle → se omite)
|
|
108
|
+
--target=<dest> Destino de despliegue del repo: vercel | railway. OBLIGATORIO en un
|
|
109
|
+
alta nueva con paso de despliegue (github, vercel, railway, dns o
|
|
110
|
+
--domain); neon/backlog solos no lo piden. Queda guardado en
|
|
111
|
+
.timekast/provision.json. Un repo sin destino guardado lo descubre
|
|
112
|
+
preguntando a Railway y a Vercel por el repo conectado (nunca por
|
|
113
|
+
vercel.json ni otro archivo). Cambiar un destino guardado: sólo
|
|
114
|
+
--adopt --force --target=<otro>. Con Railway, --adopt lo encuentra
|
|
115
|
+
por el repo conectado y --destroy borra con RAILWAY_TOKEN del rail
|
|
116
|
+
--resolve-target Sólo resuelve el destino y lo guarda en .timekast/provision.json; no
|
|
117
|
+
crea nada más. Salida: 0 = resuelto (guardado o ya estaba) ·
|
|
118
|
+
10 = sin destino (el repo no está conectado en ninguna) ·
|
|
119
|
+
11 = ambiguo (conectado en las dos, o project-config.md declara un
|
|
120
|
+
rail de cliente) · 12 = no se pudo leer un token del rail
|
|
121
|
+
--no-vault Omite la bóveda: ninguna llamada a ella, y todo como antes
|
|
122
|
+
(.env.local + escritura directa a los despliegues)
|
|
94
123
|
--slug=<slug> Identificador del stack (nombre del proyecto + dominios interinos)
|
|
95
|
-
--team=<id> Team de Vercel (override; default = \`VERCEL_TEAM_ID\`
|
|
124
|
+
--team=<id> Team de Vercel (override; default = \`VERCEL_TEAM_ID\` de rail-timekast en la bóveda)
|
|
96
125
|
--org-id=<id> Org de GitHub (override; default = org del Factory)
|
|
97
126
|
--neon-org=<x> Organización de Neon donde CREAR el proyecto: nombre o id.
|
|
98
127
|
Default = \`NEON_ORG\` del rail; si la key ve una sola org, esa.
|
|
99
128
|
Varias orgs y sin selector → aborta (nunca adivina). Un proyecto
|
|
100
129
|
que YA existe ignora este flag: su org se lee del proyecto
|
|
101
130
|
--remint Reacuña la API key project-scoped de Neon aunque el paso esté
|
|
102
|
-
completo (revoca la vigente, sube los 3 secrets de E2E y
|
|
103
|
-
reescribe .env.local).
|
|
131
|
+
completo (revoca la vigente, sube los 3 secrets de E2E y la
|
|
132
|
+
reescribe en develop:/ci de la bóveda — sin bóveda, en .env.local).
|
|
133
|
+
Para una key revocada por fuera
|
|
104
134
|
--domain=<dom> Dominio definitivo del cliente (diferible; standalone o inicial)
|
|
105
135
|
--dns=<ruta> Ruta DNS para --domain: org (default) | cliente-cf | registrar
|
|
106
136
|
--verify-timeout=<min> Timeout del sondeo de verificación de Vercel (default 10)
|
|
107
137
|
--confirm Pre-aprueba retirar el dominio interino tras verificar (headless-safe)
|
|
108
138
|
--ephemeral Marca el stack \`ephemeral\` (vs \`live\`) para \`--destroy\`
|
|
109
|
-
--dry-run Imprime el plan sin tocar ninguna API
|
|
139
|
+
--dry-run Imprime el plan sin tocar ninguna API de plataforma (Railway, Vercel,
|
|
140
|
+
Neon, Cloudflare) ni escribir estado; sin destino resuelto lo dice
|
|
110
141
|
--resume Retoma desde \`.timekast/provision.json\`, salta pasos hechos
|
|
111
142
|
--adopt Reconstruye \`.timekast/provision.json\` de un derivado existente
|
|
112
143
|
consultando los providers EN SOLO-LECTURA (nunca crea/borra)
|
|
@@ -128,16 +159,23 @@ Sobre \`status\` y \`doctor\`:
|
|
|
128
159
|
|
|
129
160
|
Sobre \`env\`:
|
|
130
161
|
\`env push\` sube las product vars de \`.env.local\` a Vercel (production+preview),
|
|
131
|
-
sin tocar las provision-owned.
|
|
162
|
+
sin tocar las provision-owned. En un repo con bóveda (bloque \`vault\`) no escribe:
|
|
163
|
+
lo dice, nombra el proyecto de la bóveda y termina sin error. \`env wizard\` configura
|
|
164
|
+
las variables — en un repo con bóveda las escribe ahí (una copia en main:/, referenciada desde develop:/ y local); sin
|
|
165
|
+
bóveda, en \`.env.local\`:
|
|
132
166
|
(sin flags) Wizard interactivo en la terminal (deriva las preguntas de \`.env.example\`)
|
|
133
167
|
--plan [--json] Emite el plan de preguntas (lo consume el agente / la skill tk-provision)
|
|
134
|
-
--apply [--push] Lee \`{ answers, push? }\` JSON por stdin y escribe
|
|
168
|
+
--apply [--push] Lee \`{ answers, push? }\` JSON por stdin y las escribe (con bóveda,
|
|
169
|
+
\`--push\` no aplica: los despliegues las reciben de la bóveda)
|
|
170
|
+
--review [--json] Resumen enmascarado (con bóveda: del entorno \`local\`)
|
|
171
|
+
--gen-vapid Genera las llaves VAPID (con bóveda, se guardan ahí; la privada no se imprime)
|
|
135
172
|
|
|
136
173
|
Sobre \`backlog\`:
|
|
137
174
|
\`backlog push\` refleja el backlog local (\`project/backlog/*\`) en
|
|
138
175
|
backlog.timekast.mx en cascada (épicas → features de los Refs \`FT-XX\` → issues
|
|
139
176
|
story/task según el campo \`Board:\`). Read-only por defecto: sin \`BACKLOG_API_KEY\`
|
|
140
|
-
en \`.env.local\` termina con exit 0 y una nota (nunca
|
|
177
|
+
(en un repo sin bóveda, la de \`.env.local\`) termina con exit 0 y una nota (nunca
|
|
178
|
+
bloquea \`/backlog\`). Flags:
|
|
141
179
|
--reconcile PATCH del status cuando el elemento ya existe (en vez de saltarlo)
|
|
142
180
|
--stamp Incluye issues/épicas sin UUID y estampa su \`Backlog UUID\` de vuelta
|
|
143
181
|
--backfill Campaña legacy: incluye + estampa un backlog pre-BSYNC-007 completo
|
|
@@ -166,10 +204,12 @@ Sobre \`ticket\`:
|
|
|
166
204
|
|
|
167
205
|
Sobre \`invite-admin\`:
|
|
168
206
|
Materializa el primer super_admin de un derivado por invitación (reemplaza el
|
|
169
|
-
viejo seed con password en texto plano).
|
|
170
|
-
target desde \`.env.local\` y spawnea el script del kit con ella:
|
|
207
|
+
viejo seed con password en texto plano). En un repo sin bóveda resuelve la
|
|
208
|
+
connection string del target desde \`.env.local\` y spawnea el script del kit con ella:
|
|
209
|
+
En un repo con bóveda lee el entorno del target directo de la bóveda, con tu sesión
|
|
210
|
+
(main → main:/ + main:/ci; develop → local) y no toca \`.env.local\`.
|
|
171
211
|
--email=<addr> Email que recibe la invitación y será super_admin (requerido)
|
|
172
|
-
--target=<t> develop (default, usa DATABASE_URL) | main (
|
|
212
|
+
--target=<t> develop (default, usa DATABASE_URL) | main (sin bóveda: DATABASE_URL_MAIN)
|
|
173
213
|
--app-url=<url> Override del NEXT_PUBLIC_APP_URL del link de aceptación
|
|
174
214
|
(úsalo con --target main para que apunte a producción)
|
|
175
215
|
Alias en el derivado: \`pnpm invite:admin --email=...\`.
|
|
@@ -191,6 +231,24 @@ function readVersion() {
|
|
|
191
231
|
return '0.0.0';
|
|
192
232
|
}
|
|
193
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* The tombstone of `factory secrets` (RETIRED — the rail lives in the vault; nothing reads a
|
|
236
|
+
* local rail file anymore). Kept instead of "unknown command" because installed desktop
|
|
237
|
+
* launchers still run `factory secrets <path>` and paint success on exit 0. Contract:
|
|
238
|
+
* · every shape (`secrets`, `secrets <path>`, `secrets pull|sync`, `secrets --help`) gets it;
|
|
239
|
+
* · non-zero exit (CLIError), so an old launcher card renders it as an error;
|
|
240
|
+
* · the LAST line is the actionable instruction — an old card shows only that line;
|
|
241
|
+
* · the arguments are never read: nothing is opened, validated or echoed (a path can name a
|
|
242
|
+
* person or a client).
|
|
243
|
+
*/
|
|
244
|
+
function secretsRetiredError() {
|
|
245
|
+
const { rail } = VAULT_CONFIG;
|
|
246
|
+
return new CLIError([
|
|
247
|
+
`\`factory secrets\` se retiró: el rail de secretos ya no se importa a ~/.claude/.env, vive en la bóveda (proyecto ${rail.name}) y los comandos lo leen de ahí.`,
|
|
248
|
+
'No se leyó ni se importó ningún archivo.',
|
|
249
|
+
`Para entrar: corre \`${loginCommand()}\` y pide a un admin acceso a ${rail.name}.`,
|
|
250
|
+
].join('\n'));
|
|
251
|
+
}
|
|
194
252
|
async function main(argv) {
|
|
195
253
|
const [command, ...rest] = argv;
|
|
196
254
|
// Explicit help flags always print the static HELP — never the TUI.
|
|
@@ -198,6 +256,11 @@ async function main(argv) {
|
|
|
198
256
|
console.log(HELP);
|
|
199
257
|
return;
|
|
200
258
|
}
|
|
259
|
+
// `factory secrets` is retired: EVERY shape answers with the tombstone — `--help`/`-h`
|
|
260
|
+
// included, which otherwise the help gate below would turn into an exit 0 that an old
|
|
261
|
+
// launcher paints as success. Before that gate on purpose.
|
|
262
|
+
if (command === 'secrets')
|
|
263
|
+
throw secretsRetiredError();
|
|
201
264
|
// A `--help`/`-h` flag ANYWHERE (e.g. `factory env push --help`) prints help and
|
|
202
265
|
// NEVER runs the command. A help flag must never trigger a mutation: `env push --help`
|
|
203
266
|
// used to fire a REAL Vercel push, because the check above only inspects the first arg.
|
|
@@ -259,7 +322,9 @@ async function main(argv) {
|
|
|
259
322
|
await runLauncherDoctor({}, flags.json);
|
|
260
323
|
}
|
|
261
324
|
else {
|
|
262
|
-
|
|
325
|
+
// Awaited: in `bóveda` mode the report waits for the bounded drift step (ADOPT-004), and
|
|
326
|
+
// `main` exits the process as soon as it resolves.
|
|
327
|
+
await runDoctor();
|
|
263
328
|
}
|
|
264
329
|
return;
|
|
265
330
|
}
|
|
@@ -300,16 +365,9 @@ async function main(argv) {
|
|
|
300
365
|
return;
|
|
301
366
|
}
|
|
302
367
|
case 'secrets': {
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
const file = rest.filter((a) => !a.startsWith('-'))[0];
|
|
307
|
-
if (!file) {
|
|
308
|
-
throw new CLIError('Falta el archivo de secretos. Uso: `factory secrets <file>` ' +
|
|
309
|
-
'(un archivo `KEY=value`, p.ej. el master `secrets.env`) que se mergea a `~/.claude/.env`.');
|
|
310
|
-
}
|
|
311
|
-
await secretsCommand(file);
|
|
312
|
-
return;
|
|
368
|
+
// Unreachable today (the tombstone fires before the help gate). Kept so that reordering
|
|
369
|
+
// the gates above can never turn `secrets` into "unknown command".
|
|
370
|
+
throw secretsRetiredError();
|
|
313
371
|
}
|
|
314
372
|
case 'env': {
|
|
315
373
|
const sub = rest[0];
|
|
@@ -329,11 +387,11 @@ async function main(argv) {
|
|
|
329
387
|
return;
|
|
330
388
|
}
|
|
331
389
|
if (wflags.review) {
|
|
332
|
-
runEnvWizardReview(wflags);
|
|
390
|
+
await runEnvWizardReview(wflags);
|
|
333
391
|
return;
|
|
334
392
|
}
|
|
335
393
|
if (wflags.genVapid) {
|
|
336
|
-
runEnvWizardGenVapid();
|
|
394
|
+
await runEnvWizardGenVapid();
|
|
337
395
|
return;
|
|
338
396
|
}
|
|
339
397
|
if (wflags.apply) {
|
|
@@ -346,6 +404,12 @@ async function main(argv) {
|
|
|
346
404
|
throw new CLIError(`Subcomando de env desconocido: \`${sub ?? '(ninguno)'}\`. ` +
|
|
347
405
|
'Uso: `factory env push` o `factory env wizard`.');
|
|
348
406
|
}
|
|
407
|
+
case 'vault': {
|
|
408
|
+
// `factory vault adopt [--apply]` (ADOPT-001) — move a repo born before the vault into it.
|
|
409
|
+
// Without a subcommand it prints the vault help.
|
|
410
|
+
await runVaultCommand(rest);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
349
413
|
case 'invite-admin': {
|
|
350
414
|
// Mint a single-use super_admin invite against the target branch's DB.
|
|
351
415
|
// Spawns the kit script `scripts/tools/invite-admin.ts` with the resolved
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
* remotely, keeping the 6↔6 bijection intact.
|
|
19
19
|
*/
|
|
20
20
|
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
+
import { readVaultLink, UNREADABLE_STATE_REMEDY, VaultLinkMalformedError, } from './provision-state.js';
|
|
22
|
+
import { INFISICAL_INSTALL_HINT, RailNoSessionError, RailNotInstalledError, loginCommand, } from './rail.js';
|
|
23
|
+
import { VAULT_ROOT_PATH, VaultApiError, VaultNoAccessError, createVaultClient, } from './vault-api.js';
|
|
21
24
|
/**
|
|
22
25
|
* LOCAL bucket → REMOTE `WorkStatus`. Half of the bijection; {@link STATUS_REMOTE_TO_LOCAL}
|
|
23
26
|
* is its exact inverse. The 6 remote states were live-verified (smoke Hallazgo 3).
|
|
@@ -106,9 +109,9 @@ export function mapStoryPoints(sp) {
|
|
|
106
109
|
return Number.isInteger(sp) && sp > 0 ? sp : null;
|
|
107
110
|
}
|
|
108
111
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
109
|
-
// Credential resolution — fail-closed, NEVER throws (issue §4)
|
|
112
|
+
// Credential resolution — fail-closed, NEVER throws (issue §4 · VAULT-006)
|
|
110
113
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
-
/** The env keys
|
|
114
|
+
/** The env keys that carry the project-scoped backlog credentials. */
|
|
112
115
|
export const BACKLOG_API_KEY_VAR = 'BACKLOG_API_KEY';
|
|
113
116
|
export const BACKLOG_PROJECT_ID_VAR = 'BACKLOG_PROJECT_ID';
|
|
114
117
|
/** Default FS reader — reads `.env.local` if present, swallowing any error to `null`. */
|
|
@@ -133,15 +136,14 @@ function readEnvValue(body, key) {
|
|
|
133
136
|
}
|
|
134
137
|
return value.length > 0 ? value : undefined;
|
|
135
138
|
}
|
|
136
|
-
/**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
export function resolveBacklogCredentials(rootDir, deps = {}) {
|
|
139
|
+
/** The names of the two credential keys that are missing, joined for a reason. */
|
|
140
|
+
function missingKeys(apiKey, projectId) {
|
|
141
|
+
return [!apiKey ? BACKLOG_API_KEY_VAR : null, !projectId ? BACKLOG_PROJECT_ID_VAR : null]
|
|
142
|
+
.filter(Boolean)
|
|
143
|
+
.join(' + ');
|
|
144
|
+
}
|
|
145
|
+
/** The pre-vault source: `{rootDir}/.env.local` (behavior unchanged). */
|
|
146
|
+
function resolveFromEnvFile(rootDir, deps) {
|
|
145
147
|
const read = deps.readFile ?? defaultReadFile;
|
|
146
148
|
let body;
|
|
147
149
|
try {
|
|
@@ -160,16 +162,107 @@ export function resolveBacklogCredentials(rootDir, deps = {}) {
|
|
|
160
162
|
const apiKey = readEnvValue(body, BACKLOG_API_KEY_VAR);
|
|
161
163
|
const projectId = readEnvValue(body, BACKLOG_PROJECT_ID_VAR);
|
|
162
164
|
if (!apiKey || !projectId) {
|
|
163
|
-
const missing = [
|
|
164
|
-
!apiKey ? BACKLOG_API_KEY_VAR : null,
|
|
165
|
-
!projectId ? BACKLOG_PROJECT_ID_VAR : null,
|
|
166
|
-
]
|
|
167
|
-
.filter(Boolean)
|
|
168
|
-
.join(' + ');
|
|
169
165
|
return {
|
|
170
166
|
status: 'not-configured',
|
|
171
|
-
reason: `Falta ${
|
|
167
|
+
reason: `Falta ${missingKeys(apiKey, projectId)} en \`.env.local\`; el backlog central no está configurado.`,
|
|
172
168
|
};
|
|
173
169
|
}
|
|
174
170
|
return { status: 'configured', apiKey, projectId };
|
|
175
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Translate a vault failure into the `reason` of a `not-configured` result. Branches on the
|
|
174
|
+
* error CLASS / `kind`, never on its text, and never quotes the error's message (it could carry a
|
|
175
|
+
* host or an operation, but never a value — still, the reason is built here, names only).
|
|
176
|
+
* 🔴 No reason mentions `.env.local`: in a repo with a vault that file is not the source, and
|
|
177
|
+
* naming it sends the developer after a file that no longer exists.
|
|
178
|
+
*/
|
|
179
|
+
function vaultFailureReason(error, link) {
|
|
180
|
+
if (error instanceof RailNoSessionError) {
|
|
181
|
+
return ('No hay una sesión activa en la bóveda de secretos, o ya caducó; entra con ' +
|
|
182
|
+
`\`${loginCommand()}\` y vuelve a correr.`);
|
|
183
|
+
}
|
|
184
|
+
if (error instanceof RailNotInstalledError) {
|
|
185
|
+
return ('El CLI de la bóveda (`infisical`) no está instalado; instálalo con ' +
|
|
186
|
+
`\`${INFISICAL_INSTALL_HINT}\` y entra con \`${loginCommand()}\`.`);
|
|
187
|
+
}
|
|
188
|
+
if (error instanceof VaultNoAccessError) {
|
|
189
|
+
return (`Tu sesión de la bóveda no tiene acceso al proyecto \`${link.projectId}\` de este repo ` +
|
|
190
|
+
'(o ese proyecto no existe); pide acceso a un admin de la bóveda.');
|
|
191
|
+
}
|
|
192
|
+
if (error instanceof VaultApiError && error.kind === 'timeout') {
|
|
193
|
+
return 'La bóveda de secretos no respondió a tiempo; reintenta más tarde.';
|
|
194
|
+
}
|
|
195
|
+
return ('La bóveda de secretos no respondió (o respondió algo que no reconozco); reintenta más tarde ' +
|
|
196
|
+
'y, si persiste, repórtalo al equipo del Factory.');
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The vault source (VAULT-006): the `local` environment of the repo's vault project, read with
|
|
200
|
+
* the vault client (VAULT-001) and the PERSON's session — no wrapper, so it works through `npx`.
|
|
201
|
+
* Never touches `.env.local`, even when a leftover one exists: the vault is the only source, and
|
|
202
|
+
* an old file would point at a rotated key.
|
|
203
|
+
*/
|
|
204
|
+
async function resolveFromVault(link, deps) {
|
|
205
|
+
let secrets;
|
|
206
|
+
try {
|
|
207
|
+
const client = createVaultClient(deps.vault);
|
|
208
|
+
secrets = await client.readSecrets(link.projectId, { env: link.envs.local, path: VAULT_ROOT_PATH }, { expandReferences: true });
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return { status: 'not-configured', reason: vaultFailureReason(error, link) };
|
|
212
|
+
}
|
|
213
|
+
const apiKey = secrets.get(BACKLOG_API_KEY_VAR)?.trim() || undefined;
|
|
214
|
+
const projectId = secrets.get(BACKLOG_PROJECT_ID_VAR)?.trim() || undefined;
|
|
215
|
+
if (!apiKey || !projectId) {
|
|
216
|
+
return {
|
|
217
|
+
status: 'not-configured',
|
|
218
|
+
reason: `Falta ${missingKeys(apiKey, projectId)} en el entorno \`${link.envs.local}\` de la bóveda ` +
|
|
219
|
+
'de este repo; el backlog central no está conectado (se da de alta con ' +
|
|
220
|
+
'`factory provision --services=backlog`).',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return { status: 'configured', apiKey, projectId };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Resolve the per-project backlog credentials. The presence of the `vault` block in
|
|
227
|
+
* `.timekast/provision.json` decides the source:
|
|
228
|
+
* - WITH a vault block → the `local` environment of the repo's vault project (never
|
|
229
|
+
* `.env.local`). Every vault failure (no session, no access, missing keys, vault down,
|
|
230
|
+
* `infisical` missing, no seam under tests) becomes `not-configured` with a reason that names
|
|
231
|
+
* the real cause.
|
|
232
|
+
* - WITHOUT one → `{rootDir}/.env.local`, exactly as before.
|
|
233
|
+
*
|
|
234
|
+
* FAIL-CLOSED: this function NEVER throws (nor rejects), so a derivative that never provisioned
|
|
235
|
+
* the backlog service does not crash `push`/`sync`; they just skip. An unreadable
|
|
236
|
+
* `provision.json` is `not-configured` too — "I could not tell" never falls back to `.env.local`.
|
|
237
|
+
*/
|
|
238
|
+
export async function resolveBacklogCredentials(rootDir, deps = {}) {
|
|
239
|
+
let link;
|
|
240
|
+
try {
|
|
241
|
+
link = (deps.vaultLink ?? readVaultLink)(rootDir);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
// Neither case is fixed by `--adopt`: a malformed `vault` block stops it on the same block,
|
|
245
|
+
// and over an unreadable file it rebuilds the state WITHOUT the `vault` block. Name the
|
|
246
|
+
// manual fix (the file is committed: git holds the last good copy).
|
|
247
|
+
return {
|
|
248
|
+
status: 'not-configured',
|
|
249
|
+
reason: error instanceof VaultLinkMalformedError
|
|
250
|
+
? 'El bloque `vault` de `.timekast/provision.json` no tiene la forma esperada; ' +
|
|
251
|
+
'corrígelo a mano (o bórralo si este repo no vive en la bóveda).'
|
|
252
|
+
: 'No pude leer `.timekast/provision.json` para saber si este repo vive en la bóveda: ' +
|
|
253
|
+
UNREADABLE_STATE_REMEDY,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (link === null)
|
|
257
|
+
return resolveFromEnvFile(rootDir, deps);
|
|
258
|
+
try {
|
|
259
|
+
return await resolveFromVault(link, deps);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Belt-and-suspenders: resolveFromVault already catches; the contract is "never throws".
|
|
263
|
+
return {
|
|
264
|
+
status: 'not-configured',
|
|
265
|
+
reason: 'La bóveda de secretos no respondió (o respondió algo que no reconozco); reintenta más tarde.',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -95,7 +95,7 @@ export function parseTitle(content) {
|
|
|
95
95
|
const match = content.match(/^# [\w-]+: (.*)$/m);
|
|
96
96
|
return match ? match[1].trim() : undefined;
|
|
97
97
|
}
|
|
98
|
-
/** Parse `> **Skills:**` into its backtick-quoted tokens (`` `sk-api`, `
|
|
98
|
+
/** Parse `> **Skills:**` into its backtick-quoted tokens (`` `sk-api`, `sk-db` `` → [...]). */
|
|
99
99
|
export function parseSkills(content) {
|
|
100
100
|
const line = matchBlockquoteField(content, 'Skills');
|
|
101
101
|
if (!line)
|