@timekast/factory 2.3.1 → 2.5.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 +32 -3
- package/dist/commands/env.js +16 -4
- package/dist/commands/provision.js +86 -41
- package/dist/commands/prune.js +305 -0
- package/dist/commands/update.js +164 -91
- package/dist/commands/usage.js +303 -0
- package/dist/commands/vault.js +81 -18
- package/dist/index.js +40 -0
- 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 +39 -0
- package/dist/lib/derived-dotfiles.js +165 -0
- package/dist/lib/email-from.js +12 -4
- package/dist/lib/flags.js +14 -0
- 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-git.js +81 -0
- package/dist/lib/prune-pr-check.js +119 -0
- package/dist/lib/prune-remote.js +466 -0
- package/dist/lib/prune-tiers.js +253 -0
- package/dist/lib/prune-update.js +78 -0
- package/dist/lib/prune-vercel.js +137 -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 +21 -0
- package/package.json +1 -1
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
|
|
@@ -42,13 +44,18 @@ import { applyPlan, clearUpdateState, defaultBackupDir, hasUpdateState, readUpda
|
|
|
42
44
|
import { applyClaudeMdRetrofit, assessClaudeMd, reportClaudeMd, } from '../lib/claude-md.js';
|
|
43
45
|
import { collectClaudePaths } from '../lib/claude-paths.js';
|
|
44
46
|
import { CLIError } from '../lib/cli-error.js';
|
|
45
|
-
import { CLAUDE_MD_FILE, GITATTRIBUTES_FILE, LOCKFILE_FILE, PRETTIERIGNORE_FILE, PROFILES, TIMEKAST_DIR, } from '../lib/constants.js';
|
|
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';
|
|
57
|
+
import { planPruneMaintenance } from '../lib/prune-update.js';
|
|
58
|
+
import { isRestricted, PRUNE_MARKER_PATH } from '../lib/prune-tiers.js';
|
|
52
59
|
import { printRetrofitReminder } from '../lib/retrofit-reminder.js';
|
|
53
60
|
import { hasProvisionState } from '../lib/provision-state.js';
|
|
54
61
|
import { loginCommand } from '../lib/rail.js';
|
|
@@ -98,16 +105,52 @@ async function acquireFromBeta(profile) {
|
|
|
98
105
|
* Hash the disk content (normalized) of every path in `paths` that exists.
|
|
99
106
|
* A missing file is simply absent from the returned map.
|
|
100
107
|
*/
|
|
101
|
-
function hashDiskPaths(rootDir, paths) {
|
|
108
|
+
function hashDiskPaths(rootDir, paths, settingsEntries = []) {
|
|
102
109
|
const map = new Map();
|
|
103
110
|
for (const rel of paths) {
|
|
104
111
|
const abs = path.join(rootDir, rel);
|
|
105
112
|
if (existsSync(abs) && statSync(abs).isFile()) {
|
|
106
|
-
|
|
113
|
+
const raw = readFileSync(abs, 'utf8');
|
|
114
|
+
// settings.json: only the kit's keys count, by content (the project's are its own).
|
|
115
|
+
map.set(rel, rel === KIT_SETTINGS_PATH
|
|
116
|
+
? effectiveSettingsHash(raw, settingsEntries)
|
|
117
|
+
: normalizeThenHash(raw));
|
|
107
118
|
}
|
|
108
119
|
}
|
|
109
120
|
return map;
|
|
110
121
|
}
|
|
122
|
+
/** The `settings.json` the project had before this apply: the run's backup, else the disk. */
|
|
123
|
+
function readPreviousSettings(rootDir, backupDir) {
|
|
124
|
+
for (const abs of [
|
|
125
|
+
path.join(backupDir, KIT_SETTINGS_PATH),
|
|
126
|
+
path.join(rootDir, KIT_SETTINGS_PATH),
|
|
127
|
+
]) {
|
|
128
|
+
if (existsSync(abs) && statSync(abs).isFile())
|
|
129
|
+
return readFileSync(abs, 'utf8');
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* After the kit's `settings.json` was written, put the project's own keys back (every top-level
|
|
135
|
+
* key the kit does not declare) and say so. No-op when the kit's file was not written.
|
|
136
|
+
*/
|
|
137
|
+
function restoreProjectSettingsKeys(rootDir, written, previous, previousKitKeys) {
|
|
138
|
+
if (previous === null || !written.includes(KIT_SETTINGS_PATH))
|
|
139
|
+
return;
|
|
140
|
+
const abs = path.join(rootDir, KIT_SETTINGS_PATH);
|
|
141
|
+
const merged = mergeProjectSettingsKeys(readFileSync(abs, 'utf8'), previous, previousKitKeys);
|
|
142
|
+
if (merged.content !== null) {
|
|
143
|
+
writeFileSync(abs, merged.content, 'utf8');
|
|
144
|
+
console.log(` ✔ ${KIT_SETTINGS_PATH}: conservé tus llaves (${merged.kept.join(', ')})`);
|
|
145
|
+
}
|
|
146
|
+
for (const key of merged.replaced) {
|
|
147
|
+
console.warn(` ⚠ ${KIT_SETTINGS_PATH}: \`${key}\` ahora la declara el kit — tu valor se reemplazó por el suyo.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** The kit's keys recorded for `settings.json` in a lockfile (the legacy set when absent). */
|
|
151
|
+
function kitSettingsKeysOf(lock) {
|
|
152
|
+
return lock?.files.find((f) => f.path === KIT_SETTINGS_PATH)?.keys ?? LEGACY_KIT_SETTINGS_KEYS;
|
|
153
|
+
}
|
|
111
154
|
/** Unchanged lines kept around each change in the `[3] Ver diff` view. */
|
|
112
155
|
const DIFF_CONTEXT_LINES = 3;
|
|
113
156
|
/**
|
|
@@ -299,7 +342,7 @@ function runVerify(rootDir) {
|
|
|
299
342
|
throw new CLIError('No hay lockfile que verificar. Corre `factory update` primero.');
|
|
300
343
|
}
|
|
301
344
|
const lock = readLockfile(rootDir);
|
|
302
|
-
const disk = hashDiskPaths(rootDir, lock.files.map((f) => f.path));
|
|
345
|
+
const disk = hashDiskPaths(rootDir, lock.files.map((f) => f.path), [lock.files.find((f) => f.path === KIT_SETTINGS_PATH)]);
|
|
303
346
|
const missing = [];
|
|
304
347
|
const drifted = [];
|
|
305
348
|
for (const entry of lock.files) {
|
|
@@ -332,15 +375,20 @@ function runVerify(rootDir) {
|
|
|
332
375
|
* → prompt y/n · headless → commit by default (a headless run needs it). Stages ONLY
|
|
333
376
|
* what the update touched (the applied manifest paths + lockfile + package.json +
|
|
334
377
|
* .gitattributes) — never `git add -A`. Best-effort: no identity / no repo → warn + skip.
|
|
378
|
+
* In a pruned repo (`prune` given) it also untracks what the tier restricts, leaves the
|
|
379
|
+
* restricted applied paths out, and stages `.gitignore`, the switch and the shared kit paths
|
|
380
|
+
* that are not tracked yet.
|
|
335
381
|
*/
|
|
336
|
-
async function maybeCommitBrain(rootDir, version, appliedPaths, flags) {
|
|
382
|
+
async function maybeCommitBrain(rootDir, version, appliedPaths, flags, prune = null) {
|
|
337
383
|
const decision = decideCommit({
|
|
338
384
|
commit: flags.commit,
|
|
339
385
|
noCommit: flags.noCommit,
|
|
340
386
|
isTTY: Boolean(process.stdout.isTTY),
|
|
341
387
|
});
|
|
342
|
-
if (decision === 'skip')
|
|
388
|
+
if (decision === 'skip') {
|
|
389
|
+
reportPendingPrune(prune);
|
|
343
390
|
return;
|
|
391
|
+
}
|
|
344
392
|
let doCommit = decision === 'commit';
|
|
345
393
|
if (decision === 'prompt') {
|
|
346
394
|
const { yes } = await prompts({
|
|
@@ -351,17 +399,31 @@ async function maybeCommitBrain(rootDir, version, appliedPaths, flags) {
|
|
|
351
399
|
});
|
|
352
400
|
doCommit = yes === true;
|
|
353
401
|
}
|
|
354
|
-
if (!doCommit)
|
|
402
|
+
if (!doCommit) {
|
|
403
|
+
reportPendingPrune(prune);
|
|
355
404
|
return;
|
|
405
|
+
}
|
|
356
406
|
const candidates = [
|
|
357
|
-
|
|
407
|
+
// A pruned repo never stages what its tier keeps local (it is ignored, and the guard would
|
|
408
|
+
// refuse it); it DOES stage the shared kit paths not yet tracked (`prune-update.ts`).
|
|
409
|
+
...(prune ? appliedPaths.filter((p) => !isRestricted(prune.tier, p)) : appliedPaths),
|
|
410
|
+
...(prune ? [...prune.publish, GITIGNORE_FILE, PRUNE_MARKER_PATH] : []),
|
|
358
411
|
`${TIMEKAST_DIR}/${LOCKFILE_FILE}`,
|
|
359
412
|
...(existsSync(path.join(rootDir, 'package.json')) ? ['package.json'] : []),
|
|
360
413
|
...(existsSync(path.join(rootDir, GITATTRIBUTES_FILE)) ? [GITATTRIBUTES_FILE] : []),
|
|
361
414
|
...(existsSync(path.join(rootDir, PRETTIERIGNORE_FILE)) ? [PRETTIERIGNORE_FILE] : []),
|
|
362
415
|
];
|
|
363
416
|
try {
|
|
364
|
-
|
|
417
|
+
const stageable = await filterStageable(rootDir, candidates);
|
|
418
|
+
if (prune && prune.untrack.length > 0) {
|
|
419
|
+
await execa('git', ['rm', '--cached', '-q', '--ignore-unmatch', '--', ...prune.untrack], {
|
|
420
|
+
cwd: rootDir,
|
|
421
|
+
reject: false,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (stageable.length > 0) {
|
|
425
|
+
await execa('git', ['add', '--', ...stageable], { cwd: rootDir, reject: false });
|
|
426
|
+
}
|
|
365
427
|
// Nothing staged (e.g. a no-op update) → no empty commit.
|
|
366
428
|
const staged = await execa('git', ['diff', '--cached', '--quiet'], {
|
|
367
429
|
cwd: rootDir,
|
|
@@ -382,6 +444,34 @@ async function maybeCommitBrain(rootDir, version, appliedPaths, flags) {
|
|
|
382
444
|
console.warn('Aviso: no se pudo commitear; el cerebro se instaló igual.');
|
|
383
445
|
}
|
|
384
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* The brain commit was skipped in a pruned repo: whatever the tier asks the index to change
|
|
449
|
+
* (untrack a restricted path, publish a newly shared one) is still pending. `prune prepare`
|
|
450
|
+
* does exactly that and is idempotent, so it is the one instruction to give.
|
|
451
|
+
*/
|
|
452
|
+
function reportPendingPrune(prune) {
|
|
453
|
+
if (!prune || (prune.untrack.length === 0 && prune.publish.length === 0))
|
|
454
|
+
return;
|
|
455
|
+
console.log(`ℹ️ Repo compartido (tier \`${prune.tierName}\`): quedan ${prune.untrack.length} ruta(s) por ` +
|
|
456
|
+
`dejar de trackear y ${prune.publish.length} por publicar. Corre \`factory prune prepare\` ` +
|
|
457
|
+
'para commitearlo.');
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Keep only the paths `git add` can take: on disk, or in the index (a deletion to stage).
|
|
461
|
+
*
|
|
462
|
+
* ONE path that is in neither — a retired file the dev never committed, which the update just
|
|
463
|
+
* removed from disk — makes `git add -- a b c` exit 128 having staged NOTHING, so the brain
|
|
464
|
+
* commit was lost in silence (the warning below only fires when `git commit` fails, and a
|
|
465
|
+
* clean index commits nothing). Exported for the CLI suite.
|
|
466
|
+
*/
|
|
467
|
+
export async function filterStageable(rootDir, candidates) {
|
|
468
|
+
const listed = await execa('git', ['ls-files', '-z', '--cached', '--', ...candidates], {
|
|
469
|
+
cwd: rootDir,
|
|
470
|
+
reject: false,
|
|
471
|
+
});
|
|
472
|
+
const inIndex = new Set(listed.exitCode === 0 ? listed.stdout.split('\0').filter(Boolean) : []);
|
|
473
|
+
return [...new Set(candidates)].filter((rel) => inIndex.has(rel) || existsSync(path.join(rootDir, rel)));
|
|
474
|
+
}
|
|
385
475
|
/**
|
|
386
476
|
* `--resume`: re-apply a persisted mid-flight state without re-downloading
|
|
387
477
|
* (design §7.5). Reads the staged dir + plan from `.timekast/.update-state.json`.
|
|
@@ -416,19 +506,32 @@ async function runResume(rootDir, flags) {
|
|
|
416
506
|
const claudeMd = priorLock
|
|
417
507
|
? assessClaudeMd(rootDir, priorLock)
|
|
418
508
|
: undefined;
|
|
509
|
+
const previousSettings = readPreviousSettings(rootDir, state.backupDir);
|
|
419
510
|
applyPlan(rootDir, state.stagedDir, state.plan, state.backupDir);
|
|
511
|
+
restoreProjectSettingsKeys(rootDir, state.plan.writes, previousSettings, kitSettingsKeysOf(priorLock));
|
|
420
512
|
writeLockfile(rootDir, newLock);
|
|
421
513
|
clearUpdateState(rootDir);
|
|
422
514
|
const claudeMdInserted = claudeMd ? applyClaudeMdRetrofit(rootDir, claudeMd) : false;
|
|
515
|
+
const kitExamples = refreshKitExamples(rootDir, state.stagedDir, newLock.files.map((f) => f.path));
|
|
423
516
|
// Maintain dotfiles BEFORE removing the staged dir (the .gitattributes source).
|
|
424
|
-
const pkgMutations = maintainDerivedDotfiles(rootDir, state.stagedDir, newLock
|
|
425
|
-
await
|
|
517
|
+
const pkgMutations = maintainDerivedDotfiles(rootDir, state.stagedDir, newLock);
|
|
518
|
+
const prune = await planPruneMaintenance(rootDir, state.stagedDir, newLock.files.map((f) => f.path));
|
|
519
|
+
await maybeCommitBrain(rootDir, newLock.version, [
|
|
520
|
+
...state.plan.writes,
|
|
521
|
+
...state.plan.deletes,
|
|
522
|
+
...kitExamples,
|
|
523
|
+
...(claudeMdInserted ? [CLAUDE_MD_FILE] : []),
|
|
524
|
+
], flags, prune);
|
|
525
|
+
const kitDeps = readKitDeps(state.stagedDir);
|
|
426
526
|
rmSync(state.stagedDir, { recursive: true, force: true });
|
|
427
527
|
// A resumed update maintains package.json exactly like a normal one, so it owes the dev the
|
|
428
528
|
// same account of what it moved there (`BND-004`).
|
|
429
529
|
reportPkgMutations(pkgMutations);
|
|
430
530
|
if (claudeMd)
|
|
431
531
|
reportClaudeMd(claudeMd, 'applied', claudeMdInserted);
|
|
532
|
+
// AFTER the brain commit: an install changes package.json and the package lockfile, which
|
|
533
|
+
// stay out of that commit (Factory#78).
|
|
534
|
+
await offerKitDeps(rootDir, kitDeps, { assumeYes: flags.yes });
|
|
432
535
|
console.log('✔ `update` retomado y completado.');
|
|
433
536
|
// A resumed update that succeeds completed the setup → show the end-pointer
|
|
434
537
|
// (PVSKILL-002 §8 edge: `--resume` SÍ lo muestra). Suppressed only in headless
|
|
@@ -472,10 +575,9 @@ function warnOnScriptConflict(action, _pkgPath) {
|
|
|
472
575
|
* the managed blocks. Runs BEFORE any brain commit, so the blocks land before a
|
|
473
576
|
* pre-commit hook could reformat the freshly-installed kit files.
|
|
474
577
|
*/
|
|
475
|
-
function maintainDerivedDotfiles(rootDir, stagedDir,
|
|
476
|
-
const pkgMutations = maintainDerivedPkg(rootDir,
|
|
477
|
-
|
|
478
|
-
syncDerivedPrettierignore(rootDir, stagedDir);
|
|
578
|
+
function maintainDerivedDotfiles(rootDir, stagedDir, manifest) {
|
|
579
|
+
const pkgMutations = maintainDerivedPkg(rootDir, manifest.version);
|
|
580
|
+
syncDerivedDotfiles(rootDir, stagedDir, manifest);
|
|
479
581
|
return pkgMutations;
|
|
480
582
|
}
|
|
481
583
|
/** A run that mutated nothing (no package.json, or the maintenance failed and warned). */
|
|
@@ -524,63 +626,6 @@ function maintainDerivedPkg(rootDir, agentKitVersion) {
|
|
|
524
626
|
return emptyPkgMutations();
|
|
525
627
|
}
|
|
526
628
|
}
|
|
527
|
-
/**
|
|
528
|
-
* Sync the Factory's managed `.gitattributes` block (the EOL/binary rules that
|
|
529
|
-
* keep the kit's committed binaries from being corrupted by CRLF→LF normalization)
|
|
530
|
-
* into the derived repo, preserving the dev's own rules. The canonical block is
|
|
531
|
-
* READ from the staged tarball (single SSOT), so the rules are never duplicated in
|
|
532
|
-
* the CLI. Runs even without package.json (a non-Node derivative needs the rules
|
|
533
|
-
* too). Best-effort like the package.json mirror: a tarball that predates the
|
|
534
|
-
* managed block (no markers → no block) is a silent no-op, and a write failure
|
|
535
|
-
* never fails an already-applied update.
|
|
536
|
-
*/
|
|
537
|
-
function syncDerivedGitattributes(rootDir, stagedDir) {
|
|
538
|
-
try {
|
|
539
|
-
const srcPath = path.join(stagedDir, GITATTRIBUTES_FILE);
|
|
540
|
-
if (!existsSync(srcPath))
|
|
541
|
-
return;
|
|
542
|
-
const block = extractManagedBlock(readFileSync(srcPath, 'utf8'));
|
|
543
|
-
if (!block)
|
|
544
|
-
return;
|
|
545
|
-
const { action } = syncManagedBlock(rootDir, block);
|
|
546
|
-
if (action === 'unchanged')
|
|
547
|
-
return;
|
|
548
|
-
console.log(action === 'created'
|
|
549
|
-
? '✔ `.gitattributes` creado con las reglas de normalización del kit.'
|
|
550
|
-
: '✔ `.gitattributes`: bloque de reglas del kit sincronizado.');
|
|
551
|
-
}
|
|
552
|
-
catch {
|
|
553
|
-
console.warn('Aviso: no se pudo sincronizar `.gitattributes`; el cerebro se instaló igual.');
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
/**
|
|
557
|
-
* Sync the Factory's managed `.prettierignore` block (keeps the derived repo's
|
|
558
|
-
* formatter away from `.claude/**` — a pre-commit `prettier --write` over the kit
|
|
559
|
-
* files drifts disk from the lockfile hashes and turns every later update into
|
|
560
|
-
* false conflicts) into the derived repo, preserving the dev's own ignore rules.
|
|
561
|
-
* Same contract as `syncDerivedGitattributes`: canonical block READ from the
|
|
562
|
-
* staged tarball (single SSOT), best-effort, tarball without the file (pre-block
|
|
563
|
-
* releases) → silent no-op, a write failure never fails an applied update.
|
|
564
|
-
*/
|
|
565
|
-
function syncDerivedPrettierignore(rootDir, stagedDir) {
|
|
566
|
-
try {
|
|
567
|
-
const srcPath = path.join(stagedDir, PRETTIERIGNORE_FILE);
|
|
568
|
-
if (!existsSync(srcPath))
|
|
569
|
-
return;
|
|
570
|
-
const block = extractManagedBlock(readFileSync(srcPath, 'utf8'));
|
|
571
|
-
if (!block)
|
|
572
|
-
return;
|
|
573
|
-
const { action } = syncManagedBlock(rootDir, block, PRETTIERIGNORE_FILE);
|
|
574
|
-
if (action === 'unchanged')
|
|
575
|
-
return;
|
|
576
|
-
console.log(action === 'created'
|
|
577
|
-
? '✔ `.prettierignore` creado con el ignore del cerebro (`.claude/`) — evita falsos conflictos por formato en futuros updates.'
|
|
578
|
-
: '✔ `.prettierignore`: bloque del kit sincronizado (`.claude/` fuera del formatter).');
|
|
579
|
-
}
|
|
580
|
-
catch {
|
|
581
|
-
console.warn('Aviso: no se pudo sincronizar `.prettierignore`; el cerebro se instaló igual.');
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
629
|
/**
|
|
585
630
|
* The kit's own test folder. It stopped shipping to derivatives (`BND-003`): those tests
|
|
586
631
|
* validate KIT tooling, and inside a derivative they ran against a `src/` that may have
|
|
@@ -878,10 +923,11 @@ export async function runUpdate(flags, deps = {}) {
|
|
|
878
923
|
if (flags.dryRun) {
|
|
879
924
|
console.log('\n🔍 Vista previa: este repo aún no tiene lockfile. El primer `factory update` ' +
|
|
880
925
|
'registra el cerebro; en el primer sync no hay conflictos posibles.');
|
|
926
|
+
await offerKitDeps(rootDir, readKitDeps(stagedDir), { dryRun: true });
|
|
881
927
|
process.exitCode = 0;
|
|
882
928
|
return;
|
|
883
929
|
}
|
|
884
|
-
await applyLegacy(rootDir, stagedDir, manifest);
|
|
930
|
+
await applyLegacy(rootDir, stagedDir, manifest, flags.yes);
|
|
885
931
|
// Legacy auto-register succeeded → end-pointer (PVSKILL-002).
|
|
886
932
|
printProvisionPointer({ assumeYes: flags.yes });
|
|
887
933
|
return;
|
|
@@ -892,13 +938,17 @@ export async function runUpdate(flags, deps = {}) {
|
|
|
892
938
|
...oldLock.files.map((f) => f.path),
|
|
893
939
|
...manifest.files.map((f) => f.path),
|
|
894
940
|
]);
|
|
895
|
-
const disk = hashDiskPaths(rootDir, trackedPaths
|
|
941
|
+
const disk = hashDiskPaths(rootDir, trackedPaths, [
|
|
942
|
+
oldLock.files.find((f) => f.path === KIT_SETTINGS_PATH),
|
|
943
|
+
manifest.files.find((f) => f.path === KIT_SETTINGS_PATH),
|
|
944
|
+
]);
|
|
896
945
|
const diff = diffLockfiles(oldLock, manifest, disk);
|
|
897
946
|
// --dry-run: preview + STOP. No prompts, no apply, no lockfile, no commit.
|
|
898
947
|
// Exit 2 when conflicts exist so the GUI branches on it (0 otherwise). `return`
|
|
899
948
|
// runs the finally → tmpRoot is cleaned (no update-state was written).
|
|
900
949
|
if (flags.dryRun) {
|
|
901
950
|
reportDryRun(diff, manifest);
|
|
951
|
+
await offerKitDeps(rootDir, readKitDeps(stagedDir), { dryRun: true });
|
|
902
952
|
process.exitCode = diff.conflicts.length > 0 ? DRY_RUN_CONFLICTS_EXIT : 0;
|
|
903
953
|
return;
|
|
904
954
|
}
|
|
@@ -928,7 +978,9 @@ export async function runUpdate(flags, deps = {}) {
|
|
|
928
978
|
// (first entry from a stable lockfile, where `priorLock.beta` is absent).
|
|
929
979
|
const stateManifest = flags.beta ? { ...manifest, beta: true } : manifest;
|
|
930
980
|
writeUpdateState(rootDir, { stagedDir, backupDir, plan, newManifest: stateManifest });
|
|
981
|
+
const previousSettings = readPreviousSettings(rootDir, backupDir);
|
|
931
982
|
applyPlan(rootDir, stagedDir, plan, backupDir);
|
|
983
|
+
restoreProjectSettingsKeys(rootDir, plan.writes, previousSettings, kitSettingsKeysOf(oldLock));
|
|
932
984
|
// Lockfile written ONLY after a clean apply. The birth stamp is preserved
|
|
933
985
|
// (agentKitVersion advances; factoryVersion stays frozen). With `--beta`,
|
|
934
986
|
// `stampBeta` composes `stampBirth` and adds `beta:true`; a stable/plain
|
|
@@ -939,13 +991,23 @@ export async function runUpdate(flags, deps = {}) {
|
|
|
939
991
|
// same commit as the rest of the update (and `CLAUDE.md` joins the staged paths —
|
|
940
992
|
// in case B it is in `ignoreLocal`, so the plan never names it).
|
|
941
993
|
const claudeMdInserted = applyClaudeMdRetrofit(rootDir, claudeMd);
|
|
942
|
-
const
|
|
943
|
-
|
|
994
|
+
const kitExamples = refreshKitExamples(rootDir, stagedDir, manifest.files.map((f) => f.path));
|
|
995
|
+
const pkgMutations = maintainDerivedDotfiles(rootDir, stagedDir, manifest);
|
|
996
|
+
const prune = await planPruneMaintenance(rootDir, stagedDir, manifest.files.map((f) => f.path));
|
|
997
|
+
await maybeCommitBrain(rootDir, manifest.version, [
|
|
998
|
+
...plan.writes,
|
|
999
|
+
...plan.deletes,
|
|
1000
|
+
...kitExamples,
|
|
1001
|
+
...(claudeMdInserted ? [CLAUDE_MD_FILE] : []),
|
|
1002
|
+
], flags, prune);
|
|
944
1003
|
// `theirsConflicts` IS the overwritten-local list: same source whether the dev answered
|
|
945
1004
|
// the prompt, passed `--theirs-all`, or ran headless — there is no second path. The
|
|
946
1005
|
// package.json mutations ride the SAME closing summary, not three loose notices.
|
|
947
1006
|
reportSummary(diff, manifest, theirsConflicts, pkgMutations);
|
|
948
1007
|
reportClaudeMd(claudeMd, 'applied', claudeMdInserted);
|
|
1008
|
+
// AFTER the brain commit: an install changes package.json and the package lockfile, which
|
|
1009
|
+
// stay out of that commit (Factory#78).
|
|
1010
|
+
await offerKitDeps(rootDir, readKitDeps(stagedDir), { assumeYes: flags.yes });
|
|
949
1011
|
// 🔴 The retrofit reminder, on EVERY applied update and regardless of how conflicts
|
|
950
1012
|
// were resolved — `--theirs-all`, `--mine-all`, per-file answers or none at all. A
|
|
951
1013
|
// retrofit is not a conflict: it is work this command never attempted, living outside
|
|
@@ -1023,7 +1085,8 @@ async function confirmLegacy(profile, assumeYes) {
|
|
|
1023
1085
|
name: 'proceed',
|
|
1024
1086
|
message: `Este repo no tiene lockfile. Se hará un auto-registro por paths (perfil \`${profile}\`).\n` +
|
|
1025
1087
|
`${detected}\nLos archivos del cerebro que coincidan se sobrescribirán con la última ` +
|
|
1026
|
-
'versión; tu `CLAUDE.md` se conserva si ya existe
|
|
1088
|
+
'versión; tu `CLAUDE.md` se conserva si ya existe, y también tus archivos fuera de ' +
|
|
1089
|
+
'`.claude/` y `scripts/tools/` (la versión del kit queda al lado como `.kit-example`). ¿Continuar?',
|
|
1027
1090
|
initial: false,
|
|
1028
1091
|
});
|
|
1029
1092
|
if (!proceed) {
|
|
@@ -1036,31 +1099,39 @@ async function confirmLegacy(profile, assumeYes) {
|
|
|
1036
1099
|
* NOT deleted on the first sync (design §6.3). Builds the lockfile from the
|
|
1037
1100
|
* normalized hashes of the installed files.
|
|
1038
1101
|
*/
|
|
1039
|
-
async function applyLegacy(rootDir, stagedDir, manifest) {
|
|
1102
|
+
async function applyLegacy(rootDir, stagedDir, manifest, assumeYes) {
|
|
1040
1103
|
const repoPaths = new Set(collectClaudePaths(rootDir));
|
|
1041
1104
|
const plan = planAutoRegister(manifest, repoPaths);
|
|
1042
|
-
//
|
|
1043
|
-
//
|
|
1044
|
-
//
|
|
1045
|
-
//
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1105
|
+
// The brain is written whole (kit-owned overwrite + adds). No deletes on the
|
|
1106
|
+
// first sync. CLAUDE.md is dev-owned (rama B / write-if-absent): if it already
|
|
1107
|
+
// exists on disk it is preserved (filtered out of writes); only written when
|
|
1108
|
+
// absent. The lockfile still records the manifest's CLAUDE.md hash, so a
|
|
1109
|
+
// preserved dev CLAUDE.md reads as "locally edited" on future updates → always
|
|
1110
|
+
// ignoreLocal (rama A), never silently clobbered. The other kit files outside
|
|
1111
|
+
// the brain get the same treatment as in `add` (#76): one the repo already has
|
|
1112
|
+
// with its own content is kept, the kit's copy goes to `<path>.kit-example`, and
|
|
1113
|
+
// the lockfile records the kit hash (ignoreLocal until the kit changes it).
|
|
1048
1114
|
const claudeMdExists = existsSync(path.join(rootDir, CLAUDE_MD_FILE));
|
|
1049
|
-
const writes = manifest
|
|
1050
|
-
.map((f) => f.path)
|
|
1051
|
-
.filter((p) => !(p === CLAUDE_MD_FILE && claudeMdExists));
|
|
1115
|
+
const { writes, kept } = partitionInitialWrites(rootDir, manifest);
|
|
1052
1116
|
const backupDir = defaultBackupDir(path.dirname(stagedDir));
|
|
1117
|
+
const previousSettings = readPreviousSettings(rootDir, backupDir);
|
|
1053
1118
|
applyPlan(rootDir, stagedDir, { writes, deletes: [] }, backupDir);
|
|
1119
|
+
restoreProjectSettingsKeys(rootDir, writes, previousSettings, LEGACY_KIT_SETTINGS_KEYS);
|
|
1120
|
+
writeKitExamples(rootDir, stagedDir, kept);
|
|
1054
1121
|
// The lockfile = the manifest (its hashes are already the normalized hashes of
|
|
1055
1122
|
// the just-installed files) PLUS the birth stamp. A legacy auto-register is the
|
|
1056
1123
|
// first sync this repo records, so the birth seal = the manifest's version.
|
|
1057
1124
|
writeLockfile(rootDir, { ...manifest, factoryVersion: manifest.version });
|
|
1058
|
-
const pkgMutations = maintainDerivedDotfiles(rootDir, stagedDir, manifest
|
|
1125
|
+
const pkgMutations = maintainDerivedDotfiles(rootDir, stagedDir, manifest);
|
|
1126
|
+
// No brain commit on this path; the block is still kept in step (the index is left to
|
|
1127
|
+
// `prune prepare`, which the pending notice names).
|
|
1128
|
+
reportPendingPrune(await planPruneMaintenance(rootDir, stagedDir, manifest.files.map((f) => f.path)));
|
|
1059
1129
|
reportPkgMutations(pkgMutations);
|
|
1060
1130
|
if (claudeMdExists) {
|
|
1061
1131
|
console.log('\nConservé tu `CLAUDE.md` (no se sobrescribió). Verifica que importe las rules del kit ' +
|
|
1062
1132
|
'(`@.claude/rules/*`); corre `factory doctor` para detectar rules sin importar.');
|
|
1063
1133
|
}
|
|
1134
|
+
reportKeptRepoFiles(kept);
|
|
1064
1135
|
if (plan.ambiguous.length > 0) {
|
|
1065
1136
|
console.log('\nArchivos en `.claude/` que no están en el manifest (revisar manualmente):');
|
|
1066
1137
|
for (const p of plan.ambiguous)
|
|
@@ -1068,4 +1139,6 @@ async function applyLegacy(rootDir, stagedDir, manifest) {
|
|
|
1068
1139
|
}
|
|
1069
1140
|
console.log(`\n✔ Auto-registro completo (v${manifest.version}, perfil ${manifest.profile}): ` +
|
|
1070
1141
|
`${plan.kitOwned.length} kit-owned, ${plan.toAdd.length} agregados, ${plan.ambiguous.length} ambiguos.`);
|
|
1142
|
+
// No brain commit on this path, so nothing to keep the install out of (Factory#78).
|
|
1143
|
+
await offerKitDeps(rootDir, readKitDeps(stagedDir), { assumeYes });
|
|
1071
1144
|
}
|