@timekast/factory 2.4.0 → 2.6.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/add.js +29 -8
- package/dist/commands/doctor.js +37 -8
- package/dist/commands/env.js +340 -8
- package/dist/commands/provision.js +327 -42
- package/dist/commands/prune.js +60 -10
- package/dist/commands/update.js +109 -85
- package/dist/commands/usage.js +303 -0
- package/dist/commands/vault.js +81 -18
- package/dist/index.js +52 -3
- package/dist/lib/backlog-mapping.js +1 -1
- package/dist/lib/claude-paths.js +8 -2
- package/dist/lib/claude-settings.js +104 -0
- package/dist/lib/constants.js +26 -0
- package/dist/lib/derived-dotfiles.js +165 -0
- package/dist/lib/email-from.js +12 -4
- package/dist/lib/env-file.js +4 -0
- package/dist/lib/env-wizard.js +1 -1
- package/dist/lib/initial-writes.js +113 -0
- package/dist/lib/kit-deps.js +163 -0
- package/dist/lib/kit-settings-history.json +13 -0
- package/dist/lib/lockfile.js +10 -5
- package/dist/lib/prune-remote.js +8 -6
- package/dist/lib/prune-vercel.js +137 -0
- package/dist/lib/public-env-keys.json +2 -0
- package/dist/lib/rail.js +5 -3
- package/dist/lib/railway-api.js +3 -0
- package/dist/lib/usage-scan.js +516 -0
- package/dist/lib/vault-adopt.js +103 -5
- package/dist/lib/vault-api.js +2 -2
- package/dist/lib/vercel-env.js +29 -0
- package/dist/lib/whats-new.js +123 -0
- package/package.json +1 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `factory usage` — measure REAL Claude Code consumption from the transcripts
|
|
3
|
+
* in the developer's home (`~/.claude/projects/`), any repo, any machine.
|
|
4
|
+
*
|
|
5
|
+
* 🔴 Unlike `update` / `status` / `doctor`, this command has NO repo preflight
|
|
6
|
+
* and must never grow one: the data source is the developer's HOME, not the
|
|
7
|
+
* cwd. It runs from any folder — inside a derivative, inside an unrelated
|
|
8
|
+
* repo, or in no repo at all — and reports every project dir it finds.
|
|
9
|
+
*
|
|
10
|
+
* v1 is LOCAL-ONLY: the command reads, prints and exports JSON. Nothing leaves
|
|
11
|
+
* the machine. (A v2 fleet aggregation — where the team's numbers land, with
|
|
12
|
+
* what credential, per-developer opt-in — is a separate decision; see the
|
|
13
|
+
* Encargo C brief. Do not wire network calls into this command.)
|
|
14
|
+
*
|
|
15
|
+
* Privacy invariant (design-level, not preference): only AGGREGATES are ever
|
|
16
|
+
* printed — dates, counts, tokens, model/agent names, repo slugs. Never message
|
|
17
|
+
* content, never client project file paths, never code fragments. The repo slug
|
|
18
|
+
* itself identifies the client, so `--anon` exists for shareable reports — the
|
|
19
|
+
* developer's explicit call, never the default.
|
|
20
|
+
*/
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { CLIError } from '../lib/cli-error.js';
|
|
24
|
+
import { aggregateUsage, scanUsage, } from '../lib/usage-scan.js';
|
|
25
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
26
|
+
const MONTH_RE = /^\d{4}-(0[1-9]|1[0-2])$/;
|
|
27
|
+
/** es-MX month names, index = month number - 1. */
|
|
28
|
+
const MONTH_NAMES = [
|
|
29
|
+
'enero',
|
|
30
|
+
'febrero',
|
|
31
|
+
'marzo',
|
|
32
|
+
'abril',
|
|
33
|
+
'mayo',
|
|
34
|
+
'junio',
|
|
35
|
+
'julio',
|
|
36
|
+
'agosto',
|
|
37
|
+
'septiembre',
|
|
38
|
+
'octubre',
|
|
39
|
+
'noviembre',
|
|
40
|
+
'diciembre',
|
|
41
|
+
];
|
|
42
|
+
const eq = (name) => (a) => {
|
|
43
|
+
const p = `--${name}=`;
|
|
44
|
+
return a.startsWith(p) ? a.slice(p.length) : null;
|
|
45
|
+
};
|
|
46
|
+
/** Parse `factory usage` flags. Unknown flags fail loudly (no silent typos). */
|
|
47
|
+
export function parseUsageFlags(argv) {
|
|
48
|
+
const flags = {
|
|
49
|
+
since: null,
|
|
50
|
+
until: null,
|
|
51
|
+
month: null,
|
|
52
|
+
repo: null,
|
|
53
|
+
agent: null,
|
|
54
|
+
json: false,
|
|
55
|
+
anon: false,
|
|
56
|
+
};
|
|
57
|
+
for (const arg of argv) {
|
|
58
|
+
if (arg === '--json')
|
|
59
|
+
flags.json = true;
|
|
60
|
+
else if (arg === '--anon')
|
|
61
|
+
flags.anon = true;
|
|
62
|
+
else if (eq('since')(arg) !== null)
|
|
63
|
+
flags.since = eq('since')(arg);
|
|
64
|
+
else if (eq('until')(arg) !== null)
|
|
65
|
+
flags.until = eq('until')(arg);
|
|
66
|
+
else if (eq('month')(arg) !== null)
|
|
67
|
+
flags.month = eq('month')(arg);
|
|
68
|
+
else if (eq('mes')(arg) !== null)
|
|
69
|
+
flags.month = eq('mes')(arg);
|
|
70
|
+
else if (eq('repo')(arg) !== null)
|
|
71
|
+
flags.repo = eq('repo')(arg);
|
|
72
|
+
else if (eq('agent')(arg) !== null)
|
|
73
|
+
flags.agent = eq('agent')(arg);
|
|
74
|
+
else {
|
|
75
|
+
throw new CLIError(`Flag desconocida para \`factory usage\`: \`${arg}\`.\n` +
|
|
76
|
+
'Uso: factory usage [--month=<mes|YYYY-MM> (alias --mes=)] [--since=YYYY-MM-DD] [--until=YYYY-MM-DD] ' +
|
|
77
|
+
'[--repo=<subcadena>] [--agent=<tipo>] [--anon] [--json]');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (flags.month && (flags.since || flags.until)) {
|
|
81
|
+
throw new CLIError('`--month` y `--since`/`--until` son excluyentes: usa uno u otro.');
|
|
82
|
+
}
|
|
83
|
+
return flags;
|
|
84
|
+
}
|
|
85
|
+
/** Local date → `YYYY-MM-DD`. */
|
|
86
|
+
const isoDay = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
87
|
+
/** Last day of a (year, monthIndex0) month. */
|
|
88
|
+
const monthEnd = (year, monthIdx) => isoDay(new Date(year, monthIdx + 1, 0));
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the report window. Precedence: `--month` (name or `YYYY-MM`) >
|
|
91
|
+
* `--since`/`--until` > default (the CURRENT calendar month, to date — the
|
|
92
|
+
* reference view of the brief). A month NAME resolves to its most recent
|
|
93
|
+
* occurrence: "julio" asked in agosto 2026 → 2026-07; asked in junio 2026 →
|
|
94
|
+
* 2025-07. `now` is injectable for tests.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveWindow(flags, now = new Date()) {
|
|
97
|
+
if (flags.month) {
|
|
98
|
+
const raw = flags.month.toLowerCase();
|
|
99
|
+
if (MONTH_RE.test(raw)) {
|
|
100
|
+
const [y, m] = raw.split('-').map(Number);
|
|
101
|
+
return { since: `${raw}-01`, until: monthEnd(y, m - 1) };
|
|
102
|
+
}
|
|
103
|
+
const idx = MONTH_NAMES.indexOf(raw);
|
|
104
|
+
if (idx === -1) {
|
|
105
|
+
throw new CLIError(`Mes no reconocido: \`${flags.month}\`. Acepta ${MONTH_NAMES.join(', ')} o \`YYYY-MM\`.`);
|
|
106
|
+
}
|
|
107
|
+
const year = idx > now.getMonth() ? now.getFullYear() - 1 : now.getFullYear();
|
|
108
|
+
const mm = String(idx + 1).padStart(2, '0');
|
|
109
|
+
return { since: `${year}-${mm}-01`, until: monthEnd(year, idx) };
|
|
110
|
+
}
|
|
111
|
+
for (const [name, v] of [
|
|
112
|
+
['--since', flags.since],
|
|
113
|
+
['--until', flags.until],
|
|
114
|
+
]) {
|
|
115
|
+
if (v && !DATE_RE.test(v)) {
|
|
116
|
+
throw new CLIError(`Fecha inválida en \`${name}\`: \`${v}\` (formato: YYYY-MM-DD).`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const today = isoDay(now);
|
|
120
|
+
const firstOfMonth = `${today.slice(0, 7)}-01`;
|
|
121
|
+
const since = flags.since ?? (flags.until ? '1970-01-01' : firstOfMonth);
|
|
122
|
+
const until = flags.until ?? today;
|
|
123
|
+
if (since > until) {
|
|
124
|
+
throw new CLIError(`Ventana vacía: \`--since=${since}\` es posterior a \`--until=${until}\`.`);
|
|
125
|
+
}
|
|
126
|
+
return { since, until };
|
|
127
|
+
}
|
|
128
|
+
/** Default projects root: the developer's home, never the cwd. */
|
|
129
|
+
export const defaultUsageRoot = () => join(homedir(), '.claude', 'projects');
|
|
130
|
+
const M = (t) => (t / 1e6).toFixed(1);
|
|
131
|
+
const pct = (part, total) => total ? `${((100 * part) / total).toFixed(1)}%` : '—';
|
|
132
|
+
/** Proportional stacked bar: █ = main, ▓ = subagentes; width ∝ day total / max. */
|
|
133
|
+
const dayBar = (main, sub, max, width = 24) => {
|
|
134
|
+
if (!max)
|
|
135
|
+
return '';
|
|
136
|
+
const total = main + sub;
|
|
137
|
+
const len = Math.max(total > 0 ? 1 : 0, Math.round((total / max) * width));
|
|
138
|
+
const mainLen = Math.round((main / Math.max(total, 1)) * len);
|
|
139
|
+
return '█'.repeat(mainLen) + '▓'.repeat(len - mainLen);
|
|
140
|
+
};
|
|
141
|
+
/** Render the human report (es-MX). Kept pure for tests: returns the text. */
|
|
142
|
+
export function renderUsageReport(report) {
|
|
143
|
+
const L = [];
|
|
144
|
+
const t = report.totals;
|
|
145
|
+
const filterNote = [
|
|
146
|
+
report.filters.repo ? `repo~"${report.filters.repo}"` : null,
|
|
147
|
+
report.filters.agent ? `agent=${report.filters.agent}` : null,
|
|
148
|
+
report.filters.anon ? 'anon' : null,
|
|
149
|
+
]
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
.join(' · ');
|
|
152
|
+
L.push('');
|
|
153
|
+
L.push(`📊 factory usage — ${report.window.since} → ${report.window.until}` +
|
|
154
|
+
(filterNote ? ` [${filterNote}]` : ''));
|
|
155
|
+
L.push(` ${M(t.total)} M tokens · ${t.mainRequests.toLocaleString('en-US')} requests con usage real · ` +
|
|
156
|
+
`${t.invocations.toLocaleString('en-US')} invocaciones de subagente (${t.agentTypes} tipos) · ${t.reposScanned} repos barridos`);
|
|
157
|
+
if (t.total === 0) {
|
|
158
|
+
L.push('');
|
|
159
|
+
L.push(' (sin datos en la ventana — ¿existe ~/.claude/projects/ en esta máquina?)');
|
|
160
|
+
L.push('');
|
|
161
|
+
return L.join('\n');
|
|
162
|
+
}
|
|
163
|
+
const maxDay = Math.max(...report.byDay.map((d) => d.main + d.sub), 0);
|
|
164
|
+
L.push('');
|
|
165
|
+
L.push('DÍA × SUPERFICIE (M tokens · █ main loop · ▓ subagentes · barra ∝ día más caro)');
|
|
166
|
+
L.push(' ' + 'día'.padEnd(12) + 'main'.padStart(8) + 'subs'.padStart(8) + 'total'.padStart(8));
|
|
167
|
+
for (const d of report.byDay) {
|
|
168
|
+
L.push(' ' +
|
|
169
|
+
d.date.padEnd(12) +
|
|
170
|
+
M(d.main).padStart(8) +
|
|
171
|
+
M(d.sub).padStart(8) +
|
|
172
|
+
M(d.main + d.sub).padStart(8) +
|
|
173
|
+
' ' +
|
|
174
|
+
dayBar(d.main, d.sub, maxDay));
|
|
175
|
+
}
|
|
176
|
+
L.push('');
|
|
177
|
+
L.push('POR MODELO');
|
|
178
|
+
for (const m of report.byModel) {
|
|
179
|
+
L.push(' ' +
|
|
180
|
+
m.model.padEnd(16) +
|
|
181
|
+
`${M(m.tokens)} M`.padStart(11) +
|
|
182
|
+
pct(m.tokens, t.total).padStart(8));
|
|
183
|
+
}
|
|
184
|
+
L.push('');
|
|
185
|
+
L.push('POR REPO' + (report.filters.anon ? ' (anonimizado)' : ''));
|
|
186
|
+
const shownRepos = report.byRepo.slice(0, 20);
|
|
187
|
+
for (const r of shownRepos) {
|
|
188
|
+
L.push(' ' +
|
|
189
|
+
r.repo.slice(0, 34).padEnd(35) +
|
|
190
|
+
`${M(r.tokens)} M`.padStart(11) +
|
|
191
|
+
pct(r.tokens, t.total).padStart(8));
|
|
192
|
+
}
|
|
193
|
+
if (report.byRepo.length > shownRepos.length) {
|
|
194
|
+
const rest = report.byRepo.slice(20);
|
|
195
|
+
const restTok = rest.reduce((a, r) => a + r.tokens, 0);
|
|
196
|
+
L.push(` … y ${rest.length} repos más`.padEnd(37) +
|
|
197
|
+
`${M(restTok)} M`.padStart(11) +
|
|
198
|
+
pct(restTok, t.total).padStart(8) +
|
|
199
|
+
' (completos en --json)');
|
|
200
|
+
}
|
|
201
|
+
L.push('');
|
|
202
|
+
L.push('POR AGENTE (top 12 de ' + String(t.agentTypes) + ')');
|
|
203
|
+
L.push(' ' +
|
|
204
|
+
'agente'.padEnd(26) +
|
|
205
|
+
'n'.padStart(5) +
|
|
206
|
+
'M tokens'.padStart(10) +
|
|
207
|
+
'% subs'.padStart(8) +
|
|
208
|
+
'% total'.padStart(9));
|
|
209
|
+
for (const a of report.byAgent.slice(0, 12)) {
|
|
210
|
+
L.push(' ' +
|
|
211
|
+
a.agent.slice(0, 25).padEnd(26) +
|
|
212
|
+
String(a.invocations).padStart(5) +
|
|
213
|
+
M(a.tokens).padStart(10) +
|
|
214
|
+
pct(a.tokens, t.sub).padStart(8) +
|
|
215
|
+
pct(a.tokens, t.total).padStart(9));
|
|
216
|
+
}
|
|
217
|
+
if (report.delivered.length) {
|
|
218
|
+
L.push('');
|
|
219
|
+
L.push('TOKENS POR UNIDAD ENTREGADA (agentes que escriben código; excluye colgadas)');
|
|
220
|
+
L.push(' ' +
|
|
221
|
+
'agente'.padEnd(24) +
|
|
222
|
+
'modelo'.padEnd(12) +
|
|
223
|
+
'n'.padStart(4) +
|
|
224
|
+
'M tokens'.padStart(10) +
|
|
225
|
+
'líneas'.padStart(9) +
|
|
226
|
+
'M tok/1k ln'.padStart(13));
|
|
227
|
+
for (const w of report.delivered) {
|
|
228
|
+
const perK = w.lines ? ((w.tokens / w.lines) * 1000) / 1e6 : null;
|
|
229
|
+
L.push(' ' +
|
|
230
|
+
w.agent.slice(0, 23).padEnd(24) +
|
|
231
|
+
w.model.padEnd(12) +
|
|
232
|
+
String(w.invocations).padStart(4) +
|
|
233
|
+
M(w.tokens).padStart(10) +
|
|
234
|
+
String(w.lines).padStart(9) +
|
|
235
|
+
(perK === null ? '—' : perK.toFixed(2)).padStart(13));
|
|
236
|
+
}
|
|
237
|
+
if (t.unenriched) {
|
|
238
|
+
L.push(` ℹ️ ${t.unenriched} invocaciones sin \`toolUseResult\` del padre: cuentan en el consumo,` +
|
|
239
|
+
' pero sin líneas — no entran a esta tabla.');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (report.reviewCost.length) {
|
|
243
|
+
L.push('');
|
|
244
|
+
L.push('REVISORES — costo por veredicto (no producen líneas; se miden por invocación)');
|
|
245
|
+
L.push(' ' +
|
|
246
|
+
'agente'.padEnd(24) +
|
|
247
|
+
'modelo'.padEnd(12) +
|
|
248
|
+
'n'.padStart(4) +
|
|
249
|
+
'M tok/inv'.padStart(11) +
|
|
250
|
+
'min/inv'.padStart(9) +
|
|
251
|
+
'ctx medio'.padStart(11));
|
|
252
|
+
for (const r of report.reviewCost) {
|
|
253
|
+
L.push(' ' +
|
|
254
|
+
r.agent.slice(0, 23).padEnd(24) +
|
|
255
|
+
r.model.padEnd(12) +
|
|
256
|
+
String(r.invocations).padStart(4) +
|
|
257
|
+
(r.tokens / r.invocations / 1e6).toFixed(1).padStart(11) +
|
|
258
|
+
(r.minutes / r.invocations).toFixed(1).padStart(9) +
|
|
259
|
+
`${Math.round(r.avgCtx / 1000)}k`.padStart(11));
|
|
260
|
+
}
|
|
261
|
+
L.push(' ⚠️ compara ctx medio antes de leer diferencias: revisar epics más chicos gasta menos por razones ajenas al modelo.');
|
|
262
|
+
}
|
|
263
|
+
if (report.reviewRounds.length) {
|
|
264
|
+
L.push('');
|
|
265
|
+
L.push('RONDAS DE REVISIÓN (re-spawns del mismo revisor en la misma sesión)');
|
|
266
|
+
for (const r of report.reviewRounds.slice(0, 8)) {
|
|
267
|
+
L.push(' ' +
|
|
268
|
+
r.agent.slice(0, 23).padEnd(24) +
|
|
269
|
+
`${String(r.sessions).padStart(3)} sesiones · ${String(r.spawns).padStart(3)} spawns · ` +
|
|
270
|
+
`${(r.spawns / r.sessions).toFixed(2)} rondas/sesión`);
|
|
271
|
+
}
|
|
272
|
+
L.push(' ⚠️ un re-spawn puede ser una fase distinta del workflow, no una re-ronda del mismo veredicto — revisa los prompts antes de leerlo como indecisión.');
|
|
273
|
+
}
|
|
274
|
+
L.push('');
|
|
275
|
+
L.push('REPARTO DEL CONSUMO');
|
|
276
|
+
L.push(` main loop: ${M(t.main).padStart(10)} M tokens ${pct(t.main, t.total)}`);
|
|
277
|
+
L.push(` subagentes: ${M(t.sub).padStart(10)} M tokens ${pct(t.sub, t.total)}`);
|
|
278
|
+
L.push(' ✔ validación cruzada: el % de subagentes debe cuadrar con "Subagents % of usage" del panel Account & Usage de tu cliente. Si no cuadra, el número de ESTA herramienta está mal.');
|
|
279
|
+
L.push(`\n Tokens exactos del \`usage\` de cada transcript — sin estimadores. Solo agregados: nunca contenido de mensajes ni paths de proyectos.` +
|
|
280
|
+
(report.filters.anon
|
|
281
|
+
? ''
|
|
282
|
+
: ' Para compartir el reporte fuera del equipo: `--anon` anonimiza los repos.'));
|
|
283
|
+
L.push('');
|
|
284
|
+
return L.join('\n');
|
|
285
|
+
}
|
|
286
|
+
/** Entry point for `factory usage`. */
|
|
287
|
+
export function runUsage(flags, root = defaultUsageRoot()) {
|
|
288
|
+
const window = resolveWindow(flags);
|
|
289
|
+
const scan = scanUsage(root, window);
|
|
290
|
+
const report = aggregateUsage(scan, window, {
|
|
291
|
+
repo: flags.repo,
|
|
292
|
+
agent: flags.agent,
|
|
293
|
+
anon: flags.anon,
|
|
294
|
+
});
|
|
295
|
+
if (flags.json) {
|
|
296
|
+
// `--json` contract (same as `doctor --launcher --json` / `status --json`):
|
|
297
|
+
// machine-readable JSON to stdout, NOTHING else, exit 0. Stable field order
|
|
298
|
+
// + deterministic array sorting → diffable between runs.
|
|
299
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
console.log(renderUsageReport(report));
|
|
303
|
+
}
|
package/dist/commands/vault.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* Contract: `fx-secrets-vault §8`. The decisions live in `lib/vault-adopt.ts`; this module runs
|
|
4
4
|
* them against the real services, in the order the contract fixes:
|
|
5
5
|
*
|
|
6
|
-
* Preconditions (both modes) —
|
|
7
|
-
* (the comparison of `factory status`); no fork in the
|
|
8
|
-
* `factory update --verify`).
|
|
6
|
+
* Preconditions (both modes) — the wrapper on disk (`full` and `core` both ship it; missing →
|
|
7
|
+
* `factory update`); the brain up to date (the comparison of `factory status`); no fork in the
|
|
8
|
+
* files the wrapper runs (the census of `factory update --verify`). A `core` repo is accepted:
|
|
9
|
+
* its own `dev`/`build` scripts read as customized and run without the vault until the team
|
|
10
|
+
* prefixes them with the wrapper (Factory#76).
|
|
9
11
|
*
|
|
10
12
|
* Plan (no `--apply`) — inventory by NAME and display hash of `.env.local`, Vercel production +
|
|
11
13
|
* preview and Railway; the state of each key, where the apply would write it, the confirmations
|
|
@@ -39,11 +41,10 @@ import { WITH_VAULT_SCRIPT_FILE } from '../lib/constants.js';
|
|
|
39
41
|
import { parseEnvBody } from '../lib/env-wizard.js';
|
|
40
42
|
import { hasLockfile, readLockfile } from '../lib/lockfile.js';
|
|
41
43
|
import { getConnectionUri, getNeonProjectOrg, isNeonKeyFamily, listNeonApiKeys, listNeonBranches, NEON_DEVELOP_BRANCH, NeonApiKeyListError, } from '../lib/neon-api.js';
|
|
42
|
-
import { detectCoreProfile } from '../lib/provision-preflight.js';
|
|
43
44
|
import { readDestroyTarget, readNeonRevokePending, readVaultDeclined, readVaultLink, addNeonRevokePending, } from '../lib/provision-state.js';
|
|
44
45
|
import { createRailResolver, RAIL_TIMEKAST } from '../lib/rail.js';
|
|
45
46
|
import { createRailwayClient, RAILWAY_TOKEN_KEY, railwayFetchTransport, } from '../lib/railway-api.js';
|
|
46
|
-
import { buildPlanEntries, classifySensitive, createDisplayHasher, describeWrite, ENV_LOCAL_BACKUP_FILE, ENV_LOCAL_FILE, FACTORY_REPO, FACTORY_SLUG, folderOfTarget, isConnectedEmptyDeploy, isFactoryIdentity, manualVaultProjectSteps, isFactoryMode, isProjectScopedNeonKey, isRailKey, isRailwayProvided, isRailwaySyncTo, listRailwayProjects, MFA_KEY, NEON_API_KEY, planAdoptionLayout, planRailwayImport, proposeRailwaySyncs, RAILWAY_VAULT_ENVS, railwaySyncDifferences, railwaySyncLabel, railwaySyncName, readRailwayInventory, readRailwayVariables, recordedAdoptionVercelId, RESEND_KEY, retiredKeys, retireEnvLocal, sealedStopExplanation, sharedCandidatesOf, SHORTIO_KEY, wrapperDrift, writeAdoptionLink, writeDeclineMark, } from '../lib/vault-adopt.js';
|
|
47
|
+
import { buildPlanEntries, classifySensitive, adoptSender, createDisplayHasher, dangerousSharedTargets, describeWrite, ENV_LOCAL_BACKUP_FILE, ENV_LOCAL_FILE, FACTORY_REPO, FACTORY_SLUG, folderOfTarget, isConnectedEmptyDeploy, isFactoryIdentity, manualVaultProjectSteps, isFactoryMode, isProjectScopedNeonKey, isRailKey, isRailwayProvided, isRailwaySyncTo, listRailwayProjects, MFA_KEY, NEON_API_KEY, planAdoptionLayout, planRailwayImport, proposeRailwaySyncs, RAILWAY_VAULT_ENVS, railwaySyncDifferences, railwaySyncLabel, railwaySyncName, readRailwayInventory, readRailwayVariables, recordedAdoptionVercelId, RESEND_KEY, retiredKeys, retireEnvLocal, sealedStopExplanation, SENDER_KEYS, sharedCandidatesOf, sharedDatabaseKeys, SHORTIO_KEY, vaultWriteConflicts, wrapperDrift, writeAdoptionLink, writeDeclineMark, } from '../lib/vault-adopt.js';
|
|
47
48
|
import { createVaultClient, formatLocation, VAULT_CI_PATH, VAULT_ENV_DEVELOP, VAULT_ENV_LOCAL, VAULT_ENV_MAIN, VAULT_ROOT_PATH, VAULT_TEST_ISOLATION_KEY, VaultSeamMissingError, } from '../lib/vault-api.js';
|
|
48
49
|
import { LOCAL_ONLY_KEYS } from '../lib/env-file.js';
|
|
49
50
|
import { buildEnvPlan, deployHostCandidates, generateAuthSecret, listEnvVarNames, listProjectDomains, locateVercelProject, readEnvVarsWithValues, relatedOriginsFor, } from '../lib/vercel-env.js';
|
|
@@ -321,6 +322,12 @@ const UNKNOWN = 'unknown';
|
|
|
321
322
|
* reports (`resolveSuccessExitCode`), distinct from 0 so no one — person or agent — reads it as done.
|
|
322
323
|
*/
|
|
323
324
|
export const VAULT_ADOPT_SEALED_EXIT_CODE = 3;
|
|
325
|
+
/** Factory#73 — why a shared Vercel record blocks (plan and apply say it the same way). */
|
|
326
|
+
const SHARED_RECORDS_REASON = 'Variables de Vercel que son UN solo registro para production y preview, y develop tendría su ' +
|
|
327
|
+
'propio valor: el sync de preview reescribiría el registro y producción terminaría con el de develop';
|
|
328
|
+
/** Factory#75 — why a shared database key needs a yes. */
|
|
329
|
+
const SHARED_DATABASES_REASON = 'Preview tiene la MISMA base que producción: los previews y el entorno local escribirían en la ' +
|
|
330
|
+
'base de producción';
|
|
324
331
|
function realField(value) {
|
|
325
332
|
return value && value !== UNKNOWN ? value : null;
|
|
326
333
|
}
|
|
@@ -331,6 +338,7 @@ function vercelMaps(vars) {
|
|
|
331
338
|
const branchScoped = [];
|
|
332
339
|
const sensitive = [];
|
|
333
340
|
const unreadableOther = new Set();
|
|
341
|
+
const sharedTargets = new Set();
|
|
334
342
|
const note = (key, type, value, target) => {
|
|
335
343
|
if (value !== null)
|
|
336
344
|
return;
|
|
@@ -340,6 +348,9 @@ function vercelMaps(vars) {
|
|
|
340
348
|
unreadableOther.add(key);
|
|
341
349
|
};
|
|
342
350
|
for (const env of vars) {
|
|
351
|
+
if (env.target.includes('production') && env.target.includes('preview') && !env.gitBranch) {
|
|
352
|
+
sharedTargets.add(env.key);
|
|
353
|
+
}
|
|
343
354
|
if (env.target.includes('production')) {
|
|
344
355
|
production.set(env.key, env.value);
|
|
345
356
|
note(env.key, env.type, env.value, 'production');
|
|
@@ -359,6 +370,7 @@ function vercelMaps(vars) {
|
|
|
359
370
|
branchScoped,
|
|
360
371
|
sensitive,
|
|
361
372
|
unreadableOther: [...unreadableOther].sort(),
|
|
373
|
+
sharedTargets: [...sharedTargets].sort(),
|
|
362
374
|
};
|
|
363
375
|
}
|
|
364
376
|
/** Why a derivation needs a host, and which one. */
|
|
@@ -1011,10 +1023,14 @@ async function prepareAdoption(rootDir, flags, deps) {
|
|
|
1011
1023
|
const resendTeamKey = hasResend ? await rail.getOptional(RESEND_KEY) : null;
|
|
1012
1024
|
const replaceResend = resendTeamKey !== null &&
|
|
1013
1025
|
[values.production, values.preview, values.local].some((m) => m.get(RESEND_KEY) === resendTeamKey);
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1026
|
+
// Factory#74 — the repo's own sender, under any name it uses; the kit's default only when none.
|
|
1027
|
+
const found = adoptSender(values);
|
|
1028
|
+
if (found.sender === null && replaceResend && found.unknown.length > 0) {
|
|
1029
|
+
throw new CLIError(`No encuentro el remitente del repo en ${SENDER_KEYS.join(', ')}, pero ${found.unknown.join(', ')} ` +
|
|
1030
|
+
'tiene una dirección de correo. No acuño la key de Resend para el dominio del kit a ciegas: ' +
|
|
1031
|
+
'agrega `EMAIL_FROM` con el remitente real al deploy y repite. No se escribió nada.');
|
|
1032
|
+
}
|
|
1033
|
+
const sender = found.sender ?? PROVISION_EMAIL_FROM;
|
|
1018
1034
|
const resendDomain = sendingDomainOf(sender);
|
|
1019
1035
|
const client = deps.vaultClient ?? createVaultClient();
|
|
1020
1036
|
const githubSecretNames = repo
|
|
@@ -1207,6 +1223,8 @@ async function prepareAdoption(rootDir, flags, deps) {
|
|
|
1207
1223
|
sealed: [],
|
|
1208
1224
|
sealedExplanation: null,
|
|
1209
1225
|
branchScoped: vercel.branchScoped,
|
|
1226
|
+
sharedRecords: dangerousSharedTargets(vercel.sharedTargets, writes),
|
|
1227
|
+
sharedDatabases: sharedDatabaseKeys(writes, neonProjectId !== null),
|
|
1210
1228
|
confirmations: confirmations.map((c) => c.name),
|
|
1211
1229
|
github,
|
|
1212
1230
|
wrapper: {
|
|
@@ -1235,6 +1253,7 @@ async function prepareAdoption(rootDir, flags, deps) {
|
|
|
1235
1253
|
hosts,
|
|
1236
1254
|
hostCandidates,
|
|
1237
1255
|
vaultProjectExists,
|
|
1256
|
+
sharedTargets: vercel.sharedTargets,
|
|
1238
1257
|
vercelToken,
|
|
1239
1258
|
teamId,
|
|
1240
1259
|
resendTeamKey: replaceResend ? resendTeamKey : null,
|
|
@@ -1354,6 +1373,14 @@ function printPlan(plan, apply) {
|
|
|
1354
1373
|
`${plan.branchScoped.map((v) => `${v.name} (rama ${v.branch})`).join(', ')}. ` +
|
|
1355
1374
|
'Decide en Vercel (quítales la rama o bórralas) y repite. Bloquean el --apply.');
|
|
1356
1375
|
}
|
|
1376
|
+
if (plan.sharedRecords.length > 0) {
|
|
1377
|
+
out(`\n🔴 ${SHARED_RECORDS_REASON}: ${plan.sharedRecords.join(', ')}. ` +
|
|
1378
|
+
'Sepáralas en Vercel (un registro por entorno) y repite. Bloquean el --apply.');
|
|
1379
|
+
}
|
|
1380
|
+
if (plan.sharedDatabases.length > 0) {
|
|
1381
|
+
out(`\n⚠ ${SHARED_DATABASES_REASON}: ${plan.sharedDatabases.join(', ')}. ` +
|
|
1382
|
+
'El --apply te pide confirmarlo; si no es lo que quieres, dale a preview su propia base en el deploy y repite.');
|
|
1383
|
+
}
|
|
1357
1384
|
if (plan.railwaySyncs.length > 0) {
|
|
1358
1385
|
out(`\nRailway${plan.railway ? ` (proyecto \`${plan.railway.project}\`, id ${plan.railway.projectId})` : ''} — ` +
|
|
1359
1386
|
'syncs propuestos (confirmas cada uno; el que no confirmes no se importa ni se crea): ' +
|
|
@@ -1483,6 +1510,32 @@ async function writeLayout(prepared, projectId, deps) {
|
|
|
1483
1510
|
}
|
|
1484
1511
|
return resend;
|
|
1485
1512
|
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Factory#72 — the writes are `create-if-missing`: a key the vault already holds keeps its value,
|
|
1515
|
+
* and the first sync carries THAT value to the deploy. A vault value that differs from what the
|
|
1516
|
+
* deploy runs (a stale copy, a Vercel ciphertext imported by hand) stops the apply BEFORE writing.
|
|
1517
|
+
*/
|
|
1518
|
+
async function assertVaultMatchesPlan(prepared, projectId) {
|
|
1519
|
+
const locations = new Map();
|
|
1520
|
+
for (const write of prepared.writes) {
|
|
1521
|
+
if (write.location.env !== VAULT_ENV_LOCAL) {
|
|
1522
|
+
locations.set(formatLocation(write.location), write.location);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
const existing = new Map();
|
|
1526
|
+
for (const [where, location] of locations) {
|
|
1527
|
+
existing.set(where, await prepared.client.readSecrets(projectId, location, { expandReferences: true }));
|
|
1528
|
+
}
|
|
1529
|
+
const conflicts = vaultWriteConflicts(prepared.writes, existing, createDisplayHasher());
|
|
1530
|
+
if (conflicts.length === 0)
|
|
1531
|
+
return;
|
|
1532
|
+
throw new CLIError('La bóveda ya tiene OTRO valor en claves que el deploy usa, y el primer sync lo llevaría al deploy: ' +
|
|
1533
|
+
conflicts
|
|
1534
|
+
.map((c) => `${c.location} ${c.key} (bóveda ${c.vault} ≠ deploy ${c.plan})`)
|
|
1535
|
+
.join(', ') +
|
|
1536
|
+
'. Lo que corre hoy es el deploy: corrige o borra esas claves en la bóveda y repite. ' +
|
|
1537
|
+
'No escribí ningún secreto ni creé ningún sync.');
|
|
1538
|
+
}
|
|
1486
1539
|
/**
|
|
1487
1540
|
* Right before creating the syncs: the vault folder must hold every key its Vercel target holds
|
|
1488
1541
|
* (import first — the first sync deletes nothing), except the retirements already confirmed; and
|
|
@@ -1753,6 +1806,23 @@ async function applyAdoption(prepared, deps) {
|
|
|
1753
1806
|
`${plan.railwayConflicts.join(', ')}. Una carpeta tiene un solo valor por clave y no elijo uno a ` +
|
|
1754
1807
|
'ciegas: iguálalos en Railway, o no confirmes uno de esos syncs. No se escribió nada.');
|
|
1755
1808
|
}
|
|
1809
|
+
// Factory#73 — recomputed on the final writes: a materialized value can give develop its own.
|
|
1810
|
+
const sharedRecords = dangerousSharedTargets(prepared.sharedTargets, prepared.writes);
|
|
1811
|
+
if (sharedRecords.length > 0) {
|
|
1812
|
+
throw new CLIError(`${SHARED_RECORDS_REASON}: ${sharedRecords.join(', ')}. Sepáralas en Vercel (un registro por ` +
|
|
1813
|
+
'entorno) y repite. No se escribió nada.');
|
|
1814
|
+
}
|
|
1815
|
+
// Factory#75 — any provider, not only Neon: a yes before previews write to production's base.
|
|
1816
|
+
const sharedDatabases = sharedDatabaseKeys(prepared.writes, prepared.neonProjectId !== null);
|
|
1817
|
+
if (sharedDatabases.length > 0) {
|
|
1818
|
+
prepared.questions = [
|
|
1819
|
+
...prepared.questions,
|
|
1820
|
+
{
|
|
1821
|
+
name: `misma base: ${sharedDatabases.join(', ')}`,
|
|
1822
|
+
question: `${SHARED_DATABASES_REASON} (${sharedDatabases.join(', ')}). ¿Confirmas?`,
|
|
1823
|
+
},
|
|
1824
|
+
];
|
|
1825
|
+
}
|
|
1756
1826
|
await confirmAll(prepared, deps);
|
|
1757
1827
|
// Resolved BEFORE writing anything: without it no Railway sync can exist, so nothing is imported.
|
|
1758
1828
|
const railwayConnection = railwaySelected.length > 0 ? await requireRailwayConnection(client) : null;
|
|
@@ -1768,6 +1838,8 @@ async function applyAdoption(prepared, deps) {
|
|
|
1768
1838
|
reportDeleteProtection(layout.project.slug, layout.deleteProtection);
|
|
1769
1839
|
for (const member of layout.members.added)
|
|
1770
1840
|
console.log(` + bóveda: miembro ${member}`);
|
|
1841
|
+
// Factory#72 — what the vault already holds must be what the deploy runs, before writing.
|
|
1842
|
+
await assertVaultMatchesPlan(prepared, projectId);
|
|
1771
1843
|
// (2) the writes — import first.
|
|
1772
1844
|
const resend = await writeLayout(prepared, projectId, deps);
|
|
1773
1845
|
// (2b) ADOPT-009 — the project's Neon key, minted key-only BEFORE any sync exists (so the GitHub
|
|
@@ -2129,15 +2201,6 @@ function declineVault(rootDir, deps) {
|
|
|
2129
2201
|
export async function runVaultAdopt(flags, deps = {}) {
|
|
2130
2202
|
assertAdoptSeams(deps);
|
|
2131
2203
|
const rootDir = deps.rootDir ?? process.cwd();
|
|
2132
|
-
const core = detectCoreProfile(rootDir);
|
|
2133
|
-
if (core !== null) {
|
|
2134
|
-
const why = core === 'lockfile'
|
|
2135
|
-
? 'el lockfile declara el perfil `core`'
|
|
2136
|
-
: `no hay lockfile y falta \`${WITH_VAULT_SCRIPT_FILE}\``;
|
|
2137
|
-
console.log(`ℹ️ La bóveda no se ofrece en perfil \`core\` (${why}): ese perfil no trae el wrapper que la inyecta ` +
|
|
2138
|
-
'a los scripts. No hice nada.');
|
|
2139
|
-
return { status: 'core' };
|
|
2140
|
-
}
|
|
2141
2204
|
if (flags.decline)
|
|
2142
2205
|
return declineVault(rootDir, deps);
|
|
2143
2206
|
const prepared = await prepareAdoption(rootDir, flags, deps);
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { CLIError } from './lib/cli-error.js';
|
|
|
13
13
|
import { resolveSuccessExitCode } from './lib/exit-code.js';
|
|
14
14
|
import { parseAddFlags, runAdd } from './commands/add.js';
|
|
15
15
|
import { parseBacklogPushFlags, parseBacklogSyncFlags, runBacklogPush, runBacklogSync, } from './commands/backlog.js';
|
|
16
|
-
import { parseEnvPushFlags, parseEnvWizardFlags, runEnvPush, runEnvWizardApply, runEnvWizardGenVapid, runEnvWizardPlan, runEnvWizardReview, } from './commands/env.js';
|
|
16
|
+
import { parseEnvPushFlags, parseEnvWizardFlags, runEnvGet, runEnvPush, runEnvSet, runEnvWizardApply, runEnvWizardGenVapid, runEnvWizardPlan, runEnvWizardReview, } from './commands/env.js';
|
|
17
17
|
import { parseInviteAdminFlags, runInviteAdmin } from './commands/invite-admin.js';
|
|
18
18
|
import { envWizardFlow } from './tui/env-wizard-flow.js';
|
|
19
19
|
import { parseDoctorFlags, runDoctor, runLauncherDoctor } from './commands/doctor.js';
|
|
@@ -22,6 +22,7 @@ import { parseProvisionFlags, runProvision } from './commands/provision.js';
|
|
|
22
22
|
import { runPruneCommand } from './commands/prune.js';
|
|
23
23
|
import { parsePublishArgs, runPublish } from './commands/publish.js';
|
|
24
24
|
import { parseStatusFlags, runStatus } from './commands/status.js';
|
|
25
|
+
import { parseUsageFlags, runUsage } from './commands/usage.js';
|
|
25
26
|
import { runTicketCommand } from './commands/ticket.js';
|
|
26
27
|
import { parseUnpublishTargets, runUnpublish } from './commands/unpublish.js';
|
|
27
28
|
import { maybeOfferAdopt, parseUpdateFlags, runUpdate } from './commands/update.js';
|
|
@@ -44,6 +45,8 @@ Comandos:
|
|
|
44
45
|
status Reporta la versión instalada vs. la última disponible
|
|
45
46
|
doctor Detecta huérfanos, conflictos y rules sin importar + aviso de seguridad
|
|
46
47
|
(--launcher: verifica el entorno — Node ≥ 22, gh, gh auth, membresía org)
|
|
48
|
+
usage Mide el consumo real de Claude Code de TU máquina (~/.claude/projects/)
|
|
49
|
+
— corre desde cualquier carpeta, no requiere repo
|
|
47
50
|
publish <qué> Publica el entregable (proposal|mockup|both) a proposals.timekast.mx
|
|
48
51
|
unpublish [qué] Baja del hub un entregable publicado (proposal|mockup|both; default both)
|
|
49
52
|
provision Aprovisiona la infra de un derivado (Neon, GitHub, DNS, backlog y su destino de
|
|
@@ -51,7 +54,8 @@ Comandos:
|
|
|
51
54
|
backlog <sub> push: refleja el backlog local en backlog.timekast.mx · sync: status + self-healing
|
|
52
55
|
ticket <sub> push: entrega un factory-ticket de project/factory/ al repo del Factory
|
|
53
56
|
env <sub> push: sube product vars a Vercel (day-2) · wizard: configura las variables
|
|
54
|
-
(en la bóveda si el repo la tiene; sin bóveda, en .env.local)
|
|
57
|
+
(en la bóveda si el repo la tiene; sin bóveda, en .env.local) · get/set:
|
|
58
|
+
lee o cambia un flag público de UN entorno (main|develop)
|
|
55
59
|
invite-admin Crea una invitación de super_admin para un derivado (sin password en texto plano)
|
|
56
60
|
prune <sub> Comparte el repo con colaboradores externos sin la metodología (lo guía /prune):
|
|
57
61
|
prepare: aplica el tier (.gitignore + .timekast/prune.json) y deja de trackear
|
|
@@ -139,6 +143,9 @@ Sobre \`provision\`:
|
|
|
139
143
|
completo (revoca la vigente, sube los 3 secrets de E2E y la
|
|
140
144
|
reescribe en develop:/ci de la bóveda — sin bóveda, en .env.local).
|
|
141
145
|
Para una key revocada por fuera
|
|
146
|
+
--mcp El proyecto lleva servidor MCP: prende MCP_ENABLED y escribe .mcp.json
|
|
147
|
+
(una entrada a producción, en su propio commit). Queda guardado: un
|
|
148
|
+
--resume no necesita repetirlo
|
|
142
149
|
--domain=<dom> Dominio definitivo del cliente (diferible; standalone o inicial)
|
|
143
150
|
--dns=<ruta> Ruta DNS para --domain: org (default) | cliente-cf | registrar
|
|
144
151
|
--verify-timeout=<min> Timeout del sondeo de verificación de Vercel (default 10)
|
|
@@ -165,6 +172,22 @@ Sobre \`status\` y \`doctor\`:
|
|
|
165
172
|
gh auth, membresía del org) — termina con código 1 si falla algún check, para
|
|
166
173
|
que un lanzador GUI / CI / el TUI puedan ramificar sobre el resultado.
|
|
167
174
|
|
|
175
|
+
Sobre \`usage\`:
|
|
176
|
+
Mide el consumo REAL por transcripts (\`usage\` de cada request — sin estimadores),
|
|
177
|
+
barriendo TODOS los directorios de \`~/.claude/projects/\`. Local-only: nada sale
|
|
178
|
+
de tu máquina. Cortes: día × superficie (main loop vs subagentes), por modelo,
|
|
179
|
+
por repo, por agente, tokens por unidad entregada y rondas de revisión. Flags:
|
|
180
|
+
--month=<mes|YYYY-MM> Atajo mensual ("julio" resuelve el más reciente; alias --mes=;
|
|
181
|
+
default: mes en curso)
|
|
182
|
+
--since=YYYY-MM-DD Inicio de ventana (excluyente con --month)
|
|
183
|
+
--until=YYYY-MM-DD Fin de ventana inclusivo (default: hoy)
|
|
184
|
+
--repo=<subcadena> Filtra por repo (match sobre nombre o slug)
|
|
185
|
+
--agent=<tipo> Filtra por tipo de agente (p.ej. imp-issue-executor)
|
|
186
|
+
--anon Anonimiza los repos (repo-01, repo-02…) para reportes compartibles
|
|
187
|
+
--json Salida machine-readable estable (diffeable entre corridas)
|
|
188
|
+
Privacidad: solo agregados — fechas, conteos, tokens, nombres de modelo/agente.
|
|
189
|
+
Nunca contenido de mensajes, paths de archivos del cliente ni código.
|
|
190
|
+
|
|
168
191
|
Sobre \`env\`:
|
|
169
192
|
\`env push\` sube las product vars de \`.env.local\` a Vercel (production+preview),
|
|
170
193
|
sin tocar las provision-owned. En un repo con bóveda (bloque \`vault\`) no escribe:
|
|
@@ -177,6 +200,16 @@ Sobre \`env\`:
|
|
|
177
200
|
\`--push\` no aplica: los despliegues las reciben de la bóveda)
|
|
178
201
|
--review [--json] Resumen enmascarado (con bóveda: del entorno \`local\`)
|
|
179
202
|
--gen-vapid Genera las llaves VAPID (con bóveda, se guardan ahí; la privada no se imprime)
|
|
203
|
+
\`env get <KEY> --env main|develop\` imprime en stdout SOLO el valor de una clave pública
|
|
204
|
+
(public-env-keys.json; un secreto se rechaza antes de consultar nada). \`env set <KEY>
|
|
205
|
+
<true|false> --env main|develop\` cambia SOLO QUESTIONS_ENABLED o MCP_ENABLED, y sólo a
|
|
206
|
+
true/false, en ese entorno y nada más. \`--env\` es obligatorio (sin default). Con bóveda
|
|
207
|
+
operan main:/ o develop:/ (el sync lleva el valor; aplica en el siguiente deploy); sin
|
|
208
|
+
bóveda, Vercel (main → production, develop → preview; el otro target conserva su valor);
|
|
209
|
+
Railway sin bóveda se rechaza (\`factory vault adopt\`). Sin prompts. Salida:
|
|
210
|
+
0 ok · 1 no se pudo leer/escribir (o un CLI viejo que no conoce get/set) ·
|
|
211
|
+
20 la clave no existe en ese entorno · 21 clave (o valor de set) rechazado ·
|
|
212
|
+
22 (get) en ese entorno la clave es una referencia a otro, no un valor propio
|
|
180
213
|
|
|
181
214
|
Sobre \`backlog\`:
|
|
182
215
|
\`backlog push\` refleja el backlog local (\`project/backlog/*\`) en
|
|
@@ -336,6 +369,13 @@ async function main(argv) {
|
|
|
336
369
|
}
|
|
337
370
|
return;
|
|
338
371
|
}
|
|
372
|
+
case 'usage': {
|
|
373
|
+
// 🔴 No repo preflight — ever. The data source is `~/.claude/projects/`
|
|
374
|
+
// (the developer's home), so this command runs from ANY folder: copying
|
|
375
|
+
// the update/status/doctor preflight here breaks the whole use case.
|
|
376
|
+
runUsage(parseUsageFlags(rest));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
339
379
|
case 'provision': {
|
|
340
380
|
await runProvision(parseProvisionFlags(rest));
|
|
341
381
|
return;
|
|
@@ -409,8 +449,17 @@ async function main(argv) {
|
|
|
409
449
|
await envWizardFlow();
|
|
410
450
|
return;
|
|
411
451
|
}
|
|
452
|
+
// `factory env get|set` (PVMCP-003) — one public flag of one deploy environment.
|
|
453
|
+
if (sub === 'get') {
|
|
454
|
+
await runEnvGet(rest.slice(1));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (sub === 'set') {
|
|
458
|
+
await runEnvSet(rest.slice(1));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
412
461
|
throw new CLIError(`Subcomando de env desconocido: \`${sub ?? '(ninguno)'}\`. ` +
|
|
413
|
-
'Uso: `factory env push` o `factory env
|
|
462
|
+
'Uso: `factory env push`, `factory env wizard`, `factory env get` o `factory env set`.');
|
|
414
463
|
}
|
|
415
464
|
case 'vault': {
|
|
416
465
|
// `factory vault adopt [--apply]` (ADOPT-001) — move a repo born before the vault into it.
|
|
@@ -183,7 +183,7 @@ function vaultFailureReason(error, link) {
|
|
|
183
183
|
}
|
|
184
184
|
if (error instanceof RailNotInstalledError) {
|
|
185
185
|
return ('El CLI de la bóveda (`infisical`) no está instalado; instálalo con ' +
|
|
186
|
-
|
|
186
|
+
`${INFISICAL_INSTALL_HINT} y entra con \`${loginCommand()}\`.`);
|
|
187
187
|
}
|
|
188
188
|
if (error instanceof VaultNoAccessError) {
|
|
189
189
|
return (`Tu sesión de la bóveda no tiene acceso al proyecto \`${link.projectId}\` de este repo ` +
|
package/dist/lib/claude-paths.js
CHANGED
|
@@ -61,8 +61,14 @@ function dropGitignored(rootDir, paths) {
|
|
|
61
61
|
return paths.filter((p) => !ignored.has(p));
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
64
|
+
* The project's own Claude Code hooks (`fx-extension-points`): dev-owned, never shipped, so
|
|
65
|
+
* never an orphan nor an ambiguous kit file.
|
|
66
|
+
*/
|
|
67
|
+
export const PROJECT_HOOKS_DIR = '.claude/hooks/project/';
|
|
68
|
+
/**
|
|
69
|
+
* Repo-relative POSIX paths of NON-gitignored files under `.claude/`, minus the project's own
|
|
70
|
+
* hooks ({@link PROJECT_HOOKS_DIR}).
|
|
65
71
|
*/
|
|
66
72
|
export function collectClaudePaths(rootDir) {
|
|
67
|
-
return dropGitignored(rootDir, walkClaude(rootDir));
|
|
73
|
+
return dropGitignored(rootDir, walkClaude(rootDir)).filter((p) => !p.startsWith(PROJECT_HOOKS_DIR));
|
|
68
74
|
}
|