@ritualai/cli 0.7.16 → 0.8.1

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.
Files changed (32) hide show
  1. package/dist/commands/init.js +406 -96
  2. package/dist/commands/init.js.map +1 -1
  3. package/dist/index.js +3 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/build-flow-explainer.js +226 -0
  6. package/dist/lib/build-flow-explainer.js.map +1 -0
  7. package/dist/lib/final-cta-box.js +224 -0
  8. package/dist/lib/final-cta-box.js.map +1 -0
  9. package/dist/lib/onboarding-state.js +140 -0
  10. package/dist/lib/onboarding-state.js.map +1 -0
  11. package/dist/lib/persona-picker.js +171 -0
  12. package/dist/lib/persona-picker.js.map +1 -0
  13. package/dist/lib/persona-samples.js +245 -0
  14. package/dist/lib/persona-samples.js.map +1 -0
  15. package/dist/lib/project-config.js.map +1 -1
  16. package/dist/lib/workspace-explainer.js +193 -0
  17. package/dist/lib/workspace-explainer.js.map +1 -0
  18. package/dist/lib/workspace-flow.js +8 -7
  19. package/dist/lib/workspace-flow.js.map +1 -1
  20. package/package.json +73 -73
  21. package/skills/claude-code/ritual/.ritual-bundle.json +2 -2
  22. package/skills/claude-code/ritual/references/build-flow.md +51 -14
  23. package/skills/codex/ritual/.ritual-bundle.json +2 -2
  24. package/skills/codex/ritual/references/build-flow.md +51 -14
  25. package/skills/cursor/ritual/.ritual-bundle.json +2 -2
  26. package/skills/cursor/ritual/references/build-flow.md +51 -14
  27. package/skills/gemini/ritual/.ritual-bundle.json +2 -2
  28. package/skills/gemini/ritual/references/build-flow.md +51 -14
  29. package/skills/kiro/ritual/.ritual-bundle.json +2 -2
  30. package/skills/kiro/ritual/references/build-flow.md +51 -14
  31. package/skills/vscode/ritual/.ritual-bundle.json +2 -2
  32. package/skills/vscode/ritual/references/build-flow.md +51 -14
@@ -39,6 +39,15 @@ const config_1 = require("../lib/config");
39
39
  const api_client_1 = require("../lib/api-client");
40
40
  const pat_store_1 = require("../lib/pat-store");
41
41
  const oidc_1 = require("../lib/oidc");
42
+ const onboarding_state_1 = require("../lib/onboarding-state");
43
+ const persona_picker_1 = require("../lib/persona-picker");
44
+ const workspace_explainer_1 = require("../lib/workspace-explainer");
45
+ const build_flow_explainer_1 = require("../lib/build-flow-explainer");
46
+ const persona_samples_1 = require("../lib/persona-samples");
47
+ const project_config_1 = require("../lib/project-config");
48
+ const repo_name_1 = require("../lib/repo-name");
49
+ const colors_1 = require("../lib/colors");
50
+ const final_cta_box_1 = require("../lib/final-cta-box");
42
51
  const detector_1 = require("../lib/agents/detector");
43
52
  const providers_1 = require("../lib/agents/providers");
44
53
  const configure_mcp_1 = require("../lib/agents/configure-mcp");
@@ -77,6 +86,13 @@ function resolveIssuerForInit(opts) {
77
86
  return undefined; // let auth-flow.ts pick from env / default
78
87
  }
