@timekast/factory 1.4.0 → 1.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.
@@ -41,7 +41,7 @@ import prompts from 'prompts';
41
41
  import { applyPlan, clearUpdateState, defaultBackupDir, hasUpdateState, readUpdateState, validateStagedManifest, writeUpdateState, } from '../lib/atomic-swap.js';
42
42
  import { collectClaudePaths } from '../lib/claude-paths.js';
43
43
  import { CLIError } from '../lib/cli-error.js';
44
- import { CLAUDE_MD_FILE, GITATTRIBUTES_FILE, LOCKFILE_FILE, PROFILES, TIMEKAST_DIR, } from '../lib/constants.js';
44
+ import { CLAUDE_MD_FILE, GITATTRIBUTES_FILE, LOCKFILE_FILE, PRETTIERIGNORE_FILE, PROFILES, TIMEKAST_DIR, } from '../lib/constants.js';
45
45
  import { decideCommit } from '../lib/commit-decision.js';
46
46
  import { extractManagedBlock, syncManagedBlock } from '../lib/gitattributes.js';
47
47
  import { diffLockfiles, hasLockfile, normalizeThenHash, planAutoRegister, readLockfile, writeLockfile, } from '../lib/lockfile.js';
@@ -186,6 +186,7 @@ async function maybeCommitBrain(rootDir, version, appliedPaths, flags) {
186
186
  `${TIMEKAST_DIR}/${LOCKFILE_FILE}`,
187
187
  ...(existsSync(path.join(rootDir, 'package.json')) ? ['package.json'] : []),
188
188
  ...(existsSync(path.join(rootDir, GITATTRIBUTES_FILE)) ? [GITATTRIBUTES_FILE] : []),
189
+ ...(existsSync(path.join(rootDir, PRETTIERIGNORE_FILE)) ? [PRETTIERIGNORE_FILE] : []),
189
190
  ];
190
191
  try {
191
192
  await execa('git', ['add', '--', ...candidates], { cwd: rootDir, reject: false });
@@ -251,16 +252,18 @@ function warnOnScriptConflict(action, _pkgPath) {
251
252
  }
252
253
  }
253
254
  /**
254
- * After a sync, maintain the derived project's co-owned dotfiles — `package.json`
255
- * AND `.gitattributes` — in one place. Centralized (not scattered per command) so
256
- * all THREE update paths (main, legacy auto-register, resume) are covered by
257
- * construction: a per-command call already regressed once (legacy + resume were
258
- * missed). `stagedDir` is the freshly-unpacked tarball, the source of the
259
- * `.gitattributes` managed block.
255
+ * After a sync, maintain the derived project's co-owned dotfiles — `package.json`,
256
+ * `.gitattributes` AND `.prettierignore` — in one place. Centralized (not scattered
257
+ * per command) so all THREE update paths (main, legacy auto-register, resume) are
258
+ * covered by construction: a per-command call already regressed once (legacy +
259
+ * resume were missed). `stagedDir` is the freshly-unpacked tarball, the source of
260
+ * the managed blocks. Runs BEFORE any brain commit, so the blocks land before a
261
+ * pre-commit hook could reformat the freshly-installed kit files.
260
262
  */
261
263
  function maintainDerivedDotfiles(rootDir, stagedDir, agentKitVersion) {
262
264
  maintainDerivedPkg(rootDir, agentKitVersion);
263
265
  syncDerivedGitattributes(rootDir, stagedDir);
266
+ syncDerivedPrettierignore(rootDir, stagedDir);
264
267
  }
265
268
  /**
266
269
  * Maintain the derived project's package.json: ensure the `factory:*` scripts
@@ -317,6 +320,34 @@ function syncDerivedGitattributes(rootDir, stagedDir) {
317
320
  console.warn('Aviso: no se pudo sincronizar `.gitattributes`; el cerebro se instaló igual.');
318
321
  }
319
322
  }
323
+ /**
324
+ * Sync the Factory's managed `.prettierignore` block (keeps the derived repo's
325
+ * formatter away from `.claude/**` — a pre-commit `prettier --write` over the kit
326
+ * files drifts disk from the lockfile hashes and turns every later update into
327
+ * false conflicts) into the derived repo, preserving the dev's own ignore rules.
328
+ * Same contract as `syncDerivedGitattributes`: canonical block READ from the
329
+ * staged tarball (single SSOT), best-effort, tarball without the file (pre-block
330
+ * releases) → silent no-op, a write failure never fails an applied update.
331
+ */
332
+ function syncDerivedPrettierignore(rootDir, stagedDir) {
333
+ try {
334
+ const srcPath = path.join(stagedDir, PRETTIERIGNORE_FILE);
335
+ if (!existsSync(srcPath))
336
+ return;
337
+ const block = extractManagedBlock(readFileSync(srcPath, 'utf8'));
338
+ if (!block)
339
+ return;
340
+ const { action } = syncManagedBlock(rootDir, block, PRETTIERIGNORE_FILE);
341
+ if (action === 'unchanged')
342
+ return;
343
+ console.log(action === 'created'
344
+ ? '✔ `.prettierignore` creado con el ignore del cerebro (`.claude/`) — evita falsos conflictos por formato en futuros updates.'
345
+ : '✔ `.prettierignore`: bloque del kit sincronizado (`.claude/` fuera del formatter).');
346
+ }
347
+ catch {
348
+ console.warn('Aviso: no se pudo sincronizar `.prettierignore`; el cerebro se instaló igual.');
349
+ }
350
+ }
320
351
  /** Print the deletes + kept-retired (design §7.6 — visible) + a one-line summary. */
