fix: stop dimming setup card bodies

Clack's `p.note` defaults to `format: e => styleText("dim", e)`, which
fades note bodies regardless of the project's stated readability stance
(see comment on `dimWrap` in setup/lib/theme.ts: "prose renders at the
terminal's regular weight"). The dim styling makes body copy hard to
read on dark terminals and visibly washes out brand-colored segments
embedded in cards (e.g. the chip + bold heading rows).

Add a `note()` helper in setup/lib/theme.ts that wraps `p.note` with a
pass-through formatter, and route every setup-flow `p.note` call site
through it: setup/auto.ts, every setup/channels/*.ts adapter, and the
two setup/lib/claude-* helpers.

Pre-styled segments (brandBold, brandChip, formatPairingCard,
formatCodeCard) now render at full strength instead of being faded
alongside surrounding prose.
This commit is contained in:
exe.dev user
2026-04-29 10:20:10 +00:00
parent ede6c01da8
commit 9c8f680ca8
11 changed files with 62 additions and 43 deletions

View File

@@ -52,7 +52,7 @@ import { claudeCliAvailable, resolveTimezoneViaClaude } from './lib/tz-from-clau
import * as setupLog from './logs.js'; import * as setupLog from './logs.js';
import { ensureAnswer, fail, runQuietChild, runQuietStep } from './lib/runner.js'; import { ensureAnswer, fail, runQuietChild, runQuietStep } from './lib/runner.js';
import { emit as phEmit } from './lib/diagnostics.js'; import { emit as phEmit } from './lib/diagnostics.js';
import { brandBold, brandChip, dimWrap, fitToWidth, wrapForGutter } from './lib/theme.js'; import { brandBold, brandChip, dimWrap, fitToWidth, note, wrapForGutter } from './lib/theme.js';
import { isValidTimezone } from '../src/timezone.js'; import { isValidTimezone } from '../src/timezone.js';
const CLI_AGENT_NAME = 'Terminal Agent'; const CLI_AGENT_NAME = 'Terminal Agent';
@@ -435,7 +435,7 @@ async function main(): Promise<void> {
); );
} }
if (notes.length > 0) { if (notes.length > 0) {
p.note(notes.join('\n'), "What's left"); note(notes.join('\n'), "What's left");
} }
// "What's left" is a soft failure — we don't abort like fail(), but the // "What's left" is a soft failure — we don't abort like fail(), but the
// user is still stuck and a fix is exactly what claude-assist is for. // user is still stuck and a fix is exactly what claude-assist is for.
@@ -467,11 +467,11 @@ async function main(): Promise<void> {
]; ];
const labelWidth = Math.max(...rows.map(([l]) => l.length)); const labelWidth = Math.max(...rows.map(([l]) => l.length));
const nextSteps = rows.map(([l, c]) => `${k.cyan(l.padEnd(labelWidth))} ${c}`).join('\n'); const nextSteps = rows.map(([l, c]) => `${k.cyan(l.padEnd(labelWidth))} ${c}`).join('\n');
p.note(nextSteps, 'Try these'); note(nextSteps, 'Try these');
// Always-on warning goes before the "check your DMs" directive so the // Always-on warning goes before the "check your DMs" directive so the
// caveat doesn't land after the user's already looked away at their phone. // caveat doesn't land after the user's already looked away at their phone.
p.note( note(
wrapForGutter( wrapForGutter(
"NanoClaw runs on this machine. It's only reachable while this computer is on and connected to the internet. For always-on availability, run it on a cloud VM — or keep this machine awake.", "NanoClaw runs on this machine. It's only reachable while this computer is on and connected to the internet. For always-on availability, run it on a cloud VM — or keep this machine awake.",
6, 6,
@@ -488,7 +488,7 @@ async function main(): Promise<void> {
// that the welcome-message signal was too easy to miss. Use p.note so it // that the welcome-message signal was too easy to miss. Use p.note so it
// renders with a visible box, cyan-bold the directive line, and put it // renders with a visible box, cyan-bold the directive line, and put it
// as the last thing before outro. // as the last thing before outro.
p.note(`${brandBold('→')} ${k.bold(`Check your ${dmTarget} — your assistant is saying hi.`)}`, 'Go say hi'); note(`${brandBold('→')} ${k.bold(`Check your ${dmTarget} — your assistant is saying hi.`)}`, 'Go say hi');
p.outro(k.green("You're set.")); p.outro(k.green("You're set."));
} else { } else {
p.outro(k.green("You're ready! Chat with `pnpm run chat hi`.")); p.outro(k.green("You're ready! Chat with `pnpm run chat hi`."));
@@ -567,7 +567,7 @@ function renderPingFailureNote(result: PingResult): void {
'No reply from your assistant within 30 seconds. Check `logs/nanoclaw.log` for clues, then try `pnpm run chat hi`.', 'No reply from your assistant within 30 seconds. Check `logs/nanoclaw.log` for clues, then try `pnpm run chat hi`.',
6, 6,
); );
p.note(body, 'Skipping the first chat'); note(body, 'Skipping the first chat');
} }
/** /**
@@ -582,7 +582,7 @@ function renderPingFailureNote(result: PingResult): void {
* clearly optional. * clearly optional.
*/ */
async function runFirstChat(): Promise<void> { async function runFirstChat(): Promise<void> {
p.note( note(
wrapForGutter( wrapForGutter(
[ [
'Your assistant runs in a sandbox on this machine.', 'Your assistant runs in a sandbox on this machine.',

View File

@@ -31,6 +31,7 @@ import { brightSelect } from '../lib/bright-select.js';
import { confirmThenOpen } from '../lib/browser.js'; import { confirmThenOpen } from '../lib/browser.js';
import { askOperatorRole } from '../lib/role-prompt.js'; import { askOperatorRole } from '../lib/role-prompt.js';
import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js'; import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js';
import { note } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano'; const DEFAULT_AGENT_NAME = 'Nano';
const DISCORD_API = 'https://discord.com/api/v10'; const DISCORD_API = 'https://discord.com/api/v10';
@@ -155,7 +156,7 @@ async function askHasBotToken(): Promise<boolean> {
async function walkThroughBotCreation(): Promise<void> { async function walkThroughBotCreation(): Promise<void> {
const url = 'https://discord.com/developers/applications'; const url = 'https://discord.com/developers/applications';
p.note( note(
[ [
"You'll create a Discord bot in the Developer Portal. It's free and takes about a minute.", "You'll create a Discord bot in the Developer Portal. It's free and takes about a minute.",
'', '',
@@ -184,7 +185,7 @@ function showTokenLocationReminder(hasExistingBot: boolean): void {
// to find it — tokens in the Dev Portal aren't visible after first reveal, // to find it — tokens in the Dev Portal aren't visible after first reveal,
// and "Reset Token" issues a new one. // and "Reset Token" issues a new one.
if (hasExistingBot) { if (hasExistingBot) {
p.note( note(
[ [
"Where to find your bot token:", "Where to find your bot token:",
'', '',
@@ -216,7 +217,7 @@ async function walkThroughServerCreation(): Promise<void> {
// the web client and rely on the + button being visible. The steps below // the web client and rely on the + button being visible. The steps below
// are the same whether they're in the desktop app or the browser. // are the same whether they're in the desktop app or the browser.
const url = 'https://discord.com/channels/@me'; const url = 'https://discord.com/channels/@me';
p.note( note(
[ [
"A Discord server is just a private space for you and the bot. Free and takes 30 seconds.", "A Discord server is just a private space for you and the bot. Free and takes 30 seconds.",
'', '',
@@ -392,7 +393,7 @@ async function resolveOwnerUserId(
} }
async function promptForUserIdWithDevMode(): Promise<string> { async function promptForUserIdWithDevMode(): Promise<string> {
p.note( note(
[ [
"To get your Discord user ID:", "To get your Discord user ID:",
'', '',
@@ -430,7 +431,7 @@ async function promptInviteBot(
`&scope=bot` + `&scope=bot` +
`&permissions=${INVITE_PERMISSIONS}`; `&permissions=${INVITE_PERMISSIONS}`;
p.note( note(
[ [
`@${botUsername} needs to share a server with you before it can DM you.`, `@${botUsername} needs to share a server with you before it can DM you.`,
'', '',

View File

@@ -36,7 +36,7 @@ import * as setupLog from '../logs.js';
import { brightSelect } from '../lib/bright-select.js'; import { brightSelect } from '../lib/bright-select.js';
import { askOperatorRole } from '../lib/role-prompt.js'; import { askOperatorRole } from '../lib/role-prompt.js';
import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js'; import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js';
import { wrapForGutter } from '../lib/theme.js'; import { note, wrapForGutter } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano'; const DEFAULT_AGENT_NAME = 'Nano';
@@ -189,7 +189,7 @@ async function walkThroughFullDiskAccess(): Promise<void> {
} }
const nodeDir = path.dirname(nodePath); const nodeDir = path.dirname(nodePath);
p.note( note(
wrapForGutter( wrapForGutter(
[ [
`iMessage needs Full Disk Access granted to the Node binary:`, `iMessage needs Full Disk Access granted to the Node binary:`,
@@ -222,7 +222,7 @@ async function walkThroughFullDiskAccess(): Promise<void> {
} }
async function collectRemoteCreds(): Promise<RemoteCreds> { async function collectRemoteCreds(): Promise<RemoteCreds> {
p.note( note(
[ [
"Photon is a separate service that owns an iMessage account and", "Photon is a separate service that owns an iMessage account and",
"exposes it over HTTP. NanoClaw will talk to it via its API.", "exposes it over HTTP. NanoClaw will talk to it via its API.",
@@ -264,7 +264,7 @@ async function collectRemoteCreds(): Promise<RemoteCreds> {
} }
async function askOperatorHandle(): Promise<string> { async function askOperatorHandle(): Promise<string> {
p.note( note(
[ [
"What phone number or email do you iMessage with?", "What phone number or email do you iMessage with?",
"That's where your assistant will send its welcome message.", "That's where your assistant will send its welcome message.",

View File

@@ -44,6 +44,7 @@ import {
writeStepEntry, writeStepEntry,
} from '../lib/runner.js'; } from '../lib/runner.js';
import { askOperatorRole } from '../lib/role-prompt.js'; import { askOperatorRole } from '../lib/role-prompt.js';
import { note } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano'; const DEFAULT_AGENT_NAME = 'Nano';
@@ -139,7 +140,7 @@ async function ensureSignalCli(): Promise<void> {
if (!probe.error && probe.status === 0) return; if (!probe.error && probe.status === 0) return;
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
p.note( note(
[ [
"NanoClaw talks to Signal through signal-cli, which isn't installed yet.", "NanoClaw talks to Signal through signal-cli, which isn't installed yet.",
'', '',
@@ -152,7 +153,7 @@ async function ensureSignalCli(): Promise<void> {
'signal-cli not found', 'signal-cli not found',
); );
} else { } else {
p.note( note(
[ [
"NanoClaw talks to Signal through signal-cli, which isn't installed yet.", "NanoClaw talks to Signal through signal-cli, which isn't installed yet.",
'', '',

View File

@@ -28,7 +28,7 @@ import * as setupLog from '../logs.js';
import { confirmThenOpen } from '../lib/browser.js'; import { confirmThenOpen } from '../lib/browser.js';
import { askOperatorRole } from '../lib/role-prompt.js'; import { askOperatorRole } from '../lib/role-prompt.js';
import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js'; import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js';
import { wrapForGutter } from '../lib/theme.js'; import { note, wrapForGutter } from '../lib/theme.js';
const SLACK_API = 'https://slack.com/api'; const SLACK_API = 'https://slack.com/api';
const SLACK_APPS_URL = 'https://api.slack.com/apps'; const SLACK_APPS_URL = 'https://api.slack.com/apps';
@@ -121,7 +121,7 @@ export async function runSlackChannel(displayName: string): Promise<void> {
} }
async function walkThroughAppCreation(): Promise<void> { async function walkThroughAppCreation(): Promise<void> {
p.note( note(
[ [
"You'll create a Slack app that the assistant talks through.", "You'll create a Slack app that the assistant talks through.",
"Free and stays inside the workspaces you pick.", "Free and stays inside the workspaces you pick.",
@@ -262,7 +262,7 @@ async function validateSlackToken(token: string): Promise<WorkspaceInfo> {
} }
async function collectSlackUserId(): Promise<string> { async function collectSlackUserId(): Promise<string> {
p.note( note(
[ [
"To get your Slack member ID:", "To get your Slack member ID:",
'', '',
@@ -367,7 +367,7 @@ async function resolveAgentName(): Promise<string> {
} }
function showPostInstallChecklist(info: WorkspaceInfo): void { function showPostInstallChecklist(info: WorkspaceInfo): void {
p.note( note(
wrapForGutter( wrapForGutter(
[ [
`Your agent is wired to Slack and a welcome DM is on its way.`, `Your agent is wired to Slack and a welcome DM is on its way.`,

View File

@@ -40,6 +40,7 @@ import {
} from '../lib/claude-handoff.js'; } from '../lib/claude-handoff.js';
import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js'; import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js';
import { buildTeamsAppPackage } from '../lib/teams-manifest.js'; import { buildTeamsAppPackage } from '../lib/teams-manifest.js';
import { note } from '../lib/theme.js';
import * as setupLog from '../logs.js'; import * as setupLog from '../logs.js';
const CHANNEL = 'teams'; const CHANNEL = 'teams';
@@ -79,7 +80,7 @@ export async function runTeamsChannel(_displayName: string): Promise<void> {
// ─── step: intro / prereqs ────────────────────────────────────────────── // ─── step: intro / prereqs ──────────────────────────────────────────────
function printIntro(): void { function printIntro(): void {
p.note( note(
[ [
'Setting up Teams is more involved than the other channels — about', 'Setting up Teams is more involved than the other channels — about',
'7 steps across the Azure portal and Teams admin.', '7 steps across the Azure portal and Teams admin.',
@@ -93,7 +94,7 @@ function printIntro(): void {
} }
async function confirmPrereqs(args: { collected: Collected; completed: string[] }): Promise<void> { async function confirmPrereqs(args: { collected: Collected; completed: string[] }): Promise<void> {
p.note( note(
[ [
'Before we start, confirm you have:', 'Before we start, confirm you have:',
'', '',
@@ -119,7 +120,7 @@ async function confirmPrereqs(args: { collected: Collected; completed: string[]
// ─── step: public URL ────────────────────────────────────────────────── // ─── step: public URL ──────────────────────────────────────────────────
async function stepPublicUrl(args: { collected: Collected; completed: string[] }): Promise<void> { async function stepPublicUrl(args: { collected: Collected; completed: string[] }): Promise<void> {
p.note( note(
[ [
"Azure Bot Service delivers messages to an HTTPS endpoint you", "Azure Bot Service delivers messages to an HTTPS endpoint you",
"control. The endpoint needs to reach this machine's webhook", "control. The endpoint needs to reach this machine's webhook",
@@ -175,7 +176,7 @@ async function stepAppRegistration(args: {
collected: Collected; collected: Collected;
completed: string[]; completed: string[];
}): Promise<void> { }): Promise<void> {
p.note( note(
[ [
`1. In ${AZURE_PORTAL_URL}, search "App registrations" → "New registration"`, `1. In ${AZURE_PORTAL_URL}, search "App registrations" → "New registration"`,
'2. Name it (e.g. "NanoClaw")', '2. Name it (e.g. "NanoClaw")',
@@ -259,7 +260,7 @@ async function stepClientSecret(args: {
collected: Collected; collected: Collected;
completed: string[]; completed: string[];
}): Promise<void> { }): Promise<void> {
p.note( note(
[ [
`1. In your app registration, open "Certificates & secrets"`, `1. In your app registration, open "Certificates & secrets"`,
'2. Click "New client secret"', '2. Click "New client secret"',
@@ -328,7 +329,7 @@ async function stepAzureBot(args: {
` --appid ${args.collected.appId} \\\n` + ` --appid ${args.collected.appId} \\\n` +
` ${tenantFlag}--endpoint "${endpoint}"`; ` ${tenantFlag}--endpoint "${endpoint}"`;
p.note( note(
[ [
`In ${AZURE_PORTAL_URL}, search "Azure Bot" → Create.`, `In ${AZURE_PORTAL_URL}, search "Azure Bot" → Create.`,
'', '',
@@ -365,7 +366,7 @@ async function stepEnableTeamsChannel(args: {
collected: Collected; collected: Collected;
completed: string[]; completed: string[];
}): Promise<void> { }): Promise<void> {
p.note( note(
[ [
'1. Open your Azure Bot resource → Channels', '1. Open your Azure Bot resource → Channels',
'2. Click Microsoft Teams → Accept terms → Apply', '2. Click Microsoft Teams → Accept terms → Apply',
@@ -435,7 +436,7 @@ async function stepSideload(args: {
completed: string[]; completed: string[];
zipPath: string; zipPath: string;
}): Promise<void> { }): Promise<void> {
p.note( note(
[ [
'1. Open Microsoft Teams', '1. Open Microsoft Teams',
'2. Go to Apps → Manage your apps → Upload an app', '2. Go to Apps → Manage your apps → Upload an app',
@@ -501,7 +502,7 @@ async function finishWithHandoff(
collected: Collected, collected: Collected,
completed: string[], completed: string[],
): Promise<void> { ): Promise<void> {
p.note( note(
[ [
'The Teams adapter is live and the service is running.', 'The Teams adapter is live and the service is running.',
'', '',
@@ -530,7 +531,7 @@ async function finishWithHandoff(
); );
if (choice === 'self') { if (choice === 'self') {
p.note( note(
[ [
' 1. Find your bot in Teams (search by name, or via the sideloaded', ' 1. Find your bot in Teams (search by name, or via the sideloaded',
' app) and send it a message ("hi" is fine)', ' app) and send it a message ("hi" is fine)',

View File

@@ -33,7 +33,7 @@ import {
spawnStep, spawnStep,
writeStepEntry, writeStepEntry,
} from '../lib/runner.js'; } from '../lib/runner.js';
import { brandBold } from '../lib/theme.js'; import { brandBold, note } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano'; const DEFAULT_AGENT_NAME = 'Nano';
@@ -47,7 +47,7 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
// installed, or the bot's web profile if not. tg://resolve?domain= is // installed, or the bot's web profile if not. tg://resolve?domain= is
// more direct but silently fails when the scheme isn't registered. // more direct but silently fails when the scheme isn't registered.
const botUrl = `https://t.me/${botUsername}`; const botUrl = `https://t.me/${botUsername}`;
p.note( note(
[ [
`Opening @${botUsername} in Telegram so it's ready when the pairing code shows up.`, `Opening @${botUsername} in Telegram so it's ready when the pairing code shows up.`,
'', '',
@@ -132,7 +132,7 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
} }
async function collectTelegramToken(): Promise<string> { async function collectTelegramToken(): Promise<string> {
p.note( note(
[ [
"Your assistant talks to you through a Telegram bot you create.", "Your assistant talks to you through a Telegram bot you create.",
"Here's how:", "Here's how:",
@@ -240,7 +240,7 @@ async function runPairTelegram(): Promise<
} else { } else {
stopSpinner("Old code expired. Here's a fresh one."); stopSpinner("Old code expired. Here's a fresh one.");
} }
p.note(formatCodeCard(block.fields.CODE ?? '????'), 'Secret code'); note(formatCodeCard(block.fields.CODE ?? '????'), 'Secret code');
s.start('Waiting for you to send the code from Telegram…'); s.start('Waiting for you to send the code from Telegram…');
spinnerActive = true; spinnerActive = true;
} else if (block.type === 'PAIR_TELEGRAM_ATTEMPT') { } else if (block.type === 'PAIR_TELEGRAM_ATTEMPT') {

View File

@@ -46,7 +46,7 @@ import {
writeStepEntry, writeStepEntry,
} from '../lib/runner.js'; } from '../lib/runner.js';
import { askOperatorRole } from '../lib/role-prompt.js'; import { askOperatorRole } from '../lib/role-prompt.js';
import { brandBold } from '../lib/theme.js'; import { brandBold, note } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano'; const DEFAULT_AGENT_NAME = 'Nano';
const AUTH_CREDS_PATH = path.join(process.cwd(), 'store', 'auth', 'creds.json'); const AUTH_CREDS_PATH = path.join(process.cwd(), 'store', 'auth', 'creds.json');
@@ -171,7 +171,7 @@ async function askAuthMethod(): Promise<AuthMethod> {
} }
async function askPhoneNumber(): Promise<string> { async function askPhoneNumber(): Promise<string> {
p.note( note(
[ [
"Enter your phone number the way WhatsApp expects it:", "Enter your phone number the way WhatsApp expects it:",
'', '',
@@ -249,7 +249,7 @@ async function runWhatsAppAuth(
} else if (block.type === 'WHATSAPP_AUTH_PAIRING_CODE') { } else if (block.type === 'WHATSAPP_AUTH_PAIRING_CODE') {
const code = block.fields.CODE ?? '????'; const code = block.fields.CODE ?? '????';
stopSpinner('Your pairing code is ready.'); stopSpinner('Your pairing code is ready.');
p.note(formatPairingCard(code), 'Pairing code'); note(formatPairingCard(code), 'Pairing code');
s.start('Waiting for you to enter the code…'); s.start('Waiting for you to enter the code…');
spinnerActive = true; spinnerActive = true;
} else if (block.type === 'WHATSAPP_AUTH') { } else if (block.type === 'WHATSAPP_AUTH') {
@@ -395,7 +395,7 @@ async function restartService(): Promise<void> {
} }
async function askChatPhone(authedPhone: string): Promise<string> { async function askChatPhone(authedPhone: string): Promise<string> {
p.note( note(
[ [
`Authenticated with ${k.cyan('+' + authedPhone)}.`, `Authenticated with ${k.cyan('+' + authedPhone)}.`,
'', '',

View File

@@ -24,7 +24,7 @@ import * as p from '@clack/prompts';
import k from 'kleur'; import k from 'kleur';
import { ensureAnswer } from './runner.js'; import { ensureAnswer } from './runner.js';
import { fitToWidth } from './theme.js'; import { fitToWidth, note } from './theme.js';
export interface AssistContext { export interface AssistContext {
stepName: string; stepName: string;
@@ -111,7 +111,7 @@ export async function offerClaudeAssist(
return false; return false;
} }
p.note( note(
`${parsed.reason}\n\n${k.cyan('$')} ${parsed.command}`, `${parsed.reason}\n\n${k.cyan('$')} ${parsed.command}`,
"Claude's suggestion", "Claude's suggestion",
); );

View File

@@ -27,6 +27,8 @@ import { execSync, spawn } from 'child_process';
import * as p from '@clack/prompts'; import * as p from '@clack/prompts';
import k from 'kleur'; import k from 'kleur';
import { note } from './theme.js';
export interface HandoffContext { export interface HandoffContext {
/** Channel this handoff is happening in (e.g., 'teams'). */ /** Channel this handoff is happening in (e.g., 'teams'). */
channel: string; channel: string;
@@ -69,7 +71,7 @@ export async function offerClaudeHandoff(ctx: HandoffContext): Promise<boolean>
const systemPrompt = buildSystemPrompt(ctx); const systemPrompt = buildSystemPrompt(ctx);
p.note( note(
[ [
"I'm handing you off to Claude in interactive mode.", "I'm handing you off to Claude in interactive mode.",
"It has the context of where you are in setup.", "It has the context of where you are in setup.",

View File

@@ -11,6 +11,7 @@
* - COLORTERM truecolor/24bit → 24-bit ANSI (exact brand cyan) * - COLORTERM truecolor/24bit → 24-bit ANSI (exact brand cyan)
* - Otherwise → kleur's 16-color cyan (closest fallback) * - Otherwise → kleur's 16-color cyan (closest fallback)
*/ */
import * as p from '@clack/prompts';
import k from 'kleur'; import k from 'kleur';
const USE_ANSI = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR; const USE_ANSI = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
@@ -68,6 +69,19 @@ export function dimWrap(text: string, gutter: number): string {
return wrapForGutter(text, gutter); return wrapForGutter(text, gutter);
} }
/**
* Wrap clack's `p.note` with the dim formatter disabled. By default
* clack renders note bodies through `styleText("dim", …)`, which the
* project's prose-readability stance (see `dimWrap` above) explicitly
* rejects. Pass-through formatter keeps body text at the terminal's
* regular weight; pre-styled segments (chips, bold, brand color) come
* through unfaded.
*/
const passthroughFormat = (s: string): string => s;
export function note(message: string, title?: string): void {
p.note(message, title, { format: passthroughFormat });
}
const ANSI_RE = /\x1b\[[0-9;]*m/g; const ANSI_RE = /\x1b\[[0-9;]*m/g;
function visibleLength(s: string): number { function visibleLength(s: string): number {