@timekast/factory 1.12.0 → 1.14.2

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.
@@ -18,9 +18,11 @@
18
18
  */
19
19
  import { existsSync, readFileSync, statSync } from 'node:fs';
20
20
  import path from 'node:path';
21
+ import { CLIError } from '../lib/cli-error.js';
21
22
  import { collectClaudePaths } from '../lib/claude-paths.js';
22
23
  import { CLAUDE_MD_FILE } from '../lib/constants.js';
23
24
  import { hasLockfile, normalizeThenHash, readLockfile } from '../lib/lockfile.js';
25
+ import { runLauncherPreflight, } from '../lib/preflight.js';
24
26
  import { detectRepo } from '../lib/repo-detection.js';
25
27
  import { BETA_BANNER } from './status.js';
26
28
  /** Matches a flat always-on rule file shipped by the kit (`.claude/rules/<name>.md`). */
@@ -44,6 +46,13 @@ export const A1_WARNING = 'Aviso de seguridad (A1): el boilerplate `src/` y sus
44
46
  'dependencia) NO llega por este canal. El canal de parches de `src/` es EPIC 2 ' +
45
47
  '(update agentico con merge inteligente), aún no disponible. Mantén `src/` y tus ' +
46
48
  'dependencias al día por tu cuenta mientras tanto.';
49
+ /** Parse `doctor`'s argv slice into flags. Unknown flags are ignored. */
50
+ export function parseDoctorFlags(argv) {
51
+ return {
52
+ launcher: argv.includes('--launcher'),
53
+ json: argv.includes('--json'),
54
+ };
55
+ }
47
56
  /**
48
57
  * Concatenate the body of the reserved "on-demand reference" section of CLAUDE.md
49
58
  * — the lines under a heading matching `ON_DEMAND_HEADING_RE`, until the next
@@ -220,8 +229,87 @@ function renderDoctor(result) {
220
229
  /**
221
230
  * Run `factory doctor`. Always exits 0 — a missing lockfile is advisory, not a
222
231
  * failure (DIST-006 §8). The A1 notice is emitted on every run.
232
+ *
233
+ * Synchronous: the standard doctor report has no network/async seam. The
234
+ * additive `--launcher` flow is async (it spawns `gh`) and lives in its own
235
+ * entrypoint, {@link runLauncherDoctor}.
223
236
  */
224
237
  export function runDoctor(deps = {}) {
225
238
  const result = computeDoctor(deps);
226
239
  renderDoctor(result);
227
240
  }
241
+ // ---------------------------------------------------------------------------
242
+ // `factory doctor --launcher` (LTUI-002) — env preflight, any-platform/any-shell
243
+ // ---------------------------------------------------------------------------
244
+ /** The glyph per launcher-check status. */
245
+ const STATUS_GLYPH = {
246
+ ok: '✅',
247
+ fail: '❌',
248
+ skipped: '—',
249
+ warn: '🟡',
250
+ };
251
+ /**
252
+ * Render the launcher preflight report. Each check on its own line with a glyph,
253
+ * label and (for fail/skip) the actionable message. Closes with a one-line
254
+ * verdict. Exported so tests can assert the rendered text.
255
+ */
256
+ export function renderLauncherDoctor(checks) {
257
+ for (const check of checks) {
258
+ const glyph = STATUS_GLYPH[check.status];
259
+ if (check.status === 'ok') {
260
+ console.log(`${glyph} ${check.label} — ${check.message}`);
261
+ }
262
+ else {
263
+ // fail / skipped: surface the actionable message (multi-line indented).
264
+ const lines = check.message.split('\n');
265
+ console.log(`${glyph} ${check.label}: ${lines[0]}`);
266
+ for (const extra of lines.slice(1)) {
267
+ if (extra.trim())
268
+ console.log(` ${extra}`);
269
+ }
270
+ }
271
+ }
272
+ const hasFail = checks.some((c) => c.status === 'fail');
273
+ const hasWarn = checks.some((c) => c.status === 'warn');
274
+ console.log('');
275
+ if (hasFail) {
276
+ console.log('Entorno incompleto: corrige los checks marcados ❌ y vuelve a intentar.');
277
+ }
278
+ else if (hasWarn) {
279
+ console.log('Entorno listo. Revisa los avisos 🟡 (no bloquean el launcher).');
280
+ }
281
+ else {
282
+ console.log('Entorno listo para usar el launcher.');
283
+ }
284
+ }
285
+ /**
286
+ * Run `factory doctor --launcher`: the cross-platform environment preflight
287
+ * (LTUI-002). Renders the four checks in order (Node ≥ 22 → gh → auth → org)
288
+ * with ✅ / ❌, then exits 0 if all pass or throws a `CLIError` (exit 1) if any
289
+ * fails — so a CI step or the TUI (LTUI-001) can branch on the exit code.
290
+ *
291
+ * In `--json` mode (TGUI-003) the `LauncherCheck[]` is written as a JSON array
292
+ * to stdout and NOTHING else (the human renderer is suppressed so stdout stays
293
+ * pure-parseable JSON). It ALWAYS exits 0 — a failed check is a `status: 'fail'`
294
+ * element the GUI reads, never a non-zero exit; the exit-1 contract is preserved
295
+ * only for the interactive / CI path (no `--json`). `runLauncherPreflight` is
296
+ * non-fail-fast (returns the full report instead of throwing), so the JSON path
297
+ * is fail-open by construction.
298
+ *
299
+ * `deps` is the same injectable seam as `runLauncherPreflight` (tests drive the
300
+ * Node version + the gh steps without spawning sub-processes).
301
+ */
302
+ export async function runLauncherDoctor(deps = {}, json = false) {
303
+ const checks = await runLauncherPreflight(deps);
304
+ if (json) {
305
+ process.stdout.write(`${JSON.stringify(checks)}\n`);
306
+ return;
307
+ }
308
+ renderLauncherDoctor(checks);
309
+ if (checks.some((c) => c.status === 'fail')) {
310
+ // Non-zero exit without a duplicate error dump: the per-check ❌ lines above
311
+ // already told the user what to fix. The empty message keeps the top-level
312
+ // handler from printing a second "✖ ..." block.
313
+ throw new CLIError('', 1);
314
+ }
315
+ }
@@ -5,7 +5,8 @@
5
5
  * and is non-empty → preflight (gh) → choose profile (full | core) → create the