321
352
  function reportSummary(diff, manifest) {
322
353
  if (diff.deleteSilent.length > 0) {
@@ -39,6 +39,15 @@ export const CLAUDE_MD_FILE = 'CLAUDE.md';
39
39
  * preserved byte-for-byte. See `syncManagedBlock` in `lib/gitattributes.ts`.
40
40
  */
41
41
  export const GITATTRIBUTES_FILE = '.gitattributes';
42
+ /**
43
+ * Repo-root `.prettierignore`. Same dual-owned managed-block treatment as
44
+ * `.gitattributes` (NOT tracked in the manifest — the dev's own ignore rules
45
+ * would read as local edits): the kit's block keeps the derived project's
46
+ * formatter away from `.claude/**`, whose prettier rewrite on the brain commit
47
+ * (table alignment, emphasis style) drifts disk from the lockfile hashes and
48
+ * turns every later `factory:update` into false conflicts (A2).
49
+ */
50
+ export const PRETTIERIGNORE_FILE = '.prettierignore';
42
51
  /**
43
52
  * Managed-block sentinels. These are PREFIXES (the live source lines carry extra
44
53
  * descriptive text + trailing `>>>`/`<<<`), matched with `startsWith` so detection
@@ -1,15 +1,16 @@
1
1
  /**
2
- * Surgical sync of the Factory's managed `.gitattributes` block into a derived
3
- * project, mirroring the `package.json` script-insertion pattern (`package-json.ts`):
4
- * the delimited block is replaced verbatim, everything outside it is preserved.
2
+ * Surgical sync of a Factory-managed block into a derived project's dual-owned
3
+ * dotfile (`.gitattributes`, `.prettierignore`), mirroring the `package.json`
4
+ * script-insertion pattern (`package-json.ts`): the delimited block is replaced
5
+ * verbatim, everything outside it is preserved.
5
6
  *
6
- * Why not the lockfile/`track` engine: `.gitattributes` is dual-owned at the
7
+ * Why not the lockfile/`track` engine: these files are dual-owned at the
7
8
  * intra-file level (the Factory block + the dev's own rules share one file). The
8
- * lockfile hashes whole files, so tracking it would read the dev's rules as a
9
+ * lockfile hashes whole files, so tracking them would read the dev's rules as a
9
10
  * local edit and conflict on every update. Instead the block is synced as an
10
11
  * idempotent post-apply step, outside `diffLockfiles`/`applyPlan`.
11
12
  *
12
- * The canonical block is the single SSOT: it is READ from the `.gitattributes`
13
+ * The canonical block is the single SSOT: it is READ from the same-named file
13
14
  * that ships in the staged tarball (see `extractManagedBlock`), never duplicated
14
15
  * as a CLI constant. A derived repo's existing block is located by the
15
16
  * `MANAGED_BLOCK_START` / `_END` sentinels (prefix match), so the descriptive
@@ -49,11 +50,11 @@ export function extractManagedBlock(content) {
49
50
  return slice.join('\n');
50
51
  }
51
52
  /**
52
- * Sync `sourceBlock` (markers inclusive) into `<rootDir>/.gitattributes`,
53
- * preserving every rule outside the managed block. The whole file is normalized
54
- * to LF (it lives at repo root, outside `.claude/** text eol=lf`, so a source
55
- * packed with CRLF on an autocrlf machine is normalized here). Idempotent:
56
- * writes only when the bytes actually change.
53
+ * Sync `sourceBlock` (markers inclusive) into `<rootDir>/<fileName>` (default
54
+ * `.gitattributes`), preserving every rule outside the managed block. The whole
55
+ * file is normalized to LF (it lives at repo root, outside `.claude/** text eol=lf`,
56
+ * so a source packed with CRLF on an autocrlf machine is normalized here).
57
+ * Idempotent: writes only when the bytes actually change.
57
58
  *
58
59
  * - no file / empty file → `created` (block only)
59
60
  * - file without a start marker → `inserted` (block appended after a blank line)
@@ -61,8 +62,8 @@ export function extractManagedBlock(content) {
61
62
  * - start without end (dev clobbered half a line) → `updated`, regenerated
62
63
  * cleanly from start to EOF (NOT appended — avoids a dangling start marker)
63
64
  */
64
- export function syncManagedBlock(rootDir, sourceBlock) {
65
- const filePath = path.join(rootDir, GITATTRIBUTES_FILE);
65
+ export function syncManagedBlock(rootDir, sourceBlock, fileName = GITATTRIBUTES_FILE) {
66
+ const filePath = path.join(rootDir, fileName);
66
67
  const raw = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
67
68
  const block = sourceBlock.replace(/\r\n/g, '\n').replace(/\n+$/, '');
68
69
  let result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timekast/factory",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Public, thin CLI to bootstrap and maintain TimeKast Factory derived projects.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",