@timekast/factory 1.3.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.
- package/dist/commands/add.js +2 -0
- package/dist/commands/new.js +8 -1
- package/dist/commands/update.js +101 -9
- package/dist/lib/commit-decision.js +7 -0
- package/dist/lib/constants.js +9 -0
- package/dist/lib/gitattributes.js +14 -13
- 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, PRETTIERIGNORE_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,67 @@ 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
|
+
...(existsSync(path.join(rootDir, PRETTIERIGNORE_FILE)) ? [PRETTIERIGNORE_FILE] : []),
|
|
190
|
+
];
|
|
191
|
+
try {
|
|
192
|
+
await execa('git', ['add', '--', ...candidates], { cwd: rootDir, reject: false });
|
|
193
|
+
// Nothing staged (e.g. a no-op update) → no empty commit.
|
|
194
|
+
const staged = await execa('git', ['diff', '--cached', '--quiet'], {
|
|
195
|
+
cwd: rootDir,
|
|
196
|
+
reject: false,
|
|
197
|
+
});
|
|
198
|
+
if (staged.exitCode === 0)
|
|
199
|
+
return;
|
|
200
|
+
const res = await execa('git', ['commit', '-m', `chore: factory update v${version}`], {
|
|
201
|
+
cwd: rootDir,
|
|
202
|
+
reject: false,
|
|
203
|
+
});
|
|
204
|
+
if (res.exitCode === 0)
|
|
205
|
+
console.log(`✔ Cerebro commiteado (v${version}).`);
|
|
206
|
+
else
|
|
207
|
+
console.warn('Aviso: no se pudo commitear (¿falta identidad git o no es un repo?); el cerebro se instaló igual.');
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
console.warn('Aviso: no se pudo commitear; el cerebro se instaló igual.');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
153
213
|
/**
|
|
154
214
|
* `--resume`: re-apply a persisted mid-flight state without re-downloading
|
|
155
215
|
* (design §7.5). Reads the staged dir + plan from `.timekast/.update-state.json`.
|
|
156
216
|
*/
|
|
157
|
-
function runResume(rootDir) {
|
|
217
|
+
async function runResume(rootDir, flags) {
|
|
158
218
|
const state = readUpdateState(rootDir);
|
|
159
219
|
if (!existsSync(state.stagedDir)) {
|
|
160
220
|
throw new CLIError('El directorio temporal del update anterior ya no existe; vuelve a correr `factory update` sin `--resume`.');
|
|
@@ -171,6 +231,7 @@ function runResume(rootDir) {
|
|
|
171
231
|
clearUpdateState(rootDir);
|
|
172
232
|
// Maintain dotfiles BEFORE removing the staged dir (the .gitattributes source).
|
|
173
233
|
maintainDerivedDotfiles(rootDir, state.stagedDir, newLock.version);
|
|
234
|
+
await maybeCommitBrain(rootDir, newLock.version, [...state.plan.writes, ...state.plan.deletes], flags);
|
|
174
235
|
rmSync(state.stagedDir, { recursive: true, force: true });
|
|
175
236
|
console.log('✔ `update` retomado y completado.');
|
|
176
237
|
}
|
|
@@ -191,16 +252,18 @@ function warnOnScriptConflict(action, _pkgPath) {
|
|
|
191
252
|
}
|
|
192
253
|
}
|
|
193
254
|
/**
|
|
194
|
-
* After a sync, maintain the derived project's co-owned dotfiles — `package.json
|
|
195
|
-
* AND `.
|
|
196
|
-
* all THREE update paths (main, legacy auto-register, resume) are
|
|
197
|
-
* construction: a per-command call already regressed once (legacy +
|
|
198
|
-
* missed). `stagedDir` is the freshly-unpacked tarball, the source of
|
|
199
|
-
*
|
|
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.
|
|
200
262
|
*/
|
|
201
263
|
function maintainDerivedDotfiles(rootDir, stagedDir, agentKitVersion) {
|
|
202
264
|
maintainDerivedPkg(rootDir, agentKitVersion);
|
|
203
265
|
syncDerivedGitattributes(rootDir, stagedDir);
|
|
266
|
+
syncDerivedPrettierignore(rootDir, stagedDir);
|
|
204
267
|
}
|
|
205
268
|
/**
|
|
206
269
|
* Maintain the derived project's package.json: ensure the `factory:*` scripts
|
|
@@ -257,6 +320,34 @@ function syncDerivedGitattributes(rootDir, stagedDir) {
|
|
|
257
320
|
console.warn('Aviso: no se pudo sincronizar `.gitattributes`; el cerebro se instaló igual.');
|
|
258
321
|
}
|
|
259
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
|
+
}
|
|
260
351
|
/** Print the deletes + kept-retired (design §7.6 — visible) + a one-line summary. */
|
|
261
352
|
function reportSummary(diff, manifest) {
|
|
262
353
|
if (diff.deleteSilent.length > 0) {
|
|
@@ -302,7 +393,7 @@ export async function runUpdate(flags, deps = {}) {
|
|
|
302
393
|
if (!hasUpdateState(rootDir)) {
|
|
303
394
|
throw new CLIError('No hay un `update` pendiente para retomar.');
|
|
304
395
|
}
|
|
305
|
-
runResume(rootDir);
|
|
396
|
+
await runResume(rootDir, flags);
|
|
306
397
|
return;
|
|
307
398
|
}
|
|
308
399
|
// Preflight (gh) before any download — reused, not reimplemented.
|
|
@@ -381,6 +472,7 @@ export async function runUpdate(flags, deps = {}) {
|
|
|
381
472
|
writeLockfile(rootDir, stampBirth(oldLock, manifest));
|
|
382
473
|
clearUpdateState(rootDir);
|
|
383
474
|
maintainDerivedDotfiles(rootDir, stagedDir, manifest.version);
|
|
475
|
+
await maybeCommitBrain(rootDir, manifest.version, [...plan.writes, ...plan.deletes], flags);
|
|
384
476
|
reportSummary(diff, manifest);
|
|
385
477
|
}
|
|
386
478
|
finally {
|
package/dist/lib/constants.js
CHANGED
|
@@ -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
|
|
3
|
-
*
|
|
4
|
-
* the delimited block is replaced
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
53
|
-
* preserving every rule outside the managed block. The whole
|
|
54
|
-
* to LF (it lives at repo root, outside `.claude/** text eol=lf`,
|
|
55
|
-
* packed with CRLF on an autocrlf machine is normalized here).
|
|
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,
|
|
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;
|
|
@@ -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
|
+
}
|