6
6
  * GitHub repo → download the profile tarball → unpack into `./<name>/` (no
7
7
  * Factory `.git/`) → `git init` → rename `package.json.name` → write the initial
8
- * lockfile → inject `factory:update` → initial commit (non-fatal if no git id).
8
+ * lockfile → inject `factory:update` → initial commit → wire `origin` + push `main`
9
+ * (the last two non-fatal if no git id / no network; provision re-establishes them).
9
10
  *
10
11
  * Expected failures throw `CLIError`; the top-level handler prints the message
11
12
  * and exits with the carried code.
@@ -18,6 +19,7 @@ import prompts from 'prompts';
18
19
  import { CLIError } from '../lib/cli-error.js';
19
20
  import { FACTORY_ORG, PROFILES } from '../lib/constants.js';
20
21
  import { parseLockfile, writeInitialLockfile } from '../lib/lockfile.js';
22
+ import { printProvisionPointer } from '../lib/provision-pointer.js';
21
23
  import { applyPackageJsonEdits, kitLocalScripts } from '../lib/package-json.js';
22
24
  import { renderDerivedReadme } from '../lib/readme.js';
23
25
  import { runPreflight } from '../lib/preflight.js';
@@ -135,16 +137,37 @@ export async function runNew(name, flags = { core: false, full: false }) {
135
137
  writeInitialLockfile(destDir, manifestRaw);
136
138
  // Initial commit (H3). Non-fatal: a fresh machine may lack git identity —
137
139
  // the project is already usable; warn and let the dev commit by hand (B2).
140
+ let committed = false;
138
141
  try {
139
142
  await execa('git', ['add', '-A'], { cwd: destDir });
140
143
  await execa('git', ['commit', '-m', 'chore: initial commit from TimeKast Factory'], {
141
144
  cwd: destDir,
142
145
  });
146
+ committed = true;
143
147
  }
144
148
  catch {
145
149
  console.warn('Aviso: no se pudo crear el commit inicial (¿falta `git config --global user.email`/`user.name`?). ' +
146
150
  'El proyecto está listo; commitea a mano cuando configures tu identidad git.');
147
151
  }
152
+ // First push: name the branch `main`, wire the `origin` remote (the repo
153
+ // `gh repo create` made is empty until now), and push. This is what lets the
154
+ // DOCS pipeline (discovery/design/backlog) commit + push BEFORE provision —
155
+ // `provision` later creates + pushes the `develop` branch when infra is set up.
156
+ // Non-fatal: no network / no auth → the local project is ready and `provision`
157
+ // re-establishes the substrate. Only attempted when the commit landed.
158
+ if (committed) {
159
+ try {
160
+ await execa('git', ['branch', '-M', 'main'], { cwd: destDir });
161
+ await execa('git', ['remote', 'add', 'origin', `https://github.com/${FACTORY_ORG}/${validName}.git`], {
162
+ cwd: destDir,
163
+ });
164
+ await execa('git', ['push', '-u', 'origin', 'main'], { cwd: destDir });
165
+ }
166
+ catch {
167
+ console.warn('Aviso: no se pudo pushear `main` al repo (¿sin red o sin permiso?). ' +
168
+ 'El repo local está listo; `factory provision` reestablece el remote y el push.');
169
+ }
170
+ }
148
171
  }
149
172
  finally {
150
173
  process.removeListener('SIGINT', onSignal);
@@ -154,4 +177,8 @@ export async function runNew(name, flags = { core: false, full: false }) {
154
177
  console.log(`\n✔ Proyecto \`${validName}\` listo (perfil \`${profile}\`, cerebro v${version}). ` +
155
178
  `Repo: ${FACTORY_ORG}/${validName}`);
156
179
  console.log(` cd ${validName}`);
180
+ // End-pointer to the next step (PVSKILL-002). Only reached on a successful run —
181
+ // an earlier throw (CLIError / Ctrl-C) never gets here. `new` has no `--yes` flag,
182
+ // so this always prints in a normal run (a pure pipe is still informative).
183
+ printProvisionPointer();
157
184
  }