79
88
  async function initCommand(opts = {}) {
89
+ // --- --re-onboard reset (must happen BEFORE any "shouldShow*"
90
+ // gate is consulted by the FTUE helpers below, so the screens
91
+ // re-render). Cheap, idempotent — just rewrites
92
+ // `~/.config/ritual/onboarding.json` to an empty record.
93
+ if (opts.reOnboard) {
94
+ (0, onboarding_state_1.resetOnboardingState)();
95
+ }
80
96
  // --- Welcome banner (TTY only; skipped for --list early-exit) ----
81
97
  // Replaces the previous single-line opener. The banner surfaces
82
98
  // identity (when signed in), workspace binding, and "what this
@@ -410,8 +426,22 @@ async function initCommand(opts = {}) {
410
426
  console.log(` ✓ Signed in as ${tokenStatus.creds.user?.email ?? tokenStatus.creds.user?.sub ?? 'unknown'}`);
411
427
  console.log('');
412
428
  }
413
- // --- 2. Detect agents (or pick a specific one) -------------------
429
+ // --- 2. Detect agents (silently announcement deferred) --------
430
+ //
431
+ // We resolve which agents to write to NOW because the build-flow
432
+ // explainer below names them in its "your coding agent" zone. But
433
+ // we intentionally suppress the "Detected N coding agents — here
434
+ // are the config files we're about to touch" announcement and the
435
+ // connect-all confirmation gate until AFTER the FTUE explainers.
436
+ //
437
+ // Narrative reason: the user just learned what Ritual is (workspace
438
+ // explainer) and what /ritual build does (build-flow explainer).
439
+ // Only THEN does "OK we're going to wire that up across these N
440
+ // agents" land in the right context. Otherwise they're being asked
441
+ // to confirm a list of agent config writes before they know what
442
+ // Ritual even does.
414
443
  let targets;
444
+ let agentSummaryToPrint = null;
415
445
  if (opts.agent) {
416
446
  const provider = (0, providers_1.findProviderById)(opts.agent);
417
447
  if (!provider) {
@@ -423,7 +453,7 @@ async function initCommand(opts = {}) {
423
453
  }
424
454
  // Explicit `--agent` overrides detection — the user knows what
425
455
  // they want, don't make them install/symlink the binary just
426
- // to pass detection.
456
+ // to pass detection. Nothing to announce; they typed the name.
427
457
  targets = [
428
458
  {
429
459
  id: provider.id,
@@ -450,73 +480,20 @@ async function initCommand(opts = {}) {
450
480
  provider: claudeProvider,
451
481
  },
452
482
  ];
453
- console.log(' No coding agents detected locally.');
454
- console.log(` Defaulting to ${claudeProvider.name} (the documented MCP launch partner).`);
455
- console.log('');
456
- console.log(' Supported agents:');
457
- for (const p of providers_1.PROVIDERS) {
458
- console.log(` • ${p.name.padEnd(20)} (--agent ${p.id})`);
459
- }
460
- console.log('');
483
+ agentSummaryToPrint = {
484
+ scenario: 'none-detected',
485
+ targets,
486
+ detected: [],
487
+ notDetected,
488
+ };
461
489
  }
462
490
  else if (detected.length === 1) {
463
- // Single-detect: lead with what we're about to do, mention
464
- // the escape hatch quietly. No menu, no confirmation —
465
- // safe default + visible action (per CLI tenet: lead with
466
- // single recommended action + escape hatch).
467
491
  targets = detected;
468
- const t = detected[0];
469
- console.log(` Detected ${t.name} locally — connecting Ritual there.`);
470
- if (t.provider.mcpConfigPath) {
471
- console.log(` Config: ${t.provider.mcpConfigPath}`);
472
- }
473
- else {
474
- console.log(` Config: managed by \`${t.provider.id}\` CLI`);
475
- }
476
- if (notDetected.length > 0) {
477
- console.log('');
478
- console.log(` Also supported (not detected here): ${notDetected.map((d) => d.name).join(', ')}`);
479
- console.log(' Connect another with `ritual init --agent <id>`.');
480
- }
481
- console.log('');
492
+ agentSummaryToPrint = { scenario: 'single', targets, detected, notDetected };
482
493
  }
483
494
  else {
484
- // Multi-detect: this IS a real decision gate (one agent vs
485
- // seven means writing to one config file vs seven). Tell
486
- // the user EXACTLY which files we're about to touch, then
487
- // pause with a single-keystroke escape hatch. We don't ask
488
- // "which agent?" — connecting all detected agents is the
489
- // safe recommended default. Power users can scope down by
490
- // pressing Ctrl-C and re-running with `--agent <id>`.
491
495
  targets = detected;
492
- console.log(` Detected ${detected.length} coding agents locally Ritual will connect to each:`);
493
- for (const d of detected) {
494
- const cfg = d.provider.mcpConfigPath
495
- ? d.provider.mcpConfigPath
496
- : `managed by \`${d.provider.id}\` CLI`;
497
- console.log(` • ${d.name.padEnd(20)} ${cfg}`);
498
- }
499
- if (notDetected.length > 0) {
500
- console.log('');
501
- console.log(` Also supported (not detected here): ${notDetected.map((d) => d.name).join(', ')}`);
502
- }
503
- console.log('');
504
- // TTY-only pause. Non-TTY (CI / scripted installs) skips
505
- // straight through — we already printed exactly what we're
506
- // about to do, and pausing in CI would hang forever.
507
- if (process.stdin.isTTY) {
508
- try {
509
- await (0, prompt_1.prompt)(' Press Enter to connect all detected agents, or Ctrl-C to abort\n' +
510
- ' (to pick a single agent, abort and re-run with `ritual init --agent <id>`): ');
511
- }
512
- catch {
513
- console.log('');
514
- console.log(' Aborted.');
515
- process.exitCode = 1;
516
- return;
517
- }
518
- }
519
- console.log('');
496
+ agentSummaryToPrint = { scenario: 'multi', targets, detected, notDetected };
520
497
  }
521
498
  }
522
499
  // Type guard for the compiler — by this point one of three paths
@@ -547,9 +524,35 @@ async function initCommand(opts = {}) {
547
524
  process.exitCode = 1;
548
525
  return;
549
526
  }
550
- printKeyCreatedBlock(pat, (0, oidc_1.webAppUrlFromIssuer)(issuer), targets);
551
- console.log('');
552
- // --- 3.5 Bind a project workspace --------------------------------
527
+ // Note: the "✓ MCP key created — stored in: <agent list>" block is
528
+ // printed AFTER the FTUE explainers + agent-connect confirmation
529
+ // (see step 3.8.5 below). The mint itself happens here so any
530
+ // failure short-circuits init before we've spent the user's time
531
+ // teaching the mental model; we just defer the user-visible noise.
532
+ // --- 3.4 FTUE persona picker (one-time per machine) --------------
533
+ // Runs AFTER auth + PAT mint (so all the mechanical "make Ritual
534
+ // work" friction is behind us — token cap recovery dialogs, etc.,
535
+ // don't ambush the user mid-onboarding question) and BEFORE the
536
+ // workspace explainer (so the explainer can render in the
537
+ // persona's accent color).
538
+ const pickedPersona = await maybeRunPersonaPicker(opts);
539
+ const projectDir = opts.cwd ?? process.cwd();
540
+ // --- 3.5 FTUE workspace explainer (BEFORE bind) -----------------
541
+ // The user needs to understand what a workspace IS before being
542
+ // asked to create one. We render with the *detected* name (from
543
+ // git-remote or cwd-basename) — the same name resolveProjectWorkspace
544
+ // will offer as the default in the next step. If they accept the
545
+ // default the displayed name matches what gets bound; if they
546
+ // rename, no harm done — the explainer was teaching the concept,
547
+ // not committing the value.
548
+ //
549
+ // We use `loadProjectConfig` first so an already-bound repo (where
550
+ // the explainer would otherwise re-show on a different machine via
551
+ // onboarding-state) uses the bound name, not a stale detection.
552
+ const existingBind = (0, project_config_1.loadProjectConfig)(projectDir);
553
+ const detectedNameForExplainer = existingBind?.workspaceName ?? (0, repo_name_1.detectRepoName)(projectDir).name;
554
+ maybeShowWorkspaceExplainer(detectedNameForExplainer, pickedPersona);
555
+ // --- 3.6 Bind a project workspace --------------------------------
553
556
  // Project-scoped state — see ./../lib/workspace-flow.ts for the
554
557
  // resolution rules. The result is null when:
555
558
  // - .ritual/config.json already exists (no change to make)
@@ -560,12 +563,60 @@ async function initCommand(opts = {}) {
560
563
  // We don't fail init when this returns null — the project just
561
564
  // doesn't get a default workspace bound, and downstream tools will
562
565
  // prompt for one on first use.
563
- const projectDir = opts.cwd ?? process.cwd();
566
+ //
564
567
  // Commander semantics: `--no-workspace` sets `opts.workspace=false`.
565
568
  // Default (no flag) leaves it `undefined`. Treat anything except
566
569
  // explicit false as "go ahead and prompt".
570
+ let boundWorkspaceName = existingBind?.workspaceName ?? null;
567
571
  if (opts.workspace !== false) {
568
- await (0, workspace_flow_1.resolveProjectWorkspace)({ api, projectDir });
572
+ const bound = await (0, workspace_flow_1.resolveProjectWorkspace)({ api, projectDir });
573
+ boundWorkspaceName = bound?.workspaceName ?? boundWorkspaceName;
574
+ }
575
+ // --- 3.7 Persist personaSlug onto the project config -------------
576
+ // Repo-team-wide default. Per-machine onboarding-state already
577
+ // holds the user's pick; here we mirror it into .ritual/config.json
578
+ // so collaborators on the repo who run `ritual init` later inherit
579
+ // the same default template, and so `/ritual build` in this repo
580
+ // can read it without touching the user's home directory.
581
+ //
582
+ // We only write if (a) we have a slug picked this run AND (b) the
583
+ // project config either doesn't have one yet or has an explicit
584
+ // override request via `--persona`. Never overwrite a teammate's
585
+ // committed pick silently on a routine re-init.
586
+ if (pickedPersona) {
587
+ const existing = (0, project_config_1.loadProjectConfig)(projectDir);
588
+ if (existing) {
589
+ const shouldUpdate = !existing.personaSlug || (opts.persona && existing.personaSlug !== pickedPersona);
590
+ if (shouldUpdate) {
591
+ (0, project_config_1.saveProjectConfig)(projectDir, { ...existing, personaSlug: pickedPersona });
592
+ }
593
+ }
594
+ }
595
+ // --- 3.8 Announce agent-config writes + gate on Enter ----------
596
+ // Agent connection is the LAST mechanical setup step before the
597
+ // build-flow explainer (which sits below as a "here's what
598
+ // /ritual build will do" payoff). User flow:
599
+ // workspace explainer → bind → wire agents → see /ritual build
600
+ // shape → "Setup complete" → they run /ritual build.
601
+ const proceed = await printAgentSummaryAndConfirm(agentSummaryToPrint);
602
+ if (!proceed) {
603
+ process.exitCode = 1;
604
+ return;
605
+ }
606
+ // --- 3.9 FTUE build-flow explainer (right before "Setup complete") -
607
+ // Now that workspace + agents are wired, show the user what
608
+ // /ritual build will look like — this is the payoff screen, the
609
+ // last thing before they go run the actual command.
610
+ maybeShowBuildFlowExplainer(pickedPersona, targets.filter((t) => t.detected).map((t) => t.name));
611
+ // --- 3.9.5 Print the deferred "MCP key created" block -----------
612
+ // The PAT was minted silently in step 3; we held the user-visible
613
+ // summary until here. Only printed in verbose mode — compact mode
614
+ // (the default) folds this into the "Ready to ship" CTA below
615
+ // without listing PAT name, scope, expiry, or storage paths
616
+ // (those live one command away: `ritual init --verbose`).
617
+ if (opts.verbose) {
618
+ printKeyCreatedBlock(pat, (0, oidc_1.webAppUrlFromIssuer)(issuer), targets);
619
+ console.log('');
569
620
  }
570
621
  // --- 4. Copy skills + register MCP for each target agent ---------
571
622
  const copyResults = [];
@@ -615,36 +666,76 @@ async function initCommand(opts = {}) {
615
666
  }
616
667
  // --- 5. Summary --------------------------------------------------
617
668
  //
618
- // Two pieces of feedback the user gets here:
669
+ // Two render paths, chosen by `--verbose`:
619
670
  //
620
- // (a) `Detected coding agents:` dotted-aligned table of all
621
- // known agents with `configured` / `not detected` / `failed`
622
- // status. Mirrors the legacy CLI's `printSummary` output so
623
- // any user who used the legacy CLI sees a familiar shape.
671
+ // COMPACT (default) a single "Ready to ship" split-column box
672
+ // directly after the build-flow explainer above. Left side:
673
+ // per-agent restart instructions + the literal command to ask
674
+ // the coding agent next (`/ritual build "<your feature>"`).
675
+ // Right side: 3 secondary commands the user can invoke if
676
+ // they want detail (`ritual init --verbose`, `ritual doctor`,
677
+ // `ritual graph status`). The PAT name / scope / expiry,
678
+ // per-agent status table, skill copy paths, and verify-then-
679
+ // restart ceremony all live behind `--verbose`. Compact is
680
+ // the FTUE default: turn the moment into action instead of
681
+ // burying the CTA under setup transcript.
624
682
  //
625
- // (b) `Next steps:` — verify-then-restart pattern. Crucially,
626
- // if the user ran `ritual init` from inside a Claude Code
627
- // session (detected via env vars), we surface a prominent
628
- // yellow warning that the CURRENT session won't see the
629
- // new MCP server until restart. This was the single biggest
630
- // footgun the legacy CLI surfaced and we want feature parity.
631
- printDetectedAgentsBlock(targets, copyResults, registrationResults);
632
- // One-liner about the .gitignore touch, only when we actually
633
- // wrote something. Silent on no-op (already up to date or no
634
- // scaffolded agents) so the summary doesn't get noisy.
635
- if (gitignoreResult?.changed) {
636
- const verb = gitignoreResult.preExisting ? 'Updated' : 'Created';
637
- const count = gitignoreResult.entries.length;
638
- const preview = count <= 2
639
- ? gitignoreResult.entries.join(', ')
640
- : `${gitignoreResult.entries.slice(0, 1).join(', ')} + ${count - 1} more`;
641
- console.log(` ✓ ${verb} .gitignore (added ${preview})`);
642
- }
643
- printNextStepsBlock({
644
- targets,
645
- registrationResults,
646
- mcpUrl,
647
- });
683
+ // VERBOSE (--verbose) legacy 3-block dump:
684
+ // (a) `Coding agent status:` dotted-aligned table of every
685
+ // known agent w/ `configured` / `not detected` / `failed`.
686
+ // (b) `.gitignore` one-liner fail-soft surface for the
687
+ // scaffolded-paths gitignore update.
688
+ // (c) `Next steps:` verify-then-restart pattern, includes
689
+ // the ⚠ inside-Claude warning, `claude mcp list`
690
+ // verification, per-agent restart hints, and quick-
691
+ // reference command list. This is what a power user
692
+ // invokes when they want to see exactly what landed on
693
+ // disk. Same detail is also reachable via `ritual doctor`.
694
+ if (opts.verbose) {
695
+ printDetectedAgentsBlock(targets, copyResults, registrationResults);
696
+ if (gitignoreResult?.changed) {
697
+ const verb = gitignoreResult.preExisting ? 'Updated' : 'Created';
698
+ const count = gitignoreResult.entries.length;
699
+ const preview = count <= 2
700
+ ? gitignoreResult.entries.join(', ')
701
+ : `${gitignoreResult.entries.slice(0, 1).join(', ')} + ${count - 1} more`;
702
+ console.log(` ✓ ${verb} .gitignore (added ${preview})`);
703
+ }
704
+ printNextStepsBlock({
705
+ targets,
706
+ registrationResults,
707
+ mcpUrl,
708
+ });
709
+ }
710
+ else {
711
+ // Compact: render the "Ready to ship" CTA box. The inside-
712
+ // Claude warning is safety-critical (the CURRENT session
713
+ // won't see the new MCP server until restart) so it surfaces
714
+ // in both modes; the CTA box renders it as a bold amber row
715
+ // above the restart bullets.
716
+ const wiredAgentNames = targets
717
+ .filter((t, i) => registrationResults[i]?.success)
718
+ .map((t) => t.name);
719
+ const insideClaudeWarning = wiredAgentNames.includes('Claude Code') && isInsideClaudeCode()
720
+ ? 'inside Claude Code — restart this session first'
721
+ : '';
722
+ // Tier the CTA renderer to the terminal's actual width. The
723
+ // rich split-column box wants ≥ 112 cols (LEFT_W=64 + divider
724
+ // + ~45-char right column + chrome) — anything narrower would
725
+ // either truncate the secondary command labels or push the
726
+ // inner divider out of alignment. Below that, fall through to
727
+ // the styled vertical stack (still ANSI-color-aware) which
728
+ // works cleanly at 78+. Below 78 — or non-TTY — colors degrade
729
+ // to plain via the colors.ts auto-detection.
730
+ const isTty = !!process.stdout.isTTY;
731
+ const cols = process.stdout.columns ?? 0;
732
+ if (isTty && cols >= 112) {
733
+ (0, final_cta_box_1.printFinalCtaBox)({ wiredAgentNames, insideClaudeWarning });
734
+ }
735
+ else {
736
+ (0, final_cta_box_1.printFinalCtaBoxTerse)({ wiredAgentNames, insideClaudeWarning });
737
+ }
738
+ }
648
739
  }
649
740
  // webAppUrlFromIssuer is exported from lib/oidc.ts — single source of
650
741
  // truth so the dev/prod/self-hosted mapping stays consistent across
@@ -868,4 +959,223 @@ function isInsideClaudeCode() {
868
959
  !!process.env.CLAUDE_CODE_ENTRYPOINT ||
869
960
  !!process.env.CLAUDE_CODE_SESSION_ID);
870
961
  }
962
+ /**
963
+ * Render the agent-connection summary (which configs we're about to
964
+ * touch) and, in the multi-detect case, gate on Enter-to-continue.
965
+ *
966
+ * Returns `false` if the user aborted; the caller must short-circuit
967
+ * the rest of init and set process.exitCode = 1. `true` means proceed.
968
+ *
969
+ * For the `explicit` path (--agent <id>) the caller passes null and
970
+ * we skip this entirely — the user typed the agent name, no surprise
971
+ * to soften.
972
+ */
973
+ async function printAgentSummaryAndConfirm(summary) {
974
+ if (!summary)
975
+ return true;
976
+ // Shared 78-char box helpers — same width as the FTUE explainers
977
+ // so the agent-connect step reads as part of the same visual
978
+ // family rather than a console dump in the middle of boxed UI.
979
+ const W = 78;
980
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, ''); // eslint-disable-line no-control-regex
981
+ const padded = (content) => {
982
+ const visLen = stripAnsi(content).length;
983
+ const pad = ' '.repeat(Math.max(0, W - 2 - visLen));
984
+ return `│${content}${pad}│`;
985
+ };
986
+ const top = (title) => {
987
+ const visLen = stripAnsi(title).length;
988
+ const fill = '─'.repeat(Math.max(0, W - visLen - 4));
989
+ return `╭ ${title} ${fill}╮`;
990
+ };
991
+ const bottom = '╰' + '─'.repeat(W - 2) + '╯';
992
+ const renderBox = (titleBar, bodyLines) => {
993
+ console.log(top(titleBar));
994
+ console.log(padded(''));
995
+ for (const line of bodyLines) {
996
+ console.log(padded(line));
997
+ }
998
+ console.log(padded(''));
999
+ console.log(bottom);
1000
+ };
1001
+ // ─── scenario: none detected ────────────────────────────────────
1002
+ if (summary.scenario === 'none-detected') {
1003
+ const claude = summary.targets[0];
1004
+ renderBox((0, colors_1.color)(colors_1.RITUAL_TEAL, '─ wire your coding agent '), [
1005
+ ` ${(0, colors_1.dim)('no coding agents detected on this machine — defaulting to:')}`,
1006
+ '',
1007
+ ` ${(0, colors_1.boldColor)(colors_1.RITUAL_TEAL, '✓ ' + claude.name)} ${(0, colors_1.dim)('(the documented MCP launch partner)')}`,
1008
+ '',
1009
+ ` ${(0, colors_1.dim)('also supported (re-run with --agent <id>):')}`,
1010
+ ...providers_1.PROVIDERS.filter((p) => p.id !== claude.id).map((p) => ` ${(0, colors_1.dim)('·')} ${(0, colors_1.dim)(p.name.padEnd(20))} ${(0, colors_1.dim)('--agent ' + p.id)}`),
1011
+ ]);
1012
+ console.log('');
1013
+ return true;
1014
+ }
1015
+ // ─── scenario: single detected ─────────────────────────────────
1016
+ if (summary.scenario === 'single') {
1017
+ const t = summary.detected[0];
1018
+ const notDetected = summary.notDetected.map((d) => d.name).join(', ');
1019
+ renderBox((0, colors_1.color)(colors_1.RITUAL_TEAL, '─ wire your coding agent '), [
1020
+ ` ${(0, colors_1.dim)('detected one coding agent on this machine — wiring up:')}`,
1021
+ '',
1022
+ ` ${(0, colors_1.boldColor)(colors_1.RITUAL_TEAL, '✓ ' + t.name)}`,
1023
+ '',
1024
+ ...(notDetected
1025
+ ? [
1026
+ ` ${(0, colors_1.dim)('also supported (not detected here): ' + notDetected)}`,
1027
+ ` ${(0, colors_1.dim)('connect another with `ritual init --agent <id>`')}`,
1028
+ ]
1029
+ : []),
1030
+ ]);
1031
+ console.log('');
1032
+ return true;
1033
+ }
1034
+ // ─── scenario: multi-detect (real decision gate) ────────────────
1035
+ const notDetectedList = summary.notDetected.map((d) => d.name).join(', ');
1036
+ renderBox((0, colors_1.color)(colors_1.RITUAL_TEAL, '─ wire your coding agents '), [
1037
+ ` ${(0, colors_1.dim)(`detected ${summary.detected.length} coding agents on this machine — wiring up:`)}`,
1038
+ '',
1039
+ ...summary.detected.map((d) => ` ${(0, colors_1.boldColor)(colors_1.RITUAL_TEAL, '✓ ' + d.name)}`),
1040
+ '',
1041
+ ...(notDetectedList
1042
+ ? [` ${(0, colors_1.dim)('also supported (not detected here): ' + notDetectedList)}`]
1043
+ : []),
1044
+ ` ${(0, colors_1.dim)('config paths: ritual doctor')}`,
1045
+ ]);
1046
+ console.log('');
1047
+ // TTY-only pause. Non-TTY (CI / scripted) skips through — we
1048
+ // already printed exactly what's about to happen, and pausing in
1049
+ // CI would hang forever.
1050
+ if (process.stdin.isTTY) {
1051
+ try {
1052
+ await (0, prompt_1.prompt)(` ${(0, colors_1.color)(colors_1.RITUAL_TEAL, '›')} press Enter to wire all ${summary.detected.length}, ` +
1053
+ `or Ctrl-C to abort\n` +
1054
+ ` ${(0, colors_1.dim)('(single agent? abort + re-run with `ritual init --agent <id>`)')}: `);
1055
+ }
1056
+ catch {
1057
+ console.log('');
1058
+ console.log(' Aborted.');
1059
+ return false;
1060
+ }
1061
+ }
1062
+ console.log('');
1063
+ return true;
1064
+ }
1065
+ // ─── FTUE onboarding helpers ────────────────────────────────────────────────
1066
+ /**
1067
+ * Coerce a raw `--persona <slug>` CLI value into a known PersonaSlug.
1068
+ * Returns the validated slug on hit, prints a friendly error and
1069
+ * returns undefined on miss. The caller treats "miss" as "abort
1070
+ * init" — silently falling through to the picker would be confusing
1071
+ * when the user clearly intended a specific persona.
1072
+ */
1073
+ function validatePersonaFlag(raw) {
1074
+ if (!raw)
1075
+ return undefined;
1076
+ const persona = (0, persona_samples_1.findPersona)(raw);
1077
+ if (!persona) {
1078
+ console.error(` ✗ Unknown persona: "${raw}".`);
1079
+ console.error(' Run `ritual init --list` for agents; persona slugs live in the CLI docs.');
1080
+ console.error('');
1081
+ return null;
1082
+ }
1083
+ return persona.slug;
1084
+ }
1085
+ /**
1086
+ * Run the persona-picker screen if the user hasn't been onboarded
1087
+ * yet (or `--re-onboard` is set / `--persona` was passed).
1088
+ *
1089
+ * Returns the resolved persona slug (or null if the user explicitly
1090
+ * skipped). Always updates the per-machine onboarding state with the
1091
+ * outcome so we don't re-prompt next time.
1092
+ *
1093
+ * Non-TTY: respects the `--persona` flag if provided; otherwise
1094
+ * silently no-ops and returns null. We never block scripted/CI init
1095
+ * on a missing persona — downstream tools degrade gracefully via
1096
+ * the generic persona.
1097
+ */
1098
+ async function maybeRunPersonaPicker(opts) {
1099
+ const state = (0, onboarding_state_1.readOnboardingState)();
1100
+ // Explicit override via flag — always takes precedence.
1101
+ if (opts.persona) {
1102
+ const slug = validatePersonaFlag(opts.persona);
1103
+ if (slug === null) {
1104
+ // Unknown slug — caller will treat the absence as a soft fail.
1105
+ return null;
1106
+ }
1107
+ if (slug) {
1108
+ (0, onboarding_state_1.updateOnboardingState)({
1109
+ personaPickedAt: new Date().toISOString(),
1110
+ personaSlug: slug,
1111
+ });
1112
+ return slug;
1113
+ }
1114
+ }
1115
+ if (!(0, onboarding_state_1.shouldShowPersonaPicker)(state)) {
1116
+ // Already onboarded — re-use the stored pick. `null` here means
1117
+ // the user explicitly skipped on a previous init; we honor that.
1118
+ return state.personaSlug ?? null;
1119
+ }
1120
+ if (!process.stdin.isTTY) {
1121
+ // Non-TTY first-time init: no picker, no persistence. We don't
1122
+ // mark onboarding as "done" because a future interactive init
1123
+ // can still ask.
1124
+ return null;
1125
+ }
1126
+ try {
1127
+ const result = await (0, persona_picker_1.pickPersona)();
1128
+ (0, onboarding_state_1.updateOnboardingState)({
1129
+ personaPickedAt: new Date().toISOString(),
1130
+ personaSlug: result?.slug ?? null,
1131
+ });
1132
+ return result?.slug ?? null;
1133
+ }
1134
+ catch {
1135
+ // Readline failed for any reason — don't crash init.
1136
+ return null;
1137
+ }
1138
+ }
1139
+ /**
1140
+ * Show the workspace explainer (screen #2) if the user hasn't seen
1141
+ * it yet on this machine. Marks it seen on first render so we don't
1142
+ * lecture the user on every subsequent `ritual init` in a new repo.
1143
+ *
1144
+ * Falls back to the terse non-TTY printer when stdout isn't a TTY
1145
+ * OR the terminal is narrower than the rich layout assumes (<80
1146
+ * cols), so we still teach the model without busting the wrap.
1147
+ */
1148
+ function maybeShowWorkspaceExplainer(workspaceName, personaSlug) {
1149
+ const state = (0, onboarding_state_1.readOnboardingState)();
1150
+ if (!(0, onboarding_state_1.shouldShowWorkspaceExplainer)(state))
1151
+ return;
1152
+ const isTty = !!process.stdout.isTTY;
1153
+ const cols = process.stdout.columns ?? 80;
1154
+ if (isTty && cols >= 80) {
1155
+ (0, workspace_explainer_1.printWorkspaceExplainer)({ workspaceName, personaSlug });
1156
+ }
1157
+ else {
1158
+ (0, workspace_explainer_1.printWorkspaceExplainerTerse)({ workspaceName, personaSlug });
1159
+ }
1160
+ (0, onboarding_state_1.updateOnboardingState)({ workspaceExplainerSeenAt: new Date().toISOString() });
1161
+ }
1162
+ /**
1163
+ * Show the build-flow explainer (screen #3) if the user hasn't seen
1164
+ * it yet on this machine. Mirrors the workspace explainer's gating
1165
+ * + non-TTY fallback. `detectedAgents` shapes the coding-agent zone.
1166
+ */
1167
+ function maybeShowBuildFlowExplainer(personaSlug, detectedAgents) {
1168
+ const state = (0, onboarding_state_1.readOnboardingState)();
1169
+ if (!(0, onboarding_state_1.shouldShowBuildFlowExplainer)(state))
1170
+ return;
1171
+ const isTty = !!process.stdout.isTTY;
1172
+ const cols = process.stdout.columns ?? 80;
1173
+ if (isTty && cols >= 80) {
1174
+ (0, build_flow_explainer_1.printBuildFlowExplainer)({ personaSlug, detectedAgents });
1175
+ }
1176
+ else {
1177
+ (0, build_flow_explainer_1.printBuildFlowExplainerTerse)({ personaSlug, detectedAgents });
1178
+ }
1179
+ (0, onboarding_state_1.updateOnboardingState)({ buildFlowExplainerSeenAt: new Date().toISOString() });
1180
+ }
871
1181
  //# sourceMappingURL=init.js.map