@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/commands/prune.js
CHANGED
|
@@ -18,12 +18,15 @@ import { GITIGNORE_FILE } from '../lib/constants.js';
|
|
|
18
18
|
import { readValueFlag } from '../lib/flags.js';
|
|
19
19
|
import { commitWithPrivateIndex, trackedFiles } from '../lib/prune-git.js';
|
|
20
20
|
import { checkPr, REJECTED_PACKAGE_FIELDS } from '../lib/prune-pr-check.js';
|
|
21
|
+
import { BRANCH_DEPLOY_RULE, VERCEL_JSON, ensureBranchDeployRule, hasBranchDeployRule, pruneVercelRelink, resolveVercelProject, } from '../lib/prune-vercel.js';
|
|
22
|
+
import { readVercelToken } from '../lib/provision-preflight.js';
|
|
21
23
|
import { applyTierFiles, defaultPruneDeps, originRepo, pruneAudit, pruneCutover, pruneFinish, pruneInvite, } from '../lib/prune-remote.js';
|
|
22
24
|
import { DEFAULT_PRUNE_TIER, PRUNE_MARKER_PATH, getTier, isRestricted, readPruneMarker, readPruneTiers, } from '../lib/prune-tiers.js';
|
|
23
25
|
import { detectRepo } from '../lib/repo-detection.js';
|
|
24
26
|
const PRUNE_USAGE = 'Uso: `factory prune prepare [--tier=collab] [--skip-verify]` · ' +
|
|
25
27
|
'`factory prune cutover [--confirm] [--discard-develop] [--resume] [--repo=owner/nombre]` · ' +
|
|
26
|
-
'`factory prune
|
|
28
|
+
'`factory prune vercel [--confirm] [--repo=owner/nombre] [--project=prj_…] [--team=team_…]` · ' +
|
|
29
|
+
'`factory prune invite <usuario-github> [--read] [--repo=owner/nombre]` · ' +
|
|
27
30
|
'`factory prune finish [--repo=owner/nombre]` · `factory prune audit [--repo=owner/nombre]` · ' +
|
|
28
31
|
'`factory prune check-pr <número> [--repo=owner/nombre] [--json]`.';
|
|
29
32
|
export function parsePrunePrepareFlags(argv) {
|
|
@@ -48,25 +51,29 @@ export async function prunePrepare(rootDir, flags) {
|
|
|
48
51
|
const tier = getTier(readPruneTiers(rootDir), flags.tier);
|
|
49
52
|
// The two files this command owns must not carry someone's uncommitted work: the commit
|
|
50
53
|
// below takes them whole.
|
|
51
|
-
const dirty = await execa('git', ['status', '--porcelain', '--', GITIGNORE_FILE, PRUNE_MARKER_PATH], { cwd: rootDir, reject: false });
|
|
54
|
+
const dirty = await execa('git', ['status', '--porcelain', '--', GITIGNORE_FILE, PRUNE_MARKER_PATH, VERCEL_JSON], { cwd: rootDir, reject: false });
|
|
52
55
|
if (dirty.stdout.trim() !== '') {
|
|
53
|
-
throw new CLIError(`\`${GITIGNORE_FILE}\` o \`${
|
|
56
|
+
throw new CLIError(`\`${GITIGNORE_FILE}\`, \`${PRUNE_MARKER_PATH}\` o \`${VERCEL_JSON}\` tienen cambios sin commitear. ` +
|
|
54
57
|
'Commitéalos o descártalos y vuelve a correr `factory prune prepare`.');
|
|
55
58
|
}
|
|
56
59
|
// 1. The block and the switch — `at` survives a re-run with the same tier, so a re-run
|
|
57
60
|
// changes nothing.
|
|
58
61
|
const prior = readPruneMarker(rootDir);
|
|
59
62
|
const legacyBlockRemoved = applyTierFiles(rootDir, flags.tier, tier, prior?.tier === flags.tier ? prior.at : new Date().toISOString());
|
|
60
|
-
// 2.
|
|
63
|
+
// 2. On Vercel, only `main` and `develop` deploy on push: a collaborator's branch would
|
|
64
|
+
// start a deployment Vercel blocks (author without a seat).
|
|
65
|
+
const branchDeployRuleAdded = ensureBranchDeployRule(rootDir) === 'added';
|
|
66
|
+
// 3. Untrack what the tier restricts, and commit it with the block, the switch and the rule.
|
|
61
67
|
const untracked = (await trackedFiles(rootDir)).filter((p) => isRestricted(tier, p));
|
|
62
68
|
const commit = await commitWithPrivateIndex(rootDir, {
|
|
63
69
|
remove: untracked,
|
|
64
|
-
add: [GITIGNORE_FILE, PRUNE_MARKER_PATH],
|
|
70
|
+
add: [GITIGNORE_FILE, PRUNE_MARKER_PATH, ...(branchDeployRuleAdded ? [VERCEL_JSON] : [])],
|
|
65
71
|
message: `chore(prune): keep the methodology local (tier ${flags.tier})`,
|
|
66
72
|
});
|
|
67
73
|
return {
|
|
68
74
|
tier: flags.tier,
|
|
69
75
|
untracked,
|
|
76
|
+
branchDeployRuleAdded,
|
|
70
77
|
legacyBlockRemoved,
|
|
71
78
|
...(commit.committed ? { commit: commit.sha } : {}),
|
|
72
79
|
};
|
|
@@ -94,6 +101,10 @@ async function runPrunePrepare(flags) {
|
|
|
94
101
|
`Dejé de trackear ${result.untracked.length} archivo(s); siguen en tu disco:`);
|
|
95
102
|
for (const line of summarizePaths(result.untracked))
|
|
96
103
|
console.log(line);
|
|
104
|
+
if (result.branchDeployRuleAdded) {
|
|
105
|
+
console.log(`✔ \`${VERCEL_JSON}\`: sólo \`main\` y \`develop\` despliegan con cada push — las ramas de ` +
|
|
106
|
+
'los colaboradores no intentan un deploy que Vercel bloquearía.');
|
|
107
|
+
}
|
|
97
108
|
}
|
|
98
109
|
if (flags.skipVerify)
|
|
99
110
|
return;
|
|
@@ -155,7 +166,7 @@ async function runPruneCutover(flags, deps) {
|
|
|
155
166
|
for (const line of result.notMigrated)
|
|
156
167
|
console.log(` • ${line}`);
|
|
157
168
|
}
|
|
158
|
-
console.log('\nSiguiente: reconecta el proyecto de Vercel al repo nuevo
|
|
169
|
+
console.log('\nSiguiente: reconecta el proyecto de Vercel al repo nuevo (`factory prune vercel`), ' +
|
|
159
170
|
'pide a los socios volver a clonar, y corre `factory prune finish` cuando Production despliegue.');
|
|
160
171
|
}
|
|
161
172
|
function printAudit(repo, audit) {
|
|
@@ -188,7 +199,7 @@ async function repoFrom(argv) {
|
|
|
188
199
|
return readValueFlag(argv, 'repo') ?? (await originRepo(requireRepoRoot()));
|
|
189
200
|
}
|
|
190
201
|
/** `factory prune <sub> [...]` — dispatch lives here so every branch is testable. */
|
|
191
|
-
export async function runPruneCommand(args, deps = defaultPruneDeps) {
|
|
202
|
+
export async function runPruneCommand(args, deps = defaultPruneDeps, vercel = {}) {
|
|
192
203
|
const sub = args[0];
|
|
193
204
|
const rest = args.slice(1);
|
|
194
205
|
switch (sub) {
|
|
@@ -203,9 +214,48 @@ export async function runPruneCommand(args, deps = defaultPruneDeps) {
|
|
|
203
214
|
if (!user)
|
|
204
215
|
throw new CLIError(`Falta el usuario de GitHub del colaborador. ${PRUNE_USAGE}`);
|
|
205
216
|
const repo = await repoFrom(rest);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
217
|
+
const read = rest.includes('--read');
|
|
218
|
+
await pruneInvite(repo, user, deps, read ? 'pull' : 'push');
|
|
219
|
+
console.log(read
|
|
220
|
+
? `✔ Invité a \`${user}\` a \`${repo}\` con lectura. Tiene que aceptar la invitación; ` +
|
|
221
|
+
'después forkea y abre PRs a la rama de trabajo.'
|
|
222
|
+
: `✔ Invité a \`${user}\` a \`${repo}\` con escritura. Tiene que aceptar la invitación; ` +
|
|
223
|
+
'después trabaja en ramas del repo y abre PRs a la rama de trabajo. El hook `pre-push` ' +
|
|
224
|
+
'del repo le rechaza el push a `main` y `develop` (no cubre la web de GitHub).');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
case 'vercel': {
|
|
228
|
+
const repo = await repoFrom(rest);
|
|
229
|
+
const { projectId, teamId } = resolveVercelProject(requireRepoRoot(), {
|
|
230
|
+
project: readValueFlag(rest, 'project'),
|
|
231
|
+
team: readValueFlag(rest, 'team'),
|
|
232
|
+
});
|
|
233
|
+
const token = await (vercel.token ?? (() => readVercelToken()))();
|
|
234
|
+
const result = await pruneVercelRelink({ repo, projectId, teamId, token, confirm: rest.includes('--confirm') }, { gh: deps.gh, ...(vercel.fetchImpl ? { fetchImpl: vercel.fetchImpl } : {}) });
|
|
235
|
+
if (hasBranchDeployRule(requireRepoRoot()) === false) {
|
|
236
|
+
console.warn(`⚠ \`${VERCEL_JSON}\` no limita los deploys por push: cada rama de un colaborador intentará ` +
|
|
237
|
+
'un deploy que Vercel bloquea (autor sin asiento). Agrega y commitea:\n' +
|
|
238
|
+
` "git": { "deploymentEnabled": ${JSON.stringify(BRANCH_DEPLOY_RULE)} }`);
|
|
239
|
+
}
|
|
240
|
+
if (result.status === 'already-linked') {
|
|
241
|
+
console.log(`✔ El proyecto de Vercel ya despliega desde \`${result.link.repo}\`. Nada que hacer.`);
|
|
242
|
+
}
|
|
243
|
+
else if (result.status === 'plan') {
|
|
244
|
+
console.log([
|
|
245
|
+
`Plan: el proyecto de Vercel (\`${projectId}\`) despliega desde ` +
|
|
246
|
+
`${result.from ? `\`${result.from.repo}\`` : 'ningún repo'}.`,
|
|
247
|
+
` 1. Desconectarlo${result.from ? ` de \`${result.from.repo}\`` : ''} (variables, dominios y deploys se quedan).`,
|
|
248
|
+
` 2. Conectarlo a \`${result.to}\` y releer el vínculo para confirmarlo.`,
|
|
249
|
+
' No dispara ningún deploy.',
|
|
250
|
+
'',
|
|
251
|
+
'Para ejecutarlo: `factory prune vercel --confirm`.',
|
|
252
|
+
].join('\n'));
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
console.log(`✔ El proyecto de Vercel ahora despliega desde \`${result.to.repo}\`` +
|
|
256
|
+
`${result.from ? ` (antes: \`${result.from.repo}\`)` : ''}. ` +
|
|
257
|
+
'El primer deploy sale con el siguiente push a `main` (p. ej. un commit vacío).');
|
|
258
|
+
}
|
|
209
259
|
return;
|
|
210
260
|
}
|
|
211
261
|
case 'finish': {
|
package/dist/commands/update.js
CHANGED
|
@@ -23,8 +23,10 @@
|
|
|
23
23
|
* Auto-register (no lockfile, pre-CLI repo, design §6.3): path-match — repo
|
|
24
24
|
* paths matching the manifest are kit-owned (overwrite); `.claude/` paths absent
|
|
25
25
|
* from the manifest are logged "ambiguous" and NOT deleted on the first sync;
|
|
26
|
-
* an existing `CLAUDE.md` is preserved (write-if-absent)
|
|
27
|
-
* the
|
|
26
|
+
* an existing `CLAUDE.md` is preserved (write-if-absent), and so is any other kit
|
|
27
|
+
* file outside the brain the repo already has with its own content (the kit's copy
|
|
28
|
+
* goes to `<path>.kit-example`). The dev is warned + sees the auto-detected profile
|
|
29
|
+
* before proceeding.
|
|
28
30
|
*
|
|
29
31
|
* `--verify`: compares disk vs lockfile and reports drift WITHOUT modifying
|
|
30
32
|
* anything. `--resume`: re-applies a persisted mid-flight state without
|
|
@@ -44,14 +46,18 @@ import { collectClaudePaths } from '../lib/claude-paths.js';
|
|
|
44
46
|
import { CLIError } from '../lib/cli-error.js';
|
|
45
47
|
import { CLAUDE_MD_FILE, GITATTRIBUTES_FILE, GITIGNORE_FILE, LOCKFILE_FILE, PRETTIERIGNORE_FILE, PROFILES, TIMEKAST_DIR, } from '../lib/constants.js';
|
|
46
48
|
import { decideCommit } from '../lib/commit-decision.js';
|
|
47
|
-
import {
|
|
49
|
+
import { syncDerivedDotfiles } from '../lib/derived-dotfiles.js';
|
|
50
|
+
import { partitionInitialWrites, refreshKitExamples, reportKeptRepoFiles, writeKitExamples, } from '../lib/initial-writes.js';
|
|
51
|
+
import { offerKitDeps, readKitDeps } from '../lib/kit-deps.js';
|
|
48
52
|
import { diffLockfiles, hasLockfile, normalizeThenHash, planAutoRegister, readLockfile, writeLockfile, } from '../lib/lockfile.js';
|
|
53
|
+
import { effectiveSettingsHash, KIT_SETTINGS_PATH, LEGACY_KIT_SETTINGS_KEYS, mergeProjectSettingsKeys, } from '../lib/claude-settings.js';
|
|
49
54
|
import { detectProfile, maintainKitScripts, setAgentKitVersion } from '../lib/package-json.js';
|
|
50
55
|
import { runPreflight } from '../lib/preflight.js';
|
|
51
56
|
import { printProvisionPointer } from '../lib/provision-pointer.js';
|
|
52
57
|
import { planPruneMaintenance } from '../lib/prune-update.js';
|
|
53
58
|
import { isRestricted, PRUNE_MARKER_PATH } from '../lib/prune-tiers.js';
|
|
54
59
|
import { printRetrofitReminder } from '../lib/retrofit-reminder.js';
|
|
60
|
+
import { printWhatsNew } from '../lib/whats-new.js';
|
|
55
61
|
import { hasProvisionState } from '../lib/provision-state.js';
|
|
56
62
|
import { loginCommand } from '../lib/rail.js';
|
|
57
63
|
import { resolveBrainRoot } from '../lib/repo-detection.js';
|
|
@@ -100,16 +106,52 @@ async function acquireFromBeta(profile) {
|
|
|
100
106
|
* Hash the disk content (normalized) of every path in `paths` that exists.
|
|
101
107
|
* A missing file is simply absent from the returned map.
|
|
102
108
|
*/
|
|
103
|
-
function hashDiskPaths(rootDir, paths) {
|
|
109
|
+
function hashDiskPaths(rootDir, paths, settingsEntries = []) {
|
|
104
110
|
const map = new Map();
|
|
105
111
|
for (const rel of paths) {
|
|
106
112
|
const abs = path.join(rootDir, rel);
|
|
107
113
|
if (existsSync(abs) && statSync(abs).isFile()) {
|
|
108
|
-
|
|
114
|
+
const raw = readFileSync(abs, 'utf8');
|
|
115
|
+
// settings.json: only the kit's keys count, by content (the project's are its own).
|
|
116
|
+
map.set(rel, rel === KIT_SETTINGS_PATH
|
|
117
|
+
? effectiveSettingsHash(raw, settingsEntries)
|
|
118
|
+
: normalizeThenHash(raw));
|
|
109
119
|
}
|
|
110
120
|
}
|
|
111
121
|
return map;
|
|
112
122
|
}
|
|
123
|
+
/** The `settings.json` the project had before this apply: the run's backup, else the disk. */
|
|
124
|
+
function readPreviousSettings(rootDir, backupDir) {
|
|
125
|
+
for (const abs of [
|
|
126
|
+
path.join(backupDir, KIT_SETTINGS_PATH),
|
|
127
|
+
path.join(rootDir, KIT_SETTINGS_PATH),
|
|
128
|
+
]) {
|
|
129
|
+
if (existsSync(abs) && statSync(abs).isFile())
|
|
130
|
+
return readFileSync(abs, 'utf8');
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* After the kit's `settings.json` was written, put the project's own keys back (every top-level
|
|
136
|
+
* key the kit does not declare) and say so. No-op when the kit's file was not written.
|
|
137
|
+
*/
|
|
138
|
+
function restoreProjectSettingsKeys(rootDir, written, previous, previousKitKeys) {
|
|
139
|
+
if (previous === null || !written.includes(KIT_SETTINGS_PATH))
|
|
140
|
+
return;
|
|
141
|
+
const abs = path.join(rootDir, KIT_SETTINGS_PATH);
|
|
142
|
+
const merged = mergeProjectSettingsKeys(readFileSync(abs, 'utf8'), previous, previousKitKeys);
|
|
143
|
+
if (merged.content !== null) {
|
|
144
|
+
writeFileSync(abs, merged.content, 'utf8');
|
|
145
|
+
console.log(` ✔ ${KIT_SETTINGS_PATH}: conservé tus llaves (${merged.kept.join(', ')})`);
|
|
146
|
+
}
|
|
147
|
+
for (const key of merged.replaced) {
|
|
148
|
+
console.warn(` ⚠ ${KIT_SETTINGS_PATH}: \`${key}\` ahora la declara el kit — tu valor se reemplazó por el suyo.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** The kit's keys recorded for `settings.json` in a lockfile (the legacy set when absent). */
|
|
152
|
+
function kitSettingsKeysOf(lock) {
|
|
153
|
+
return lock?.files.find((f) => f.path === KIT_SETTINGS_PATH)?.keys ?? LEGACY_KIT_SETTINGS_KEYS;
|
|
154
|
+
}
|
|
113
155
|
/** Unchanged lines kept around each change in the `[3] Ver diff` view. */
|
|
114
156
|
const DIFF_CONTEXT_LINES = 3;
|
|
115
157
|
/**
|
|
@@ -301,7 +343,7 @@ function runVerify(rootDir) {
|
|
|
301
343
|
throw new CLIError('No hay lockfile que verificar. Corre `factory update` primero.');
|
|
302
344
|
}
|
|
303
345
|
const lock = readLockfile(rootDir);
|
|
304
|
-
const disk = hashDiskPaths(rootDir, lock.files.map((f) => f.path));
|
|
346
|
+
const disk = hashDiskPaths(rootDir, lock.files.map((f) => f.path), [lock.files.find((f) => f.path === KIT_SETTINGS_PATH)]);
|
|
305
347
|
const missing = [];
|
|
306
348
|
const drifted = [];
|
|
307
349
|
for (const entry of lock.files) {
|
|
@@ -465,20 +507,32 @@ async function runResume(rootDir, flags) {
|
|
|
465
507
|
const claudeMd = priorLock
|
|
466
508
|
? assessClaudeMd(rootDir, priorLock)
|
|
467
509
|
: undefined;
|
|
510
|
+
const previousSettings = readPreviousSettings(rootDir, state.backupDir);
|
|
468
511
|
applyPlan(rootDir, state.stagedDir, state.plan, state.backupDir);
|
|
512
|
+
restoreProjectSettingsKeys(rootDir, state.plan.writes, previousSettings, kitSettingsKeysOf(priorLock));
|
|
469
513
|
writeLockfile(rootDir, newLock);
|
|
470
514
|
clearUpdateState(rootDir);
|
|
471
515
|
const claudeMdInserted = claudeMd ? applyClaudeMdRetrofit(rootDir, claudeMd) : false;
|
|
516
|
+
const kitExamples = refreshKitExamples(rootDir, state.stagedDir, newLock.files.map((f) => f.path));
|
|
472
517
|
// Maintain dotfiles BEFORE removing the staged dir (the .gitattributes source).
|
|
473
|
-
const pkgMutations = maintainDerivedDotfiles(rootDir, state.stagedDir, newLock
|
|
518
|
+
const pkgMutations = maintainDerivedDotfiles(rootDir, state.stagedDir, newLock);
|
|
474
519
|
const prune = await planPruneMaintenance(rootDir, state.stagedDir, newLock.files.map((f) => f.path));
|
|
475
|
-
await maybeCommitBrain(rootDir, newLock.version, [
|
|
520
|
+
await maybeCommitBrain(rootDir, newLock.version, [
|
|
521
|
+
...state.plan.writes,
|
|
522
|
+
...state.plan.deletes,
|
|
523
|
+
...kitExamples,
|
|
524
|
+
...(claudeMdInserted ? [CLAUDE_MD_FILE] : []),
|
|
525
|
+
], flags, prune);
|
|
526
|
+
const kitDeps = readKitDeps(state.stagedDir);
|
|
476
527
|
rmSync(state.stagedDir, { recursive: true, force: true });
|
|
477
528
|
// A resumed update maintains package.json exactly like a normal one, so it owes the dev the
|
|
478
529
|
// same account of what it moved there (`BND-004`).
|
|
479
530
|
reportPkgMutations(pkgMutations);
|
|
480
531
|
if (claudeMd)
|
|
481
532
|
reportClaudeMd(claudeMd, 'applied', claudeMdInserted);
|
|
533
|
+
// AFTER the brain commit: an install changes package.json and the package lockfile, which
|
|
534
|
+
// stay out of that commit (Factory#78).
|
|
535
|
+
await offerKitDeps(rootDir, kitDeps, { assumeYes: flags.yes });
|
|
482
536
|
console.log('✔ `update` retomado y completado.');
|
|
483
537
|
// A resumed update that succeeds completed the setup → show the end-pointer
|
|
484
538
|
// (PVSKILL-002 §8 edge: `--resume` SÍ lo muestra). Suppressed only in headless
|
|
@@ -522,10 +576,9 @@ function warnOnScriptConflict(action, _pkgPath) {
|
|
|
522
576
|
* the managed blocks. Runs BEFORE any brain commit, so the blocks land before a
|
|
523
577
|
* pre-commit hook could reformat the freshly-installed kit files.
|
|
524
578
|
*/
|
|
525
|
-
function maintainDerivedDotfiles(rootDir, stagedDir,
|
|
526
|
-
const pkgMutations = maintainDerivedPkg(rootDir,
|
|
527
|
-
|
|
528
|
-
syncDerivedPrettierignore(rootDir, stagedDir);
|
|
579
|
+
function maintainDerivedDotfiles(rootDir, stagedDir, manifest) {
|
|
580
|
+
const pkgMutations = maintainDerivedPkg(rootDir, manifest.version);
|
|
581
|
+
syncDerivedDotfiles(rootDir, stagedDir, manifest);
|
|
529
582
|
return pkgMutations;
|
|
530
583
|
}
|
|
531
584
|
/** A run that mutated nothing (no package.json, or the maintenance failed and warned). */
|
|
@@ -574,63 +627,6 @@ function maintainDerivedPkg(rootDir, agentKitVersion) {
|
|
|
574
627
|
return emptyPkgMutations();
|
|
575
628
|
}
|
|
576
629
|
}
|
|
577
|
-
/**
|
|
578
|
-
* Sync the Factory's managed `.gitattributes` block (the EOL/binary rules that
|
|
579
|
-
* keep the kit's committed binaries from being corrupted by CRLF→LF normalization)
|
|
580
|
-
* into the derived repo, preserving the dev's own rules. The canonical block is
|
|
581
|
-
* READ from the staged tarball (single SSOT), so the rules are never duplicated in
|
|
582
|
-
* the CLI. Runs even without package.json (a non-Node derivative needs the rules
|
|
583
|
-
* too). Best-effort like the package.json mirror: a tarball that predates the
|
|
584
|
-
* managed block (no markers → no block) is a silent no-op, and a write failure
|
|
585
|
-
* never fails an already-applied update.
|
|
586
|
-
*/
|
|
587
|
-
function syncDerivedGitattributes(rootDir, stagedDir) {
|
|
588
|
-
try {
|
|
589
|
-
const srcPath = path.join(stagedDir, GITATTRIBUTES_FILE);
|
|
590
|
-
if (!existsSync(srcPath))
|
|
591
|
-
return;
|
|
592
|
-
const block = extractManagedBlock(readFileSync(srcPath, 'utf8'));
|
|
593
|
-
if (!block)
|
|
594
|
-
return;
|
|
595
|
-
const { action } = syncManagedBlock(rootDir, block);
|
|
596
|
-
if (action === 'unchanged')
|
|
597
|
-
return;
|
|
598
|
-
console.log(action === 'created'
|
|
599
|
-
? '✔ `.gitattributes` creado con las reglas de normalización del kit.'
|
|
600
|
-
: '✔ `.gitattributes`: bloque de reglas del kit sincronizado.');
|
|
601
|
-
}
|
|
602
|
-
catch {
|
|
603
|
-
console.warn('Aviso: no se pudo sincronizar `.gitattributes`; el cerebro se instaló igual.');
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
/**
|
|
607
|
-
* Sync the Factory's managed `.prettierignore` block (keeps the derived repo's
|
|
608
|
-
* formatter away from `.claude/**` — a pre-commit `prettier --write` over the kit
|
|
609
|
-
* files drifts disk from the lockfile hashes and turns every later update into
|
|
610
|
-
* false conflicts) into the derived repo, preserving the dev's own ignore rules.
|
|
611
|
-
* Same contract as `syncDerivedGitattributes`: canonical block READ from the
|
|
612
|
-
* staged tarball (single SSOT), best-effort, tarball without the file (pre-block
|
|
613
|
-
* releases) → silent no-op, a write failure never fails an applied update.
|
|
614
|
-
*/
|
|
615
|
-
function syncDerivedPrettierignore(rootDir, stagedDir) {
|
|
616
|
-
try {
|
|
617
|
-
const srcPath = path.join(stagedDir, PRETTIERIGNORE_FILE);
|
|
618
|
-
if (!existsSync(srcPath))
|
|
619
|
-
return;
|
|
620
|
-
const block = extractManagedBlock(readFileSync(srcPath, 'utf8'));
|
|
621
|
-
if (!block)
|
|
622
|
-
return;
|
|
623
|
-
const { action } = syncManagedBlock(rootDir, block, PRETTIERIGNORE_FILE);
|
|
624
|
-
if (action === 'unchanged')
|
|
625
|
-
return;
|
|
626
|
-
console.log(action === 'created'
|
|
627
|
-
? '✔ `.prettierignore` creado con el ignore del cerebro (`.claude/`) — evita falsos conflictos por formato en futuros updates.'
|
|
628
|
-
: '✔ `.prettierignore`: bloque del kit sincronizado (`.claude/` fuera del formatter).');
|
|
629
|
-
}
|
|
630
|
-
catch {
|
|
631
|
-
console.warn('Aviso: no se pudo sincronizar `.prettierignore`; el cerebro se instaló igual.');
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
630
|
/**
|
|
635
631
|
* The kit's own test folder. It stopped shipping to derivatives (`BND-003`): those tests
|
|
636
632
|
* validate KIT tooling, and inside a derivative they ran against a `src/` that may have
|
|
@@ -928,10 +924,11 @@ export async function runUpdate(flags, deps = {}) {
|
|
|
928
924
|
if (flags.dryRun) {
|
|
929
925
|
console.log('\n🔍 Vista previa: este repo aún no tiene lockfile. El primer `factory update` ' +
|
|
930
926
|
'registra el cerebro; en el primer sync no hay conflictos posibles.');
|
|
927
|
+
await offerKitDeps(rootDir, readKitDeps(stagedDir), { dryRun: true });
|
|
931
928
|
process.exitCode = 0;
|
|
932
929
|
return;
|
|
933
930
|
}
|
|
934
|
-
await applyLegacy(rootDir, stagedDir, manifest);
|
|
931
|
+
await applyLegacy(rootDir, stagedDir, manifest, flags.yes);
|
|
935
932
|
// Legacy auto-register succeeded → end-pointer (PVSKILL-002).
|
|
936
933
|
printProvisionPointer({ assumeYes: flags.yes });
|
|
937
934
|
return;
|
|
@@ -942,13 +939,17 @@ export async function runUpdate(flags, deps = {}) {
|
|
|
942
939
|
...oldLock.files.map((f) => f.path),
|
|
943
940
|
...manifest.files.map((f) => f.path),
|
|
944
941
|
]);
|
|
945
|
-
const disk = hashDiskPaths(rootDir, trackedPaths
|
|
942
|
+
const disk = hashDiskPaths(rootDir, trackedPaths, [
|
|
943
|
+
oldLock.files.find((f) => f.path === KIT_SETTINGS_PATH),
|
|
944
|
+
manifest.files.find((f) => f.path === KIT_SETTINGS_PATH),
|
|
945
|
+
]);
|
|
946
946
|
const diff = diffLockfiles(oldLock, manifest, disk);
|
|
947
947
|
// --dry-run: preview + STOP. No prompts, no apply, no lockfile, no commit.
|
|
948
948
|
// Exit 2 when conflicts exist so the GUI branches on it (0 otherwise). `return`
|
|
949
949
|
// runs the finally → tmpRoot is cleaned (no update-state was written).
|
|
950
950
|
if (flags.dryRun) {
|
|
951
951
|
reportDryRun(diff, manifest);
|
|
952
|
+
await offerKitDeps(rootDir, readKitDeps(stagedDir), { dryRun: true });
|
|
952
953
|
process.exitCode = diff.conflicts.length > 0 ? DRY_RUN_CONFLICTS_EXIT : 0;
|
|
953
954
|
return;
|
|
954
955
|
}
|
|
@@ -978,7 +979,9 @@ export async function runUpdate(flags, deps = {}) {
|
|
|
978
979
|
// (first entry from a stable lockfile, where `priorLock.beta` is absent).
|
|
979
980
|
const stateManifest = flags.beta ? { ...manifest, beta: true } : manifest;
|
|
980
981
|
writeUpdateState(rootDir, { stagedDir, backupDir, plan, newManifest: stateManifest });
|
|
982
|
+
const previousSettings = readPreviousSettings(rootDir, backupDir);
|
|
981
983
|
applyPlan(rootDir, stagedDir, plan, backupDir);
|
|
984
|
+
restoreProjectSettingsKeys(rootDir, plan.writes, previousSettings, kitSettingsKeysOf(oldLock));
|
|
982
985
|
// Lockfile written ONLY after a clean apply. The birth stamp is preserved
|
|
983
986
|
// (agentKitVersion advances; factoryVersion stays frozen). With `--beta`,
|
|
984
987
|
// `stampBeta` composes `stampBirth` and adds `beta:true`; a stable/plain
|
|
@@ -989,14 +992,23 @@ export async function runUpdate(flags, deps = {}) {
|
|
|
989
992
|
// same commit as the rest of the update (and `CLAUDE.md` joins the staged paths —
|
|
990
993
|
// in case B it is in `ignoreLocal`, so the plan never names it).
|
|
991
994
|
const claudeMdInserted = applyClaudeMdRetrofit(rootDir, claudeMd);
|
|
992
|
-
const
|
|
995
|
+
const kitExamples = refreshKitExamples(rootDir, stagedDir, manifest.files.map((f) => f.path));
|
|
996
|
+
const pkgMutations = maintainDerivedDotfiles(rootDir, stagedDir, manifest);
|
|
993
997
|
const prune = await planPruneMaintenance(rootDir, stagedDir, manifest.files.map((f) => f.path));
|
|
994
|
-
await maybeCommitBrain(rootDir, manifest.version, [
|
|
998
|
+
await maybeCommitBrain(rootDir, manifest.version, [
|
|
999
|
+
...plan.writes,
|
|
1000
|
+
...plan.deletes,
|
|
1001
|
+
...kitExamples,
|
|
1002
|
+
...(claudeMdInserted ? [CLAUDE_MD_FILE] : []),
|
|
1003
|
+
], flags, prune);
|
|
995
1004
|
// `theirsConflicts` IS the overwritten-local list: same source whether the dev answered
|
|
996
1005
|
// the prompt, passed `--theirs-all`, or ran headless — there is no second path. The
|
|
997
1006
|
// package.json mutations ride the SAME closing summary, not three loose notices.
|
|
998
1007
|
reportSummary(diff, manifest, theirsConflicts, pkgMutations);
|
|
999
1008
|
reportClaudeMd(claudeMd, 'applied', claudeMdInserted);
|
|
1009
|
+
// AFTER the brain commit: an install changes package.json and the package lockfile, which
|
|
1010
|
+
// stay out of that commit (Factory#78).
|
|
1011
|
+
await offerKitDeps(rootDir, readKitDeps(stagedDir), { assumeYes: flags.yes });
|
|
1000
1012
|
// 🔴 The retrofit reminder, on EVERY applied update and regardless of how conflicts
|
|
1001
1013
|
// were resolved — `--theirs-all`, `--mine-all`, per-file answers or none at all. A
|
|
1002
1014
|
// retrofit is not a conflict: it is work this command never attempted, living outside
|
|
@@ -1006,6 +1018,10 @@ export async function runUpdate(flags, deps = {}) {
|
|
|
1006
1018
|
// `oldLock.version` is the version BEFORE this update, which is what separates the guides
|
|
1007
1019
|
// that just arrived from the ones that were already sitting there. A legacy repo carries
|
|
1008
1020
|
// `0.0.0`, so every guide reads as new — correct: it has seen none of them.
|
|
1021
|
+
// What changed first, then what still needs hands: the headlines of every kit release
|
|
1022
|
+
// this update crossed, from the changelog that just arrived. Same reasons as the reminder
|
|
1023
|
+
// for printing on every outcome and headless.
|
|
1024
|
+
printWhatsNew({ rootDir, fromVersion: oldLock.version });
|
|
1009
1025
|
printRetrofitReminder({ rootDir, fromVersion: oldLock.version, guiSignal: flags.launcher });
|
|
1010
1026
|
await maybeOfferEmptyE2EBranch(rootDir, flags);
|
|
1011
1027
|
// End-pointer to the next step (PVSKILL-002). Reached only after a clean apply;
|
|
@@ -1074,7 +1090,8 @@ async function confirmLegacy(profile, assumeYes) {
|
|
|
1074
1090
|
name: 'proceed',
|
|
1075
1091
|
message: `Este repo no tiene lockfile. Se hará un auto-registro por paths (perfil \`${profile}\`).\n` +
|
|
1076
1092
|
`${detected}\nLos archivos del cerebro que coincidan se sobrescribirán con la última ` +
|
|
1077
|
-
'versión; tu `CLAUDE.md` se conserva si ya existe
|
|
1093
|
+
'versión; tu `CLAUDE.md` se conserva si ya existe, y también tus archivos fuera de ' +
|
|
1094
|
+
'`.claude/` y `scripts/tools/` (la versión del kit queda al lado como `.kit-example`). ¿Continuar?',
|
|
1078
1095
|
initial: false,
|
|
1079
1096
|
});
|
|
1080
1097
|
if (!proceed) {
|
|
@@ -1087,26 +1104,30 @@ async function confirmLegacy(profile, assumeYes) {
|
|
|
1087
1104
|
* NOT deleted on the first sync (design §6.3). Builds the lockfile from the
|
|
1088
1105
|
* normalized hashes of the installed files.
|
|
1089
1106
|
*/
|
|
1090
|
-
async function applyLegacy(rootDir, stagedDir, manifest) {
|
|
1107
|
+
async function applyLegacy(rootDir, stagedDir, manifest, assumeYes) {
|
|
1091
1108
|
const repoPaths = new Set(collectClaudePaths(rootDir));
|
|
1092
1109
|
const plan = planAutoRegister(manifest, repoPaths);
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
//
|
|
1098
|
-
//
|
|
1110
|
+
// The brain is written whole (kit-owned overwrite + adds). No deletes on the
|
|
1111
|
+
// first sync. CLAUDE.md is dev-owned (rama B / write-if-absent): if it already
|
|
1112
|
+
// exists on disk it is preserved (filtered out of writes); only written when
|
|
1113
|
+
// absent. The lockfile still records the manifest's CLAUDE.md hash, so a
|
|
1114
|
+
// preserved dev CLAUDE.md reads as "locally edited" on future updates → always
|
|
1115
|
+
// ignoreLocal (rama A), never silently clobbered. The other kit files outside
|
|
1116
|
+
// the brain get the same treatment as in `add` (#76): one the repo already has
|
|
1117
|
+
// with its own content is kept, the kit's copy goes to `<path>.kit-example`, and
|
|
1118
|
+
// the lockfile records the kit hash (ignoreLocal until the kit changes it).
|
|
1099
1119
|
const claudeMdExists = existsSync(path.join(rootDir, CLAUDE_MD_FILE));
|
|
1100
|
-
const writes = manifest
|
|
1101
|
-
.map((f) => f.path)
|
|
1102
|
-
.filter((p) => !(p === CLAUDE_MD_FILE && claudeMdExists));
|
|
1120
|
+
const { writes, kept } = partitionInitialWrites(rootDir, manifest);
|
|
1103
1121
|
const backupDir = defaultBackupDir(path.dirname(stagedDir));
|
|
1122
|
+
const previousSettings = readPreviousSettings(rootDir, backupDir);
|
|
1104
1123
|
applyPlan(rootDir, stagedDir, { writes, deletes: [] }, backupDir);
|
|
1124
|
+
restoreProjectSettingsKeys(rootDir, writes, previousSettings, LEGACY_KIT_SETTINGS_KEYS);
|
|
1125
|
+
writeKitExamples(rootDir, stagedDir, kept);
|
|
1105
1126
|
// The lockfile = the manifest (its hashes are already the normalized hashes of
|
|
1106
1127
|
// the just-installed files) PLUS the birth stamp. A legacy auto-register is the
|
|
1107
1128
|
// first sync this repo records, so the birth seal = the manifest's version.
|
|
1108
1129
|
writeLockfile(rootDir, { ...manifest, factoryVersion: manifest.version });
|
|
1109
|
-
const pkgMutations = maintainDerivedDotfiles(rootDir, stagedDir, manifest
|
|
1130
|
+
const pkgMutations = maintainDerivedDotfiles(rootDir, stagedDir, manifest);
|
|
1110
1131
|
// No brain commit on this path; the block is still kept in step (the index is left to
|
|
1111
1132
|
// `prune prepare`, which the pending notice names).
|
|
1112
1133
|
reportPendingPrune(await planPruneMaintenance(rootDir, stagedDir, manifest.files.map((f) => f.path)));
|
|
@@ -1115,6 +1136,7 @@ async function applyLegacy(rootDir, stagedDir, manifest) {
|
|
|
1115
1136
|
console.log('\nConservé tu `CLAUDE.md` (no se sobrescribió). Verifica que importe las rules del kit ' +
|
|
1116
1137
|
'(`@.claude/rules/*`); corre `factory doctor` para detectar rules sin importar.');
|
|
1117
1138
|
}
|
|
1139
|
+
reportKeptRepoFiles(kept);
|
|
1118
1140
|
if (plan.ambiguous.length > 0) {
|
|
1119
1141
|
console.log('\nArchivos en `.claude/` que no están en el manifest (revisar manualmente):');
|
|
1120
1142
|
for (const p of plan.ambiguous)
|
|
@@ -1122,4 +1144,6 @@ async function applyLegacy(rootDir, stagedDir, manifest) {
|
|
|
1122
1144
|
}
|
|
1123
1145
|
console.log(`\n✔ Auto-registro completo (v${manifest.version}, perfil ${manifest.profile}): ` +
|
|
1124
1146
|
`${plan.kitOwned.length} kit-owned, ${plan.toAdd.length} agregados, ${plan.ambiguous.length} ambiguos.`);
|
|
1147
|
+
// No brain commit on this path, so nothing to keep the install out of (Factory#78).
|
|
1148
|
+
await offerKitDeps(rootDir, readKitDeps(stagedDir), { assumeYes });
|
|
1125
1149
|
}
|