@timekast/factory 1.2.0 → 1.4.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.
@@ -85,6 +85,8 @@ export async function runAdd(flags = { core: false, full: false }) {
85
85
  mineAll: false,
86
86
  resume: false,
87
87
  verify: false,
88
+ commit: false,
89
+ noCommit: false,
88
90
  });
89
91
  return;
90
92
  }
@@ -19,6 +19,7 @@ import { CLIError } from '../lib/cli-error.js';
19
19
  import { FACTORY_ORG, PROFILES } from '../lib/constants.js';
20
20
  import { parseLockfile, writeInitialLockfile } from '../lib/lockfile.js';
21
21
  import { applyPackageJsonEdits } from '../lib/package-json.js';
22
+ import { renderDerivedReadme } from '../lib/readme.js';
22
23
  import { runPreflight } from '../lib/preflight.js';
23
24
  import { downloadProfileTarball, moveContentsInto, stageProfileTarball } from '../lib/unpack.js';
24
25
  import { validateProjectName } from '../lib/validate-name.js';
@@ -100,13 +101,19 @@ export async function runNew(name) {
100
101
  await execa('git', ['init'], { cwd: destDir });
101
102
  // Rename package.json.name + inject factory:update (surgical, §7.4).
102
103
  const pkgPath = path.join(destDir, 'package.json');
103
- if (existsSync(pkgPath)) {
104
+ const hasPackageJson = existsSync(pkgPath);
105
+ if (hasPackageJson) {
104
106
  const { content, scriptAlreadyPresent } = applyPackageJsonEdits(readFileSync(pkgPath, 'utf8'), validName);
105
107
  writeFileSync(pkgPath, content, 'utf8');
106
108
  if (scriptAlreadyPresent) {
107
109
  console.warn('Aviso: `factory:update` ya existía en package.json con otro valor; no se sobrescribió.');
108
110
  }
109
111
  }
112
+ // Replace the inherited README. The `full` tarball ships the Factory's own
113
+ // README (it documents the distribution CLI), which is wrong inside a derived
114
+ // app — overwrite it with one scoped to this project. Written either way so a
115
+ // brain-only `core` project also gets a README.
116
+ writeFileSync(path.join(destDir, 'README.md'), renderDerivedReadme(validName, profile, hasPackageJson), 'utf8');
110
117
  // Write the initial lockfile = the tarball's embedded manifest verbatim
111
118
  // (no hash recompute — that's DIST-005).
112
119
  writeInitialLockfile(destDir, manifestRaw);
@@ -36,11 +36,14 @@
36
36
  import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
37
37
  import { tmpdir } from 'node:os';
38
38
  import path from 'node:path';
39
+ import { execa } from 'execa';
39
40
  import prompts from 'prompts';
40
41
  import { applyPlan, clearUpdateState, defaultBackupDir, hasUpdateState, readUpdateState, validateStagedManifest, writeUpdateState, } from '../lib/atomic-swap.js';
41
42
  import { collectClaudePaths } from '../lib/claude-paths.js';
42
43
  import { CLIError } from '../lib/cli-error.js';
43
- import { CLAUDE_MD_FILE, PROFILES } from '../lib/constants.js';
44
+ import { CLAUDE_MD_FILE, GITATTRIBUTES_FILE, LOCKFILE_FILE, PROFILES, TIMEKAST_DIR, } from '../lib/constants.js';
45
+ import { decideCommit } from '../lib/commit-decision.js';
46
+ import { extractManagedBlock, syncManagedBlock } from '../lib/gitattributes.js';
44
47
  import { diffLockfiles, hasLockfile, normalizeThenHash, planAutoRegister, readLockfile, writeLockfile, } from '../lib/lockfile.js';
45
48
  import { detectProfile, insertFactoryUpdateScript, setAgentKitVersion, } from '../lib/package-json.js';
46
49
  import { runPreflight } from '../lib/preflight.js';
@@ -55,6 +58,8 @@ export function parseUpdateFlags(argv) {
55
58
  verify: argv.includes('--verify'),
56
59
  core: argv.includes('--core'),
57
60
  full: argv.includes('--full'),
61
+ commit: argv.includes('--commit'),
62
+ noCommit: argv.includes('--no-commit'),
58
63
  };
59
64
  }
60
65
  /** Default acquire: download the sticky-profile tarball, stage + validate it. */
@@ -149,11 +154,66 @@ function runVerify(rootDir) {
149
154
  for (const p of drifted)
150
155
  console.log(` • modificado localmente: ${p}`);
151
156
  }
157
+ /**
158
+ * After a clean update, optionally commit the refreshed brain (local, NEVER push).
159
+ * Decision (`commit-decision.ts`): `--no-commit`→skip · `--commit`→commit · interactive
160
+ * → prompt y/n · headless → commit by default (the Agent Server needs it). Stages ONLY
161
+ * what the update touched (the applied manifest paths + lockfile + package.json +
162
+ * .gitattributes) — never `git add -A`. Best-effort: no identity / no repo → warn + skip.
163
+ */
164
+ async function maybeCommitBrain(rootDir, version, appliedPaths, flags) {
165
+ const decision = decideCommit({
166
+ commit: flags.commit,
167
+ noCommit: flags.noCommit,
168
+ isTTY: Boolean(process.stdout.isTTY),
169
+ });
170
+ if (decision === 'skip')
171
+ return;
172
+ let doCommit = decision === 'commit';
173
+ if (decision === 'prompt') {
174
+ const { yes } = await prompts({
175
+ type: 'confirm',
176
+ name: 'yes',
177
+ message: `¿Commitear el cerebro actualizado (v${version})? No se pushea.`,
178
+ initial: true,
179
+ });
180
+ doCommit = yes === true;
181
+ }
182
+ if (!doCommit)
183
+ return;
184
+ const candidates = [
185
+ ...appliedPaths,
186
+ `${TIMEKAST_DIR}/${LOCKFILE_FILE}`,
187
+ ...(existsSync(path.join(rootDir, 'package.json')) ? ['package.json'] : []),
188
+ ...(existsSync(path.join(rootDir, GITATTRIBUTES_FILE)) ? [GITATTRIBUTES_FILE] : []),
189
+ ];
190
+ try {
191
+ await execa('git', ['add', '--', ...candidates], { cwd: rootDir, reject: false });
192
+ // Nothing staged (e.g. a no-op update) → no empty commit.
193
+ const staged = await execa('git', ['diff', '--cached', '--quiet'], {
194
+ cwd: rootDir,
195
+ reject: false,
196
+ });
197
+ if (staged.exitCode === 0)
198
+ return;
199
+ const res = await execa('git', ['commit', '-m', `chore: factory update v${version}`], {
200
+ cwd: rootDir,
201
+ reject: false,
202
+ });
203
+ if (res.exitCode === 0)
204
+ console.log(`✔ Cerebro commiteado (v${version}).`);
205
+ else
206
+ console.warn('Aviso: no se pudo commitear (¿falta identidad git o no es un repo?); el cerebro se instaló igual.');
207
+ }
208
+ catch {
209
+ console.warn('Aviso: no se pudo commitear; el cerebro se instaló igual.');
210
+ }
211
+ }
152
212
  /**
153
213
  * `--resume`: re-apply a persisted mid-flight state without re-downloading
154
214
  * (design §7.5). Reads the staged dir + plan from `.timekast/.update-state.json`.
155
215
  */
156
- function runResume(rootDir) {
216
+ async function runResume(rootDir, flags) {
157
217
  const state = readUpdateState(rootDir);
158
218
  if (!existsSync(state.stagedDir)) {
159
219
  throw new CLIError('El directorio temporal del update anterior ya no existe; vuelve a correr `factory update` sin `--resume`.');
@@ -168,9 +228,9 @@ function runResume(rootDir) {
168
228
  applyPlan(rootDir, state.stagedDir, state.plan, state.backupDir);
169
229
  writeLockfile(rootDir, newLock);
170
230
  clearUpdateState(rootDir);
171
- const pkgPath = path.join(rootDir, 'package.json');
172
- if (existsSync(pkgPath))
173
- maintainDerivedPkg(pkgPath, newLock.version);
231
+ // Maintain dotfiles BEFORE removing the staged dir (the .gitattributes source).
232
+ maintainDerivedDotfiles(rootDir, state.stagedDir, newLock.version);
233
+ await maybeCommitBrain(rootDir, newLock.version, [...state.plan.writes, ...state.plan.deletes], flags);
174
234
  rmSync(state.stagedDir, { recursive: true, force: true });
175
235
  console.log('✔ `update` retomado y completado.');
176
236
  }
@@ -191,15 +251,31 @@ function warnOnScriptConflict(action, _pkgPath) {
191
251
  }
192
252
  }
193
253
  /**
194
- * After a sync, maintain the derived project's package.json: ensure the
195
- * `factory:update` script (warn on a divergent value) and set `agentKitVersion`
196
- * to the just-installed brain version, mirroring the lockfile's `version`. Run on
197
- * every update, so the field tracks each `factory:update` (and a derivative whose
198
- * field was missing or stale gets reconciled same shape as the Factory's own
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.
260
+ */
261
+ function maintainDerivedDotfiles(rootDir, stagedDir, agentKitVersion) {
262
+ maintainDerivedPkg(rootDir, agentKitVersion);
263
+ syncDerivedGitattributes(rootDir, stagedDir);
264
+ }
265
+ /**
266
+ * Maintain the derived project's package.json: ensure the `factory:*` scripts
267
+ * (warn on a divergent `factory:update`) and set `agentKitVersion` to the
268
+ * just-installed brain version, mirroring the lockfile's `version`. Run on every
269
+ * update, so the field tracks each `factory:update` (and a derivative whose field
270
+ * was missing or stale gets reconciled — same shape as the Factory's own
199
271
  * package.json). `factoryVersion` (the frozen birth stamp) and `version` (the app
200
- * semver) are untouched.
272
+ * semver) are untouched. No-op (not an error) when the repo has no package.json
273
+ * (a non-Node derivative).
201
274
  */
202
- function maintainDerivedPkg(pkgPath, agentKitVersion) {
275
+ function maintainDerivedPkg(rootDir, agentKitVersion) {
276
+ const pkgPath = path.join(rootDir, 'package.json');
277
+ if (!existsSync(pkgPath))
278
+ return;
203
279
  // Tolerant (mirrors `add`): a malformed package.json must NOT throw AFTER the
204
280
  // brain was already applied + the lockfile written — the install succeeded; the
205
281
  // script/version mirror is best-effort. Warn and move on.
@@ -212,6 +288,35 @@ function maintainDerivedPkg(pkgPath, agentKitVersion) {
212
288
  'Corrige package.json y vuelve a correr `factory update` para sincronizar agentKitVersion.');
213
289
  }
214
290
  }
291
+ /**
292
+ * Sync the Factory's managed `.gitattributes` block (the EOL/binary rules that
293
+ * keep the kit's committed binaries from being corrupted by CRLF→LF normalization)
294
+ * into the derived repo, preserving the dev's own rules. The canonical block is
295
+ * READ from the staged tarball (single SSOT), so the rules are never duplicated in
296
+ * the CLI. Runs even without package.json (a non-Node derivative needs the rules
297
+ * too). Best-effort like the package.json mirror: a tarball that predates the
298
+ * managed block (no markers → no block) is a silent no-op, and a write failure
299
+ * never fails an already-applied update.
300
+ */
301
+ function syncDerivedGitattributes(rootDir, stagedDir) {
302
+ try {
303
+ const srcPath = path.join(stagedDir, GITATTRIBUTES_FILE);
304
+ if (!existsSync(srcPath))
305
+ return;
306
+ const block = extractManagedBlock(readFileSync(srcPath, 'utf8'));
307
+ if (!block)
308
+ return;
309
+ const { action } = syncManagedBlock(rootDir, block);
310
+ if (action === 'unchanged')
311
+ return;
312
+ console.log(action === 'created'
313
+ ? '✔ `.gitattributes` creado con las reglas de normalización del kit.'
314
+ : '✔ `.gitattributes`: bloque de reglas del kit sincronizado.');
315
+ }
316
+ catch {
317
+ console.warn('Aviso: no se pudo sincronizar `.gitattributes`; el cerebro se instaló igual.');
318
+ }
319
+ }
215
320
  /** Print the deletes + kept-retired (design §7.6 — visible) + a one-line summary. */
216
321
  function reportSummary(diff, manifest) {
217
322
  if (diff.deleteSilent.length > 0) {
@@ -257,7 +362,7 @@ export async function runUpdate(flags, deps = {}) {
257
362
  if (!hasUpdateState(rootDir)) {
258
363
  throw new CLIError('No hay un `update` pendiente para retomar.');
259
364
  }
260
- runResume(rootDir);
365
+ await runResume(rootDir, flags);
261
366
  return;
262
367
  }
263
368
  // Preflight (gh) before any download — reused, not reimplemented.
@@ -335,9 +440,8 @@ export async function runUpdate(flags, deps = {}) {
335
440
  // (agentKitVersion advances; factoryVersion stays frozen).
336
441
  writeLockfile(rootDir, stampBirth(oldLock, manifest));
337
442
  clearUpdateState(rootDir);
338
- const pkgPath = path.join(rootDir, 'package.json');
339
- if (existsSync(pkgPath))
340
- maintainDerivedPkg(pkgPath, manifest.version);
443
+ maintainDerivedDotfiles(rootDir, stagedDir, manifest.version);
444
+ await maybeCommitBrain(rootDir, manifest.version, [...plan.writes, ...plan.deletes], flags);
341
445
  reportSummary(diff, manifest);
342
446
  }
343
447
  finally {
@@ -394,9 +498,7 @@ async function applyLegacy(rootDir, stagedDir, manifest) {
394
498
  // the just-installed files) PLUS the birth stamp. A legacy auto-register is the
395
499
  // first sync this repo records, so the birth seal = the manifest's version.
396
500
  writeLockfile(rootDir, { ...manifest, factoryVersion: manifest.version });
397
- const pkgPath = path.join(rootDir, 'package.json');
398
- if (existsSync(pkgPath))
399
- maintainDerivedPkg(pkgPath, manifest.version);
501
+ maintainDerivedDotfiles(rootDir, stagedDir, manifest.version);
400
502
  if (claudeMdExists) {
401
503
  console.log('\nConservé tu `CLAUDE.md` (no se sobrescribió). Verifica que importe las rules del kit ' +
402
504
  '(`@.claude/rules/*`); corre `factory doctor` para detectar rules sin importar.');
@@ -0,0 +1,7 @@
1
+ export function decideCommit({ commit, noCommit, isTTY }) {
2
+ if (noCommit)
3
+ return 'skip';
4
+ if (commit)
5
+ return 'commit';
6
+ return isTTY ? 'prompt' : 'commit';
7
+ }
@@ -30,6 +30,23 @@ export const LOCKFILE_FILE = 'lockfile.json';
30
30
  * exists on disk (path-match path). See `diffLockfiles` + the install commands.
31
31
  */
32
32
  export const CLAUDE_MD_FILE = 'CLAUDE.md';
33
+ /**
34
+ * Repo-root `.gitattributes`. NOT a tracked manifest file (absent from `track`),
35
+ * so it never flows through the diff/lockfile engine — a derived repo's own rules
36
+ * would otherwise read as "locally edited" and conflict on every update. Instead a
37
+ * delimited managed block (markers below) is synced surgically post-apply, like
38
+ * `package.json` scripts: the block is replaced verbatim, everything outside it is
39
+ * preserved byte-for-byte. See `syncManagedBlock` in `lib/gitattributes.ts`.
40
+ */
41
+ export const GITATTRIBUTES_FILE = '.gitattributes';
42
+ /**
43
+ * Managed-block sentinels. These are PREFIXES (the live source lines carry extra
44
+ * descriptive text + trailing `>>>`/`<<<`), matched with `startsWith` so detection
45
+ * survives edits to the comment copy. Do not change without a migration: a derived
46
+ * repo's existing block is located by these prefixes.
47
+ */
48
+ export const MANAGED_BLOCK_START = '# >>> timekast-factory managed';
49
+ export const MANAGED_BLOCK_END = '# <<< timekast-factory managed';
33
50
  /** The scripts the CLI injects into a derived project's package.json. */
34
51
  export const UPDATE_SCRIPT_NAME = 'factory:update';
35
52
  // `npx` so the script resolves in a fresh derived repo where @timekast/factory
@@ -0,0 +1,101 @@
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.
5
+ *
6
+ * Why not the lockfile/`track` engine: `.gitattributes` is dual-owned at the
7
+ * 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
+ * local edit and conflict on every update. Instead the block is synced as an
10
+ * idempotent post-apply step, outside `diffLockfiles`/`applyPlan`.
11
+ *
12
+ * The canonical block is the single SSOT: it is READ from the `.gitattributes`
13
+ * that ships in the staged tarball (see `extractManagedBlock`), never duplicated
14
+ * as a CLI constant. A derived repo's existing block is located by the
15
+ * `MANAGED_BLOCK_START` / `_END` sentinels (prefix match), so the descriptive
16
+ * comment copy can evolve without breaking detection.
17
+ */
18
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
19
+ import path from 'node:path';
20
+ import { GITATTRIBUTES_FILE, MANAGED_BLOCK_END, MANAGED_BLOCK_START } from './constants.js';
21
+ const isStart = (line) => line.trimStart().startsWith(MANAGED_BLOCK_START);
22
+ const isEnd = (line) => line.trimStart().startsWith(MANAGED_BLOCK_END);
23
+ /** Drop trailing all-whitespace lines (so appends don't pile up blank lines). */
24
+ function stripTrailingEmpty(lines) {
25
+ const out = [...lines];
26
+ while (out.length > 0 && out[out.length - 1].trim() === '')
27
+ out.pop();
28
+ return out;
29
+ }
30
+ /**
31
+ * Extract the managed block (markers inclusive) from a `.gitattributes` content,
32
+ * or `null` if no start marker is present. Used on the well-formed staged source
33
+ * to obtain the canonical block to sync. If a start marker is found with no end
34
+ * marker, returns from start to EOF (defensive — the source is well-formed).
35
+ */
36
+ export function extractManagedBlock(content) {
37
+ const lines = content.replace(/\r\n/g, '\n').split('\n');
38
+ const start = lines.findIndex(isStart);
39
+ if (start === -1)
40
+ return null;
41
+ let end = -1;
42
+ for (let i = start + 1; i < lines.length; i++) {
43
+ if (isEnd(lines[i])) {
44
+ end = i;
45
+ break;
46
+ }
47
+ }
48
+ const slice = end === -1 ? lines.slice(start) : lines.slice(start, end + 1);
49
+ return slice.join('\n');
50
+ }
51
+ /**
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.
57
+ *
58
+ * - no file / empty file → `created` (block only)
59
+ * - file without a start marker → `inserted` (block appended after a blank line)
60
+ * - start + end markers → `updated` (content between markers replaced)
61
+ * - start without end (dev clobbered half a line) → `updated`, regenerated
62
+ * cleanly from start to EOF (NOT appended — avoids a dangling start marker)
63
+ */
64
+ export function syncManagedBlock(rootDir, sourceBlock) {
65
+ const filePath = path.join(rootDir, GITATTRIBUTES_FILE);
66
+ const raw = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
67
+ const block = sourceBlock.replace(/\r\n/g, '\n').replace(/\n+$/, '');
68
+ let result;
69
+ let action;
70
+ if (raw.trim() === '') {
71
+ result = `${block}\n`;
72
+ action = 'created';
73
+ }
74
+ else {
75
+ const lines = raw.replace(/\r\n/g, '\n').split('\n');
76
+ const start = lines.findIndex(isStart);
77
+ if (start === -1) {
78
+ const before = stripTrailingEmpty(lines);
79
+ result = `${before.join('\n')}\n\n${block}\n`;
80
+ action = 'inserted';
81
+ }
82
+ else {
83
+ // Replace from start to the end marker, or to EOF when the end marker is
84
+ // missing (malformed block → regenerate clean rather than append).
85
+ let end = lines.length - 1;
86
+ for (let i = start + 1; i < lines.length; i++) {
87
+ if (isEnd(lines[i])) {
88
+ end = i;
89
+ break;
90
+ }
91
+ }
92
+ const merged = [...lines.slice(0, start), ...block.split('\n'), ...lines.slice(end + 1)];
93
+ result = `${stripTrailingEmpty(merged).join('\n')}\n`;
94
+ action = 'updated';
95
+ }
96
+ }
97
+ if (result === raw)
98
+ return { action: 'unchanged' };
99
+ writeFileSync(filePath, result, 'utf8');
100
+ return { action };
101
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Render a README scoped to a freshly-bootstrapped derived project.
3
+ *
4
+ * Why: the `full` tarball ships the Factory's own README (it documents the
5
+ * distribution CLI — `factory new` etc.), which is wrong inside a derived app.
6
+ * `factory new` overwrites it with this one, scoped to the project: how to run
7
+ * it (full) and how to keep the `.claude/` brain current. Pure + deterministic
8
+ * so it unit-tests without mocks (mirrors `publish-core.ts`).
9
+ */
10
+ const FACTORY_URL = 'https://github.com/TimeKast/TimeKast-Factory';
11
+ /** The `factory *` invocation prefix: a Node project gets the `pnpm` scripts; otherwise `npx`. */
12
+ function factoryCmd(sub, hasPackageJson) {
13
+ return hasPackageJson ? `pnpm factory:${sub}` : `npx @timekast/factory ${sub}`;
14
+ }
15
+ /**
16
+ * Build the derived project's README markdown.
17
+ *
18
+ * @param name The project name (already validated/slugged by the caller).
19
+ * @param profile `full` ships the Next.js boilerplate; `core` is brain-only.
20
+ * @param hasPackageJson Whether the project carries a `package.json` (drives `pnpm` vs `npx`).
21
+ */
22
+ export function renderDerivedReadme(name, profile, hasPackageJson) {
23
+ const isFull = profile === 'full';
24
+ const dev = isFull
25
+ ? `## Desarrollo
26
+
27
+ \`\`\`bash
28
+ pnpm install
29
+ cp .env.example .env.local # edita DATABASE_URL + AUTH_SECRET
30
+ pnpm dev
31
+ \`\`\`
32
+
33
+ Abre [http://localhost:3000](http://localhost:3000).
34
+
35
+ `
36
+ : '';
37
+ const stack = isFull
38
+ ? `Boilerplate Next.js 16 + TypeScript + Drizzle ORM + Tailwind + NextAuth, más el cerebro AI-first \`.claude/\` (Claude Code).`
39
+ : `Este repo usa el cerebro AI-first \`.claude/\` de TimeKast Factory (reglas + skills + workflows para Claude Code), sin el boilerplate Next.js.`;
40
+ return `# ${name}
41
+
42
+ > Proyecto creado con [TimeKast Factory](${FACTORY_URL}).
43
+
44
+ ${stack}
45
+
46
+ ${dev}## Mantener el cerebro al día
47
+
48
+ El cerebro \`.claude/\` se actualiza sin pisar tu trabajo local:
49
+
50
+ \`\`\`bash
51
+ ${factoryCmd('update', hasPackageJson)} # actualizar a la última versión
52
+ ${factoryCmd('status', hasPackageJson)} # versión instalada vs. disponible
53
+ ${factoryCmd('doctor', hasPackageJson)} # diagnóstico (huérfanos, conflictos, seguridad)
54
+ \`\`\`
55
+
56
+ ---
57
+
58
+ _Bootstrapped from TimeKast Factory._
59
+ `;
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timekast/factory",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Public, thin CLI to bootstrap and maintain TimeKast Factory derived projects.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",