@timekast/factory 1.26.0 → 1.28.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/dist/commands/doctor.js +5 -4
- package/dist/commands/env.js +110 -24
- package/dist/commands/provision.js +384 -90
- package/dist/commands/ticket.js +487 -0
- package/dist/index.js +29 -0
- package/dist/lib/constants.js +133 -0
- package/dist/lib/env-file.js +264 -11
- package/dist/lib/env-wizard.js +110 -21
- package/dist/lib/initial-migration.js +12 -6
- package/dist/lib/neon-api.js +186 -21
- package/dist/lib/package-json.js +66 -2
- package/dist/lib/preflight.js +5 -1
- package/dist/lib/provision-preflight.js +29 -9
- package/dist/lib/provision-state.js +44 -0
- package/dist/lib/public-env-keys.json +27 -0
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* post-bootstrap, and that the patch channel for `src/` is EPIC 2 (agentic
|
|
14
14
|
* update, not yet available).
|
|
15
15
|
* - MISSING INITIAL MIGRATION: a derivative that ships a Drizzle schema and no
|
|
16
|
-
* generated `.sql`, so its `vercel-build` (`pnpm
|
|
17
|
-
*
|
|
16
|
+
* generated `.sql`, so its `vercel-build` (`pnpm build && pnpm db:migrate`)
|
|
17
|
+
* has nothing to apply and its first deploy never creates the tables.
|
|
18
|
+
* INFORMATIONAL — see below.
|
|
18
19
|
*
|
|
19
20
|
* No lockfile (pre-CLI repo) → delegate to the "run factory:update" advice, exit
|
|
20
21
|
* 0, never abort (same pattern as `status` / the §8 edge case).
|
|
@@ -65,8 +66,8 @@ export const A1_WARNING = 'Aviso de seguridad (A1): el boilerplate `src/` y sus
|
|
|
65
66
|
*/
|
|
66
67
|
export const MISSING_INITIAL_MIGRATION_NOTICE = `⚠ Falta la migración inicial: hay schema en \`${SCHEMA_DIR}/\` y ningún \`.sql\` en ` +
|
|
67
68
|
`\`${MIGRATIONS_DIR}/\`.\n` +
|
|
68
|
-
' El deploy corre `pnpm
|
|
69
|
-
'
|
|
69
|
+
' El deploy corre `pnpm build && pnpm db:migrate`, así que el primer deploy no crearía ' +
|
|
70
|
+
'ninguna tabla. Genera la migración con `pnpm db:generate` y commitéala antes del próximo ' +
|
|
70
71
|
'deploy.\n' +
|
|
71
72
|
' (Aviso informativo: `factory doctor` no bloquea — sale con éxito de todos modos.)';
|
|
72
73
|
/** Parse `doctor`'s argv slice into flags. Unknown flags are ignored. */
|
package/dist/commands/env.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { CLIError } from '../lib/cli-error.js';
|
|
16
|
-
import { PROVISION_OWNED, setEnvLine } from '../lib/env-file.js';
|
|
16
|
+
import { PER_ENV_MARKER, PER_ENV_REGISTRY_FILE, PROVISION_OWNED, addToPerEnvRegistry, findInlinePerEnvMarkers, findPerEnvMarkedKeys, listEnvKeys, readPerEnvRegistry, setEnvLine, } from '../lib/env-file.js';
|
|
17
17
|
import { applyDefaultOverrides, applyWizardAnswers, buildEnvReview, buildQuestionPlan, checkEnvCompleteness, generateVapidKeys, isQuestionActive, parseEnvBody, parseEnvExample, provisionEmailDefaults, } from '../lib/env-wizard.js';
|
|
18
18
|
import { defaultEnvPath, readVercelToken } from '../lib/provision-preflight.js';
|
|
19
19
|
import { readProvisionState } from '../lib/provision-state.js';
|
|
@@ -30,23 +30,40 @@ export const REDEPLOY_NOTICE = 'ℹ️ Las variables aplican en el próximo dep
|
|
|
30
30
|
export function parseEnvPushFlags(argv) {
|
|
31
31
|
return { dryRun: argv.includes('--dry-run') };
|
|
32
32
|
}
|
|
33
|
+
/** No extra exclusions — the common case (the derivative declared nothing). */
|
|
34
|
+
const NO_PER_ENV_KEYS = new Set();
|
|
33
35
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
36
|
+
* Read `.env.local` as text, or `''` when it is absent or unreadable.
|
|
37
|
+
*
|
|
38
|
+
* Fail-SAFE on purpose, and the polarity is the opposite of `readPerEnvRegistry`:
|
|
39
|
+
* this file is the source of VALUES, so an empty body means "there is nothing to
|
|
40
|
+
* push" — the harmless direction. The registry is the source of EXCLUSIONS, where
|
|
41
|
+
* an empty result would mean "push everything", so it throws instead. Do NOT
|
|
42
|
+
* copy this `catch` over there.
|
|
37
43
|
*/
|
|
38
|
-
|
|
39
|
-
const out = {};
|
|
44
|
+
function readEnvBody(filePath) {
|
|
40
45
|
if (!existsSync(filePath))
|
|
41
|
-
return
|
|
42
|
-
let text;
|
|
46
|
+
return '';
|
|
43
47
|
try {
|
|
44
|
-
|
|
48
|
+
return readFileSync(filePath, 'utf8');
|
|
45
49
|
}
|
|
46
50
|
catch {
|
|
47
|
-
return
|
|
51
|
+
return '';
|
|
48
52
|
}
|
|
49
|
-
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse an `.env.local` body into the product vars to push: skip comments/blanks,
|
|
56
|
+
* the provision-owned keys, the per-environment keys the derivative declared, and
|
|
57
|
+
* empty values. First-`=` split; surrounding quotes stripped (mirrors the rail
|
|
58
|
+
* reader in provision.ts).
|
|
59
|
+
*
|
|
60
|
+
* `perEnvKeys` only ever SUBTRACTS from the push — it is applied ALONGSIDE
|
|
61
|
+
* `PROVISION_OWNED` (a union), never instead of it, so a derivative cannot name a
|
|
62
|
+
* provision-owned key to opt it back in.
|
|
63
|
+
*/
|
|
64
|
+
export function parseProductVars(body, perEnvKeys = NO_PER_ENV_KEYS) {
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const line of body.split('\n')) {
|
|
50
67
|
const trimmed = line.trimStart();
|
|
51
68
|
if (trimmed === '' || trimmed.startsWith('#'))
|
|
52
69
|
continue;
|
|
@@ -54,7 +71,7 @@ export function readProductVars(filePath) {
|
|
|
54
71
|
if (eq === -1)
|
|
55
72
|
continue;
|
|
56
73
|
const key = line.slice(0, eq).trim();
|
|
57
|
-
if (key === '' || PROVISION_OWNED.has(key))
|
|
74
|
+
if (key === '' || PROVISION_OWNED.has(key) || perEnvKeys.has(key))
|
|
58
75
|
continue;
|
|
59
76
|
let value = line.slice(eq + 1).replace(/\s+$/, '');
|
|
60
77
|
if (value.length >= 2 &&
|
|
@@ -68,6 +85,10 @@ export function readProductVars(filePath) {
|
|
|
68
85
|
}
|
|
69
86
|
return out;
|
|
70
87
|
}
|
|
88
|
+
/** {@link parseProductVars} over the file at `filePath`. */
|
|
89
|
+
export function readProductVars(filePath, perEnvKeys = NO_PER_ENV_KEYS) {
|
|
90
|
+
return parseProductVars(readEnvBody(filePath), perEnvKeys);
|
|
91
|
+
}
|
|
71
92
|
/** Read `{ projectId, teamId }` from `.vercel/project.json` (written by provision). */
|
|
72
93
|
function readProjectLink(rootDir) {
|
|
73
94
|
const file = vercelProjectPath(rootDir);
|
|
@@ -90,13 +111,65 @@ function readProjectLink(rootDir) {
|
|
|
90
111
|
return { projectId, teamId };
|
|
91
112
|
}
|
|
92
113
|
export async function runEnvPush(flags, rootDir = process.cwd()) {
|
|
93
|
-
const
|
|
114
|
+
const body = readEnvBody(path.join(rootDir, '.env.local'));
|
|
115
|
+
// Resolve the derivative's per-environment declaration BEFORE anything can reach
|
|
116
|
+
// Vercel: a present-but-broken registry throws here and aborts the whole run
|
|
117
|
+
// (`--dry-run` included), instead of showing a plan as if nothing were protected.
|
|
118
|
+
const registry = readPerEnvRegistry(rootDir);
|
|
119
|
+
const marked = findPerEnvMarkedKeys(body);
|
|
120
|
+
const pendingAdds = marked.filter((key) => !registry.keys.includes(key));
|
|
121
|
+
// Reconciliation: a marker the registry lacks is APPENDED to it (add-only — see
|
|
122
|
+
// `addToPerEnvRegistry`). `--dry-run` reports it and writes nothing.
|
|
123
|
+
if (pendingAdds.length > 0 && !flags.dryRun) {
|
|
124
|
+
addToPerEnvRegistry(rootDir, registry, pendingAdds);
|
|
125
|
+
}
|
|
126
|
+
// Protected = registry ∪ marker ∪ PROVISION_OWNED. A union, never a replacement:
|
|
127
|
+
// the derivative's declaration can only SUBTRACT variables from the push.
|
|
128
|
+
const perEnvKeys = new Set([...registry.keys, ...marked]);
|
|
129
|
+
const productVars = parseProductVars(body, perEnvKeys);
|
|
94
130
|
const keys = Object.keys(productVars).sort();
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
131
|
+
const envKeys = listEnvKeys(body);
|
|
132
|
+
const omittedProvisionOwned = envKeys.filter((key) => PROVISION_OWNED.has(key)).sort();
|
|
133
|
+
// Provision-owned wins the label, so a redundant declaration is listed ONCE.
|
|
134
|
+
const omittedPerEnv = envKeys
|
|
135
|
+
.filter((key) => perEnvKeys.has(key) && !PROVISION_OWNED.has(key))
|
|
136
|
+
.sort();
|
|
137
|
+
const declaredButAbsent = [...perEnvKeys].filter((key) => !envKeys.includes(key)).sort();
|
|
138
|
+
if (pendingAdds.length > 0) {
|
|
139
|
+
const names = pendingAdds.join(', ');
|
|
140
|
+
console.log(flags.dryRun
|
|
141
|
+
? `ℹ️ Agregaría a \`${PER_ENV_REGISTRY_FILE}\` por el marcador ` +
|
|
142
|
+
`\`${PER_ENV_MARKER}\`: ${names} (--dry-run: no escribí nada).`
|
|
143
|
+
: `✔ Agregué a \`${PER_ENV_REGISTRY_FILE}\` (marcador \`${PER_ENV_MARKER}\`): ${names}.`);
|
|
144
|
+
}
|
|
145
|
+
// 🔴 An INLINE marker ABORTS, it does not warn. Warning and pushing anyway shipped a value the
|
|
146
|
+
// code had ALREADY PROVEN corrupt: `parseProductVars` splits on the first `=` and strips no
|
|
147
|
+
// inline comment, so `KEY="x" # @per-env` uploads the literal `"x" # @per-env`. Aborting is
|
|
148
|
+
// exactly as loud as the warning — the dev sees the wrong form and fixes it — and it is the
|
|
149
|
+
// only option that also protects. Filtered to keys the registry does NOT already cover: for a
|
|
150
|
+
// protected one the diagnosis would be false (it does not go up at all), and a message that
|
|
151
|
+
// sometimes lies trains everyone to skip reading it.
|
|
152
|
+
const inlineMarked = findInlinePerEnvMarkers(body).filter((key) => !perEnvKeys.has(key));
|
|
153
|
+
if (inlineMarked.length > 0) {
|
|
154
|
+
throw new CLIError(`${inlineMarked.map((key) => `\`${key}\``).join(', ')} lleva \`${PER_ENV_MARKER}\` en la MISMA ` +
|
|
155
|
+
'línea de la asignación. Ahí no protege nada, y además el valor subiría con el comentario ' +
|
|
156
|
+
'pegado (a Vercel llegaría el texto literal, comillas incluidas).\n' +
|
|
157
|
+
` El marcador va en un comentario en la línea de ARRIBA (\`SK.md §7.2\`). Aborté el push.`);
|
|
158
|
+
}
|
|
159
|
+
for (const key of declaredButAbsent) {
|
|
160
|
+
console.log(`⚠ \`${key}\` está declarada por ambiente pero no aparece en \`.env.local\` ` +
|
|
161
|
+
'— revisa si es un typo: así no está protegiendo nada.');
|
|
99
162
|
}
|
|
163
|
+
/** Names + reason, NEVER values: the omitted vars are the secret-bearing ones. */
|
|
164
|
+
const printOmitted = () => {
|
|
165
|
+
if (omittedProvisionOwned.length === 0 && omittedPerEnv.length === 0)
|
|
166
|
+
return;
|
|
167
|
+
console.log('\nOmitidas (no se envían):');
|
|
168
|
+
for (const key of omittedProvisionOwned)
|
|
169
|
+
console.log(` ${key} — provision-owned`);
|
|
170
|
+
for (const key of omittedPerEnv)
|
|
171
|
+
console.log(` ${key} — declarada por ambiente`);
|
|
172
|
+
};
|
|
100
173
|
const targets = ['production', 'preview'];
|
|
101
174
|
const plan = keys.map((key) => ({
|
|
102
175
|
key,
|
|
@@ -104,12 +177,23 @@ export async function runEnvPush(flags, rootDir = process.cwd()) {
|
|
|
104
177
|
target: targets,
|
|
105
178
|
}));
|
|
106
179
|
if (flags.dryRun) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
180
|
+
if (keys.length === 0) {
|
|
181
|
+
console.log('No hay variables de producto que subir en `.env.local`.');
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log(`Plan (${plan.length} vars → production, preview):`);
|
|
185
|
+
for (const key of keys)
|
|
186
|
+
console.log(` ${key}`);
|
|
187
|
+
}
|
|
188
|
+
printOmitted();
|
|
110
189
|
console.log('\n(--dry-run: nada se envió a Vercel.)');
|
|
111
190
|
return;
|
|
112
191
|
}
|
|
192
|
+
if (keys.length === 0) {
|
|
193
|
+
console.log('No hay variables de producto que subir en `.env.local` ' +
|
|
194
|
+
'(todo es provision-owned, está declarado por ambiente, o está vacío).');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
113
197
|
const { projectId, teamId } = readProjectLink(rootDir);
|
|
114
198
|
const token = readVercelToken(defaultEnvPath());
|
|
115
199
|
if (!token) {
|
|
@@ -219,9 +303,11 @@ export async function runEnvWizardApply(flags, rootDir = process.cwd(), deps = {
|
|
|
219
303
|
}
|
|
220
304
|
/**
|
|
221
305
|
* `factory env wizard --review [--json]` — a secret-safe summary of the resolved
|
|
222
|
-
* `.env.local`. Non-secret values are shown; secret values are masked
|
|
223
|
-
*
|
|
224
|
-
*
|
|
306
|
+
* `.env.local`. Non-secret values are shown; secret values are masked down to their
|
|
307
|
+
* last four characters, enough to tell WHICH value is in place while rotating and
|
|
308
|
+
* useless on their own; empty ones are flagged. This is what the guided flow shows
|
|
309
|
+
* the dev at the end (the agent NEVER echoes a raw secret) — the dev opens
|
|
310
|
+
* `.env.local` to see the full values.
|
|
225
311
|
*/
|
|
226
312
|
export function runEnvWizardReview(flags, rootDir = process.cwd()) {
|
|
227
313
|
const localPath = path.join(rootDir, '.env.local');
|
|
@@ -252,7 +338,7 @@ export function runEnvWizardReview(flags, rootDir = process.cwd()) {
|
|
|
252
338
|
for (const w of warnings)
|
|
253
339
|
console.log(` ⚠ ${w}`);
|
|
254
340
|
}
|
|
255
|
-
console.log('\nAbre `.env.local` para ver los valores completos (
|
|
341
|
+
console.log('\nAbre `.env.local` para ver los valores completos (de un secreto solo se ven sus últimos 4).');
|
|
256
342
|
}
|
|
257
343
|
/**
|
|
258
344
|
* `factory env wizard --gen-vapid` — generate a VAPID keypair and write it to
|