@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
package/dist/lib/vercel-env.js
CHANGED
|
@@ -183,6 +183,27 @@ export async function linkGitRepo(projectId, token, teamId, repo, deps = {}) {
|
|
|
183
183
|
throw error;
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* The project's current git connection, or `null` when it has none.
|
|
188
|
+
*
|
|
189
|
+
* @throws {VercelApiError} on any non-2xx.
|
|
190
|
+
*/
|
|
191
|
+
export async function getGitLink(projectId, token, teamId, deps = {}) {
|
|
192
|
+
const data = (await vercelApi('GET', `/v9/projects/${encodeURIComponent(projectId)}`, token, teamId, deps));
|
|
193
|
+
const link = data.link;
|
|
194
|
+
if (!link || link.type !== 'github' || typeof link.repoId !== 'number')
|
|
195
|
+
return null;
|
|
196
|
+
return { repo: `${link.org ?? ''}/${link.repo ?? ''}`, repoId: link.repoId };
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Disconnect the project from its git repo (`DELETE /v9/projects/{id}/link`). The project,
|
|
200
|
+
* its env vars, domains and deployments stay; only the repo binding goes.
|
|
201
|
+
*
|
|
202
|
+
* @throws {VercelApiError} on any non-2xx.
|
|
203
|
+
*/
|
|
204
|
+
export async function unlinkGitRepo(projectId, token, teamId, deps = {}) {
|
|
205
|
+
await vercelApi('DELETE', `/v9/projects/${encodeURIComponent(projectId)}/link`, token, teamId, deps);
|
|
206
|
+
}
|
|
186
207
|
/**
|
|
187
208
|
* Apply the project SETTINGS via `PATCH /v9/projects/{id}?teamId=` — the kit's
|
|
188
209
|
* contract that `POST /v9/projects` refuses at create time:
|
|
@@ -828,6 +849,14 @@ export function buildEnvPlan(input) {
|
|
|
828
849
|
{ key: 'NEXT_PUBLIC_APP_URL', value: `https://${input.prodHost}`, target: ['production'] },
|
|
829
850
|
{ key: 'NEXT_PUBLIC_APP_URL', value: `https://${input.previewHost}`, target: ['preview'] },
|
|
830
851
|
];
|
|
852
|
+
// Per-env feature flags (PVMCP-001) — one record PER TARGET, like NEXT_PUBLIC_APP_ENV, so a later
|
|
853
|
+
// change in one target (`/deploy` turns questions off in production) never drags the other.
|
|
854
|
+
// Without MCP, `MCP_ENABLED` is not written at all: absent means off.
|
|
855
|
+
const flags = ['QUESTIONS_ENABLED', ...(input.mcp ? ['MCP_ENABLED'] : [])];
|
|
856
|
+
for (const key of flags) {
|
|
857
|
+
plan.push({ key, value: 'true', target: ['production'] });
|
|
858
|
+
plan.push({ key, value: 'true', target: ['preview'] });
|
|
859
|
+
}
|
|
831
860
|
for (const [key, value] of Object.entries(input.shared ?? {})) {
|
|
832
861
|
if (!value)
|
|
833
862
|
continue; // skip empty shared values
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "what's new" block printed after every applied `factory update`.
|
|
3
|
+
*
|
|
4
|
+
* WHY IT EXISTS. A version number tells the dev that something changed, not what. The kit's
|
|
5
|
+
* changelog already travels with the brain (`.claude/docs/CHANGELOG.md`, tracked), but nobody
|
|
6
|
+
* opens it after an update: the dev reads "✔ v14.0.0" and moves on without knowing the kit
|
|
7
|
+
* now has a client-questions section or a new rule they are expected to follow. This block
|
|
8
|
+
* puts the headline of every release they just crossed in front of them, before the retrofit
|
|
9
|
+
* reminder — first what changed, then what still needs hands.
|
|
10
|
+
*
|
|
11
|
+
* Read from disk AFTER the apply, like the retrofit reminder: the changelog that just arrived
|
|
12
|
+
* is the source, never a list baked into this binary. Headless too — an agent running the
|
|
13
|
+
* update is the reader who has to relay it to the dev.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { compareVersions } from './retrofit-reminder.js';
|
|
18
|
+
/** Where the kit's changelog lives inside the derivative. */
|
|
19
|
+
export const KIT_CHANGELOG = '.claude/docs/CHANGELOG.md';
|
|
20
|
+
/** Releases shown in full; older ones crossed by the same update collapse into one line. */
|
|
21
|
+
export const MAX_RELEASES = 5;
|
|
22
|
+
/** Headlines shown per release; the rest collapse into "+N más". */
|
|
23
|
+
export const MAX_ITEMS_PER_RELEASE = 8;
|
|
24
|
+
/** How much of a headline survives — enough to understand, short enough to scan. */
|
|
25
|
+
const ITEM_MAX = 110;
|
|
26
|
+
/** `## [13.6.0] - 2026-10-14 — Title` → version + title. `[Unreleased]` and junk → null. */
|
|
27
|
+
function parseHeading(line) {
|
|
28
|
+
const match = line.match(/^##\s+\[(\d+\.\d+\.\d+)\]\s*(.*)$/);
|
|
29
|
+
if (!match)
|
|
30
|
+
return null;
|
|
31
|
+
const rest = match[2];
|
|
32
|
+
const dash = rest.indexOf('—');
|
|
33
|
+
const title = dash === -1 ? null : rest.slice(dash + 1).trim() || null;
|
|
34
|
+
return { version: match[1], title };
|
|
35
|
+
}
|
|
36
|
+
/** Markdown inline marks out: the block is plain terminal text. */
|
|
37
|
+
function plain(text) {
|
|
38
|
+
return text
|
|
39
|
+
.replace(/\*\*/g, '')
|
|
40
|
+
.replace(/`/g, '')
|
|
41
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
|
42
|
+
.replace(/\s+/g, ' ')
|
|
43
|
+
.trim();
|
|
44
|
+
}
|
|
45
|
+
/** A bullet's headline: its leading bold span, else its first sentence. Capped. */
|
|
46
|
+
export function headline(bullet) {
|
|
47
|
+
const body = bullet.replace(/^-\s+/, '');
|
|
48
|
+
const bold = body.match(/^\*\*(.+?)\*\*/);
|
|
49
|
+
let text = plain(bold ? bold[1] : (body.match(/^(.+?[.!?])(\s|$)/)?.[1] ?? body));
|
|
50
|
+
text = text.replace(/[.:]$/, '');
|
|
51
|
+
return text.length > ITEM_MAX ? `${text.slice(0, ITEM_MAX - 1)}…` : text;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Every numbered release in the changelog, newest first, with its top-level bullets.
|
|
55
|
+
* Sub-bullets, `###` group headings and prose are skipped: the headline is the bullet itself.
|
|
56
|
+
*/
|
|
57
|
+
export function parseChangelog(markdown) {
|
|
58
|
+
const releases = [];
|
|
59
|
+
let current = null;
|
|
60
|
+
for (const line of markdown.split('\n')) {
|
|
61
|
+
if (line.startsWith('## ')) {
|
|
62
|
+
const heading = parseHeading(line);
|
|
63
|
+
current = heading ? { ...heading, items: [] } : null;
|
|
64
|
+
if (current)
|
|
65
|
+
releases.push(current);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (current && /^-\s+\S/.test(line))
|
|
69
|
+
current.items.push(headline(line));
|
|
70
|
+
}
|
|
71
|
+
return releases.sort((a, b) => compareVersions(b.version, a.version));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The releases this update crossed: newer than `fromVersion`. Without a `fromVersion` (first
|
|
75
|
+
* install) there is nothing to compare against and the block stays silent — "everything is
|
|
76
|
+
* new" helps nobody. A legacy `0.0.0` lockfile crosses every release, and the cap handles it.
|
|
77
|
+
*/
|
|
78
|
+
export function releasesSince(releases, fromVersion) {
|
|
79
|
+
if (!fromVersion)
|
|
80
|
+
return [];
|
|
81
|
+
return releases.filter((r) => compareVersions(r.version, fromVersion) > 0);
|
|
82
|
+
}
|
|
83
|
+
/** Render the block, or `null` when the update crossed no documented release. */
|
|
84
|
+
export function renderWhatsNew(crossed) {
|
|
85
|
+
if (crossed.length === 0)
|
|
86
|
+
return null;
|
|
87
|
+
const rule = '═'.repeat(74);
|
|
88
|
+
const lines = ['', rule, ' ✨ NOVEDADES DEL KIT EN ESTE UPDATE', rule, ''];
|
|
89
|
+
for (const release of crossed.slice(0, MAX_RELEASES)) {
|
|
90
|
+
lines.push(` ▸ v${release.version}${release.title ? ` — ${release.title}` : ''}`, '');
|
|
91
|
+
for (const item of release.items.slice(0, MAX_ITEMS_PER_RELEASE))
|
|
92
|
+
lines.push(` · ${item}`);
|
|
93
|
+
const extra = release.items.length - MAX_ITEMS_PER_RELEASE;
|
|
94
|
+
if (extra > 0)
|
|
95
|
+
lines.push(` · +${extra} más`);
|
|
96
|
+
lines.push('');
|
|
97
|
+
}
|
|
98
|
+
const older = crossed.length - MAX_RELEASES;
|
|
99
|
+
if (older > 0) {
|
|
100
|
+
const oldest = crossed[crossed.length - 1].version;
|
|
101
|
+
lines.push(` ▸ Y ${older} versión(es) anteriores, hasta v${oldest}.`, '');
|
|
102
|
+
}
|
|
103
|
+
lines.push(` El detalle de cada punto está en ${KIT_CHANGELOG}.`, ' Si trabajas con un agente, pídele que te explique qué cambia para tu proyecto.', rule, '');
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Print the block. A missing or unreadable changelog prints nothing — a `core` derivative may
|
|
108
|
+
* not carry it, and a notice must never break the update that already succeeded.
|
|
109
|
+
*/
|
|
110
|
+
export function printWhatsNew(opts) {
|
|
111
|
+
const readFile = opts.readFile ?? ((file) => readFileSync(file, 'utf8'));
|
|
112
|
+
const log = opts.log ?? ((m) => console.log(m));
|
|
113
|
+
let markdown;
|
|
114
|
+
try {
|
|
115
|
+
markdown = readFile(join(opts.rootDir, KIT_CHANGELOG));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const rendered = renderWhatsNew(releasesSince(parseChangelog(markdown), opts.fromVersion));
|
|
121
|
+
if (rendered)
|
|
122
|
+
log(rendered);
|
|
123
|
+
}
|