@ritualai/cli 0.7.15 → 0.8.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.
Files changed (42) hide show
  1. package/dist/commands/doctor.js +66 -0
  2. package/dist/commands/doctor.js.map +1 -1
  3. package/dist/commands/init.js +468 -108
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/index.js +3 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/agents/providers.js +33 -5
  8. package/dist/lib/agents/providers.js.map +1 -1
  9. package/dist/lib/build-flow-explainer.js +226 -0
  10. package/dist/lib/build-flow-explainer.js.map +1 -0
  11. package/dist/lib/final-cta-box.js +224 -0
  12. package/dist/lib/final-cta-box.js.map +1 -0
  13. package/dist/lib/gitignore-update.js +13 -4
  14. package/dist/lib/gitignore-update.js.map +1 -1
  15. package/dist/lib/onboarding-state.js +140 -0
  16. package/dist/lib/onboarding-state.js.map +1 -0
  17. package/dist/lib/persona-picker.js +171 -0
  18. package/dist/lib/persona-picker.js.map +1 -0
  19. package/dist/lib/persona-samples.js +245 -0
  20. package/dist/lib/persona-samples.js.map +1 -0
  21. package/dist/lib/project-config.js.map +1 -1
  22. package/dist/lib/skill-bundles.js +4 -0
  23. package/dist/lib/skill-bundles.js.map +1 -1
  24. package/dist/lib/skill-copy.js +62 -10
  25. package/dist/lib/skill-copy.js.map +1 -1
  26. package/dist/lib/workspace-explainer.js +193 -0
  27. package/dist/lib/workspace-explainer.js.map +1 -0
  28. package/dist/lib/workspace-flow.js +8 -7
  29. package/dist/lib/workspace-flow.js.map +1 -1
  30. package/package.json +73 -73
  31. package/skills/claude-code/ritual/.ritual-bundle.json +2 -2
  32. package/skills/claude-code/ritual/references/build-flow.md +51 -14
  33. package/skills/codex/ritual/.ritual-bundle.json +2 -2
  34. package/skills/codex/ritual/references/build-flow.md +51 -14
  35. package/skills/cursor/ritual/.ritual-bundle.json +2 -2
  36. package/skills/cursor/ritual/references/build-flow.md +51 -14
  37. package/skills/gemini/ritual/.ritual-bundle.json +2 -2
  38. package/skills/gemini/ritual/references/build-flow.md +51 -14
  39. package/skills/kiro/ritual/.ritual-bundle.json +2 -2
  40. package/skills/kiro/ritual/references/build-flow.md +51 -14
  41. package/skills/vscode/ritual/.ritual-bundle.json +2 -2
  42. 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
