@timekast/factory 1.3.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.
- package/dist/commands/add.js +2 -0
- package/dist/commands/new.js +8 -1
- package/dist/commands/update.js +64 -3
- package/dist/lib/commit-decision.js +7 -0
- package/dist/lib/readme.js +60 -0
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
package/dist/commands/new.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/commands/update.js
CHANGED
|
@@ -36,11 +36,13 @@
|
|
|
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, GITATTRIBUTES_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';
|
|
44
46
|
import { extractManagedBlock, syncManagedBlock } from '../lib/gitattributes.js';
|
|
45
47
|
import { diffLockfiles, hasLockfile, normalizeThenHash, planAutoRegister, readLockfile, writeLockfile, } from '../lib/lockfile.js';
|
|
46
48
|
import { detectProfile, insertFactoryUpdateScript, setAgentKitVersion, } from '../lib/package-json.js';
|
|
@@ -56,6 +58,8 @@ export function parseUpdateFlags(argv) {
|
|
|
56
58
|
verify: argv.includes('--verify'),
|
|
57
59
|
core: argv.includes('--core'),
|
|
58
60
|
full: argv.includes('--full'),
|
|
61
|
+
commit: argv.includes('--commit'),
|
|
62
|
+
noCommit: argv.includes('--no-commit'),
|
|
59
63
|
};
|
|
60
64
|
}
|
|
61
65
|
/** Default acquire: download the sticky-profile tarball, stage + validate it. */
|
|
@@ -150,11 +154,66 @@ function runVerify(rootDir) {
|
|
|
150
154
|
for (const p of drifted)
|
|
151
155
|
console.log(` • modificado localmente: ${p}`);
|
|
152
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
|
+
}
|
|
153
212
|
/**
|
|
154
213
|
* `--resume`: re-apply a persisted mid-flight state without re-downloading
|
|
155
214
|
* (design §7.5). Reads the staged dir + plan from `.timekast/.update-state.json`.
|
|
156
215
|
*/
|
|
157
|
-
function runResume(rootDir) {
|
|
216
|
+
async function runResume(rootDir, flags) {
|
|
158
217
|
const state = readUpdateState(rootDir);
|
|
159
218
|
if (!existsSync(state.stagedDir)) {
|
|
160
219
|
throw new CLIError('El directorio temporal del update anterior ya no existe; vuelve a correr `factory update` sin `--resume`.');
|
|
@@ -171,6 +230,7 @@ function runResume(rootDir) {
|
|
|
171
230
|
clearUpdateState(rootDir);
|
|
172
231
|
// Maintain dotfiles BEFORE removing the staged dir (the .gitattributes source).
|
|
173
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
|
}
|
|
@@ -302,7 +362,7 @@ export async function runUpdate(flags, deps = {}) {
|
|
|
302
362
|
if (!hasUpdateState(rootDir)) {
|
|
303
363
|
throw new CLIError('No hay un `update` pendiente para retomar.');
|
|
304
364
|
}
|
|
305
|
-
runResume(rootDir);
|
|
365
|
+
await runResume(rootDir, flags);
|
|
306
366
|
return;
|
|
307
367
|
}
|
|
308
368
|
// Preflight (gh) before any download — reused, not reimplemented.
|
|
@@ -381,6 +441,7 @@ export async function runUpdate(flags, deps = {}) {
|
|
|
381
441
|
writeLockfile(rootDir, stampBirth(oldLock, manifest));
|
|
382
442
|
clearUpdateState(rootDir);
|
|
383
443
|
maintainDerivedDotfiles(rootDir, stagedDir, manifest.version);
|
|
444
|
+
await maybeCommitBrain(rootDir, manifest.version, [...plan.writes, ...plan.deletes], flags);
|
|
384
445
|
reportSummary(diff, manifest);
|
|
385
446
|
}
|
|
386
447
|
finally {
|
|
@@ -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
|
+
}
|