@@ -173,7 +189,13 @@ async function initCommand(opts = {}) {
173
189
  // who deliberately scoped a previous init with `--agent`.
174
190
  const { existsSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
175
191
  const { join } = await Promise.resolve().then(() => __importStar(require('node:path')));
176
- refreshTargets = (0, detector_1.detectAgents)().filter((d) => existsSync(join(projectDir, d.provider.projectSkillDir)));
192
+ refreshTargets = (0, detector_1.detectAgents)().filter((d) => {
193
+ // MCP-only agents (no projectSkillDir) have nothing to
194
+ // refresh on the skill side — silently skipped here.
195
+ if (!d.provider.projectSkillDir)
196
+ return false;
197
+ return existsSync(join(projectDir, d.provider.projectSkillDir));
198
+ });
177
199
  if (refreshTargets.length === 0) {
178
200
  console.log(' No Ritual skill directories found in this project.');
179
201
  console.log(' Run `ritual init` (without --skills-only) to scaffold them first.');
@@ -186,11 +208,20 @@ async function initCommand(opts = {}) {
186
208
  for (const t of refreshTargets) {
187
209
  const result = (0, skill_copy_1.copySkillsForProvider)(t.provider, projectDir);
188
210
  refreshCopyResults.push(result);
189
- if (result.copied > 0) {
190
- console.log(` ✓ ${t.name.padEnd(20)} ${t.provider.projectSkillDir}/ (${result.copied} skill(s))`);
211
+ if (result.scopes.length === 0) {
212
+ // MCP-only agent (e.g. Windsurf as of 0.7.16) no skill
213
+ // destination at any scope.
214
+ console.log(` – ${t.name.padEnd(20)} ${result.reason ?? 'no skill destination'}`);
215
+ continue;
191
216
  }
192
- else {
193
- console.log(` ✗ ${t.name.padEnd(20)} ${result.reason ?? 'unknown failure'}`);
217
+ for (const s of result.scopes) {
218
+ const tag = s.scope === 'project' ? 'project' : 'global ';
219
+ if (s.copied > 0) {
220
+ console.log(` ✓ ${t.name.padEnd(20)} [${tag}] → ${s.destination}/ (${s.copied} skill(s))`);
221
+ }
222
+ else {
223
+ console.log(` ✗ ${t.name.padEnd(20)} [${tag}] ${s.reason ?? 'unknown failure'}`);
224
+ }
194
225
  }
195
226
  }
196
227
  console.log('');
@@ -395,8 +426,22 @@ async function initCommand(opts = {}) {
395
426
  console.log(` ✓ Signed in as ${tokenStatus.creds.user?.email ?? tokenStatus.creds.user?.sub ?? 'unknown'}`);
396
427
  console.log('');
397
428
  }
398
- // --- 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.
399
443
  let targets;
444
+ let agentSummaryToPrint = null;
400
445
  if (opts.agent) {
401
446
  const provider = (0, providers_1.findProviderById)(opts.agent);
402
447
  if (!provider) {
@@ -408,7 +453,7 @@ async function initCommand(opts = {}) {
408
453
  }
409
454
  // Explicit `--agent` overrides detection — the user knows what
410
455
  // they want, don't make them install/symlink the binary just
411
- // to pass detection.
456
+ // to pass detection. Nothing to announce; they typed the name.
412
457
  targets = [
413
458
  {
414
459
  id: provider.id,
@@ -435,73 +480,20 @@ async function initCommand(opts = {}) {
435
480
  provider: claudeProvider,
436
481
  },
437
482
  ];
438
- console.log(' No coding agents detected locally.');
439
- console.log(` Defaulting to ${claudeProvider.name} (the documented MCP launch partner).`);
440
- console.log('');
441
- console.log(' Supported agents:');
442
- for (const p of providers_1.PROVIDERS) {
443
- console.log(` • ${p.name.padEnd(20)} (--agent ${p.id})`);
444
- }
445
- console.log('');
483
+ agentSummaryToPrint = {
484
+ scenario: 'none-detected',
485
+ targets,
486
+ detected: [],
487
+ notDetected,
488
+ };
446
489
  }
447
490
  else if (detected.length === 1) {
448
- // Single-detect: lead with what we're about to do, mention
449
- // the escape hatch quietly. No menu, no confirmation —
450
- // safe default + visible action (per CLI tenet: lead with
451
- // single recommended action + escape hatch).
452
491
  targets = detected;
453
- const t = detected[0];
454
- console.log(` Detected ${t.name} locally — connecting Ritual there.`);
455
- if (t.provider.mcpConfigPath) {
456
- console.log(` Config: ${t.provider.mcpConfigPath}`);
457
- }
458
- else {
459
- console.log(` Config: managed by \`${t.provider.id}\` CLI`);
460
- }
461
- if (notDetected.length > 0) {
462
- console.log('');
463
- console.log(` Also supported (not detected here): ${notDetected.map((d) => d.name).join(', ')}`);
464
- console.log(' Connect another with `ritual init --agent <id>`.');
465
- }
466
- console.log('');
492
+ agentSummaryToPrint = { scenario: 'single', targets, detected, notDetected };
467
493
  }
468
494
  else {
469
- // Multi-detect: this IS a real decision gate (one agent vs
470
- // seven means writing to one config file vs seven). Tell
471
- // the user EXACTLY which files we're about to touch, then
472
- // pause with a single-keystroke escape hatch. We don't ask
473
- // "which agent?" — connecting all detected agents is the
474
- // safe recommended default. Power users can scope down by
475
- // pressing Ctrl-C and re-running with `--agent <id>`.
476
495
  targets = detected;
477
- console.log(` Detected ${detected.length} coding agents locally Ritual will connect to each:`);
478
- for (const d of detected) {
479
- const cfg = d.provider.mcpConfigPath
480
- ? d.provider.mcpConfigPath
481
- : `managed by \`${d.provider.id}\` CLI`;
482
- console.log(` • ${d.name.padEnd(20)} ${cfg}`);
483
- }
484
- if (notDetected.length > 0) {
485
- console.log('');
486
- console.log(` Also supported (not detected here): ${notDetected.map((d) => d.name).join(', ')}`);
487
- }
488
- console.log('');
489
- // TTY-only pause. Non-TTY (CI / scripted installs) skips
490
- // straight through — we already printed exactly what we're
491
- // about to do, and pausing in CI would hang forever.
492
- if (process.stdin.isTTY) {
493
- try {
494
- await (0, prompt_1.prompt)(' Press Enter to connect all detected agents, or Ctrl-C to abort\n' +
495
- ' (to pick a single agent, abort and re-run with `ritual init --agent <id>`): ');
496
- }
497
- catch {
498
- console.log('');
499
- console.log(' Aborted.');
500
- process.exitCode = 1;
501
- return;
502
- }
503
- }
504
- console.log('');
496
+ agentSummaryToPrint = { scenario: 'multi', targets, detected, notDetected };
505
497
  }
506
498
  }
507
499
  // Type guard for the compiler — by this point one of three paths
@@ -532,9 +524,35 @@ async function initCommand(opts = {}) {
532
524
  process.exitCode = 1;
533
525
  return;
534
526
  }
535
- printKeyCreatedBlock(pat, (0, oidc_1.webAppUrlFromIssuer)(issuer), targets);
536
- console.log('');
537
- // --- 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 --------------------------------
538
556
  // Project-scoped state — see ./../lib/workspace-flow.ts for the
539
557
  // resolution rules. The result is null when:
540
558
  // - .ritual/config.json already exists (no change to make)
@@ -545,12 +563,60 @@ async function initCommand(opts = {}) {
545
563
  // We don't fail init when this returns null — the project just
546
564
  // doesn't get a default workspace bound, and downstream tools will
547
565
  // prompt for one on first use.
548
- const projectDir = opts.cwd ?? process.cwd();
566
+ //
549
567
  // Commander semantics: `--no-workspace` sets `opts.workspace=false`.
550
568
  // Default (no flag) leaves it `undefined`. Treat anything except
551
569
  // explicit false as "go ahead and prompt".
570
+ let boundWorkspaceName = existingBind?.workspaceName ?? null;
552
571
  if (opts.workspace !== false) {
553
- 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('');
554
620
  }
555
621
  // --- 4. Copy skills + register MCP for each target agent ---------
556
622
  const copyResults = [];
@@ -571,8 +637,19 @@ async function initCommand(opts = {}) {
571
637
  // keeps the .gitignore block aligned with what's on disk. The
572
638
  // helper is idempotent: re-runs replace the marker-bracketed block
573
639
  // in place rather than appending duplicates.
640
+ //
641
+ // We filter on the PROJECT scope's copy count specifically — a
642
+ // user-global-only landing (e.g. nothing actually written to the
643
+ // project tree) shouldn't add a .gitignore entry pointing at an
644
+ // empty directory.
574
645
  const scaffoldedProviders = targets
575
- .filter((_, i) => copyResults[i]?.copied)
646
+ .filter((_, i) => {
647
+ const result = copyResults[i];
648
+ if (!result)
649
+ return false;
650
+ const projectScope = result.scopes.find((s) => s.scope === 'project');
651
+ return (projectScope?.copied ?? 0) > 0;
652
+ })
576
653
  .map((t) => t.provider);
577
654
  // Fail-soft: a .gitignore write failure (filesystem permissions,
578
655
  // read-only volume, exotic project layout) must NOT abort init —
@@ -589,36 +666,76 @@ async function initCommand(opts = {}) {
589
666
  }
590
667
  // --- 5. Summary --------------------------------------------------
591
668
  //
592
- // Two pieces of feedback the user gets here:
669
+ // Two render paths, chosen by `--verbose`:
593
670
  //
594
- // (a) `Detected coding agents:` dotted-aligned table of all
595
- // known agents with `configured` / `not detected` / `failed`
596
- // status. Mirrors the legacy CLI's `printSummary` output so
597
- // 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.
598
682
  //
599
- // (b) `Next steps:` — verify-then-restart pattern. Crucially,
600
- // if the user ran `ritual init` from inside a Claude Code
601
- // session (detected via env vars), we surface a prominent
602
- // yellow warning that the CURRENT session won't see the
603
- // new MCP server until restart. This was the single biggest
604
- // footgun the legacy CLI surfaced and we want feature parity.
605
- printDetectedAgentsBlock(targets, copyResults, registrationResults);
606
- // One-liner about the .gitignore touch, only when we actually
607
- // wrote something. Silent on no-op (already up to date or no
608
- // scaffolded agents) so the summary doesn't get noisy.
609
- if (gitignoreResult?.changed) {
610
- const verb = gitignoreResult.preExisting ? 'Updated' : 'Created';
611
- const count = gitignoreResult.entries.length;
612
- const preview = count <= 2
613
- ? gitignoreResult.entries.join(', ')
614
- : `${gitignoreResult.entries.slice(0, 1).join(', ')} + ${count - 1} more`;
615
- console.log(` ✓ ${verb} .gitignore (added ${preview})`);
616
- }
617
- printNextStepsBlock({
618
- targets,
619
- registrationResults,
620
- mcpUrl,
621
- });
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
+ }
622
739
  }
623
740
  // webAppUrlFromIssuer is exported from lib/oidc.ts — single source of
624
741
  // truth so the dev/prod/self-hosted mapping stays consistent across
@@ -719,15 +836,39 @@ function printDetectedAgentsBlock(targets, copyResults, registrationResults) {
719
836
  }
720
837
  }
721
838
  // Per-agent skill copy summary — kept here because users sometimes
722
- // scan the dotted table and miss the more detailed counts.
839
+ // scan the dotted table and miss the more detailed counts. Includes
840
+ // per-scope detail (project vs user-global) so users can see exactly
841
+ // where /ritual landed and verify the scope-match invariant: any
842
+ // session that can reach mcp__ritual__* should also know what
843
+ // /ritual is. See FTUE analysis 2026-05-19.
723
844
  console.log('');
724
- console.log(' Skills copied:');
845
+ console.log(' Skills installed:');
725
846
  for (const t of targets) {
726
847
  const copy = copyResults.find((r) => r.provider.id === t.provider.id);
727
- const status = copy.copied > 0
728
- ? `${copy.copied} ${t.provider.projectSkillDir}/`
729
- : `skipped (${copy.reason ?? 'unknown'})`;
730
- console.log(` ${t.provider.name}: ${status}`);
848
+ if (copy.scopes.length === 0) {
849
+ // MCP-only agent (Windsurf): MCP tools are configured but
850
+ // the agent doesn't have a skill-install convention the
851
+ // CLI populates. Be explicit so the user isn't surprised
852
+ // that `/ritual` (or its agent-specific equivalent) is
853
+ // absent — MCP tools still work.
854
+ console.log(` ${t.provider.name}: MCP tools only — agent has no skill-install convention this CLI populates`);
855
+ continue;
856
+ }
857
+ for (const s of copy.scopes) {
858
+ const scopeLabel = s.scope === 'project' ? 'project' : 'user-global';
859
+ const status = s.copied > 0
860
+ ? `${s.copied} → ${s.destination}/`
861
+ : `skipped (${s.reason ?? 'unknown'})`;
862
+ console.log(` ${t.provider.name} [${scopeLabel}]: ${status}`);
863
+ }
864
+ // Surface the Cursor / Windsurf asymmetry inline so the user
865
+ // understands why some agents lack a user-global entry. Keep
866
+ // the note narrow and actionable; full detail lives in
867
+ // `ritual doctor`.
868
+ if (!t.provider.userSkillDir && t.provider.projectSkillDir) {
869
+ console.log(` note: ${t.provider.name} has no on-disk user-global skill location — ` +
870
+ `open ${t.provider.name} in this project's directory to use the skill.`);
871
+ }
731
872
  }
732
873
  }
733
874
  /**
@@ -818,4 +959,223 @@ function isInsideClaudeCode() {
818
959
  !!process.env.CLAUDE_CODE_ENTRYPOINT ||
819
960
  !!process.env.CLAUDE_CODE_SESSION_ID);
820
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
+ }
821
1181
  //# sourceMappingURL=init.js.map