Skip to content

Commit 9d9738d

Browse files
committed
feat(cli): complete phase 4 visual overhaul
1 parent 7c6335c commit 9d9738d

21 files changed

Lines changed: 912 additions & 500 deletions

packages/cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@
5252
"test:e2e": "tsc && tsc -p tsconfig.test.json && find .test-build/tests/e2e -name '*.test.js' -exec node --test {} +"
5353
},
5454
"dependencies": {
55-
"@inquirer/prompts": "^7.0.0",
55+
"@clack/prompts": "^0.9.0",
5656
"chokidar": "^3.6.0",
5757
"commander": "^13.0.0",
5858
"yaml": "^2.7.0",
59-
"chalk": "^5.4.0"
59+
"picocolors": "^1.1.0"
6060
},
6161
"devDependencies": {
6262
"typescript": "^5.7.0",

packages/cli/src/commands/add.ts

Lines changed: 75 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import { join } from 'node:path';
22
import { readFile, writeFile, mkdir } from 'node:fs/promises';
33
import type { Command } from 'commander';
4-
import chalk from 'chalk';
4+
import pc from 'picocolors';
55
import { stringify, parse } from 'yaml';
6-
import { select, checkbox, confirm } from '@inquirer/prompts';
76
import { fetchRawContent, fetchContent, listDirectory, listContentDirectory } from '../utils/github.js';
87
import { convert } from '../core/converter.js';
98
import { isAssetType, parseAssetFrontmatter } from '../core/assets.js';
109
import { fileExists } from '../utils/fs.js';
1110
import { readConfig } from '../core/parser.js';
11+
import {
12+
selectPrompt,
13+
multiselectPrompt,
14+
confirmPrompt,
15+
introPrompt,
16+
outroPrompt,
17+
spinnerTask,
18+
isInteractiveSession,
19+
} from '../utils/prompt.js';
1220
import * as cache from '../utils/cache.js';
1321
import * as ui from '../utils/ui.js';
1422
import { ICONS } from '../utils/ui.js';
@@ -58,7 +66,10 @@ export async function fetchRegistry(cwd: string): Promise<CachedRegistry | null>
5866

5967
let topLevel;
6068
try {
61-
topLevel = await listDirectory();
69+
topLevel = await spinnerTask({
70+
label: 'Fetching rule categories',
71+
task: async () => listDirectory(),
72+
});
6273
} catch (err) {
6374
const msg = err instanceof Error ? err.message : String(err);
6475
ui.error(`Could not fetch rule registry: ${msg}`);
@@ -129,15 +140,15 @@ async function runList(categoryFilter: string | undefined): Promise<void> {
129140
ui.newline();
130141

131142
for (const category of displayCategories) {
132-
console.log(` ${chalk.cyan(`${category.name}/`)}`);
143+
console.log(` ${pc.cyan(`${category.name}/`)}`);
133144
for (const rule of category.rules) {
134-
const desc = rule.description ? chalk.dim(` ${rule.description}`) : '';
135-
console.log(` ${chalk.white(rule.name.padEnd(20))}${desc}`);
145+
const desc = rule.description ? pc.dim(` ${rule.description}`) : '';
146+
console.log(` ${pc.white(rule.name.padEnd(20))}${desc}`);
136147
}
137148
ui.newline();
138149
}
139150

140-
console.log(` ${chalk.dim(`Add a rule: devw add <category>/<rule>`)}`);
151+
console.log(` ${pc.dim(`Add a rule: devw add <category>/<rule>`)}`);
141152

142153
// Show available assets if not filtering by category
143154
if (!categoryFilter) {
@@ -161,13 +172,13 @@ async function runList(categoryFilter: string | undefined): Promise<void> {
161172
const names = result.value.filter((e) => e.type === 'file').map((e) => e.name);
162173
if (names.length === 0) continue;
163174
const singular = type.replace(/s$/, '');
164-
console.log(` ${chalk.cyan(`${singular}/`)}`);
175+
console.log(` ${pc.cyan(`${singular}/`)}`);
165176
for (const name of names) {
166-
console.log(` ${chalk.white(name)}`);
177+
console.log(` ${pc.white(name)}`);
167178
}
168179
ui.newline();
169180
}
170-
console.log(` ${chalk.dim(`Add an asset: devw add command/<name>`)}`);
181+
console.log(` ${pc.dim(`Add an asset: devw add command/<name>`)}`);
171182
}
172183
}
173184
}
@@ -265,7 +276,10 @@ export async function downloadAndInstallAsset(
265276

266277
let content: string;
267278
try {
268-
content = await fetchContent(getAssetContentPath(type, name));
279+
content = await spinnerTask({
280+
label: `Fetching ${source}`,
281+
task: async () => fetchContent(getAssetContentPath(type, name)),
282+
});
269283
} catch (err) {
270284
const msg = err instanceof Error ? err.message : String(err);
271285
ui.error(msg);
@@ -290,9 +304,9 @@ export async function downloadAndInstallAsset(
290304
if (!options.force) {
291305
ui.info(`${source} already exists locally`);
292306
try {
293-
const shouldOverwrite = await confirm({
307+
const shouldOverwrite = await confirmPrompt({
294308
message: 'Overwrite?',
295-
default: true,
309+
defaultValue: true,
296310
});
297311
if (!shouldOverwrite) {
298312
ui.error('Cancelled');
@@ -309,7 +323,7 @@ export async function downloadAndInstallAsset(
309323
ui.newline();
310324
ui.header('Dry run — would write:');
311325
ui.newline();
312-
console.log(chalk.dim(` .dwf/assets/${type}s/${fileName}`));
326+
console.log(pc.dim(` .dwf/assets/${type}s/${fileName}`));
313327
return false;
314328
}
315329

@@ -342,7 +356,10 @@ async function downloadAndInstall(
342356

343357
let markdown: string;
344358
try {
345-
markdown = await fetchRawContent(source);
359+
markdown = await spinnerTask({
360+
label: `Fetching ${source}`,
361+
task: async () => fetchRawContent(source),
362+
});
346363
} catch (err) {
347364
const msg = err instanceof Error ? err.message : String(err);
348365
ui.error(msg);
@@ -376,10 +393,10 @@ async function downloadAndInstall(
376393
ui.newline();
377394
ui.info(`${source} already exists locally (v${existingVersion} ${ICONS.arrow} v${result.version})`);
378395
try {
379-
const shouldOverwrite = await confirm({
380-
message: 'Overwrite with new version?',
381-
default: true,
382-
});
396+
const shouldOverwrite = await confirmPrompt({
397+
message: 'Overwrite with new version?',
398+
defaultValue: true,
399+
});
383400
if (!shouldOverwrite) {
384401
ui.error('Cancelled');
385402
return false;
@@ -401,7 +418,7 @@ async function downloadAndInstall(
401418
ui.newline();
402419
ui.header('Dry run — would write:');
403420
ui.newline();
404-
console.log(chalk.dim(` ${fileName}`));
421+
console.log(pc.dim(` ${fileName}`));
405422
ui.newline();
406423
console.log(yamlOutput);
407424
return false;
@@ -422,15 +439,16 @@ async function downloadAndInstall(
422439
}
423440

424441
async function runInteractiveAsset(cwd: string, options: AddOptions): Promise<void> {
442+
introPrompt('Add assets');
425443
let assetType: AssetType | 'preset';
426444
try {
427-
assetType = await select<AssetType | 'preset'>({
445+
assetType = await selectPrompt<AssetType | 'preset'>({
428446
message: 'Asset type',
429-
choices: [
430-
{ name: 'command — Slash commands for Claude Code', value: 'command' },
431-
{ name: 'template — Spec and document templates', value: 'template' },
432-
{ name: 'hook — Editor hooks (auto-format, etc.)', value: 'hook' },
433-
{ name: 'preset — Bundle of rules + assets', value: 'preset' },
447+
options: [
448+
{ label: 'command — Slash commands for Claude Code', value: 'command' },
449+
{ label: 'template — Spec and document templates', value: 'template' },
450+
{ label: 'hook — Editor hooks (auto-format, etc.)', value: 'hook' },
451+
{ label: 'preset — Bundle of rules + assets', value: 'preset' },
434452
],
435453
});
436454
} catch {
@@ -458,9 +476,9 @@ async function runInteractiveAsset(cwd: string, options: AddOptions): Promise<vo
458476

459477
let selected: string[];
460478
try {
461-
selected = await checkbox<string>({
479+
selected = await multiselectPrompt<string>({
462480
message: `Select ${assetType}s to install`,
463-
choices: names.map((name) => ({ name, value: name })),
481+
options: names.map((name) => ({ label: name, value: name })),
464482
});
465483
} catch {
466484
ui.error('Cancelled');
@@ -487,16 +505,19 @@ async function runInteractiveAsset(cwd: string, options: AddOptions): Promise<vo
487505
const { runCompileFromAdd } = await import('./compile.js');
488506
await runCompileFromAdd();
489507
}
508+
509+
outroPrompt('Asset flow completed');
490510
}
491511

492512
async function runInteractive(cwd: string, options: AddOptions): Promise<void> {
513+
introPrompt('Add rules or assets');
493514
let mode: 'rules' | 'assets';
494515
try {
495-
mode = await select<'rules' | 'assets'>({
516+
mode = await selectPrompt<'rules' | 'assets'>({
496517
message: 'What do you want to add?',
497-
choices: [
498-
{ name: 'Rules — Install rules from the registry', value: 'rules' },
499-
{ name: 'Assets — Commands, templates, hooks, presets', value: 'assets' },
518+
options: [
519+
{ label: 'Rules — Install rules from the registry', value: 'rules' },
520+
{ label: 'Assets — Commands, templates, hooks, presets', value: 'assets' },
500521
],
501522
});
502523
} catch {
@@ -538,15 +559,15 @@ async function runInteractive(cwd: string, options: AddOptions): Promise<void> {
538559
);
539560
if (availableCategories.length === 0) break;
540561

541-
const selectedCategoryName = await select<string>({
562+
const selectedCategoryName = await selectPrompt<string>({
542563
message: 'Choose a category',
543-
choices: availableCategories.map((c) => {
564+
options: availableCategories.map((c) => {
544565
const allInstalled = c.rules.every((r) =>
545566
installedPaths.has(`${c.name}/${r.name}`),
546567
);
547568
const label = `${c.name} (${pluralRules(c.rules.length)})`;
548569
return {
549-
name: allInstalled ? `${label} ${chalk.dim('(all installed)')}` : label,
570+
label: allInstalled ? `${label} ${pc.dim('(all installed)')}` : label,
550571
value: c.name,
551572
};
552573
}),
@@ -555,17 +576,17 @@ async function runInteractive(cwd: string, options: AddOptions): Promise<void> {
555576
const category = registry.categories.find((c) => c.name === selectedCategoryName);
556577
if (!category) break;
557578

558-
const selected = await checkbox<string>({
579+
const selected = await multiselectPrompt<string>({
559580
message: 'Select rules to add',
560-
choices: [
561-
{ name: '\u2190 Back to categories', value: BACK_VALUE },
581+
options: [
582+
{ label: '\u2190 Back to categories', value: BACK_VALUE },
562583
...category.rules.map((r) => {
563584
const path = `${category.name}/${r.name}`;
564585
const installed = installedPaths.has(path);
565586
const desc = r.description ? ` ${ICONS.dash} ${r.description}` : '';
566-
const suffix = installed ? chalk.dim(' (already installed)') : '';
587+
const suffix = installed ? pc.dim(' (already installed)') : '';
567588
return {
568-
name: `${r.name}${desc}${suffix}`,
589+
label: `${r.name}${desc}${suffix}`,
569590
value: r.name,
570591
};
571592
}),
@@ -595,9 +616,9 @@ async function runInteractive(cwd: string, options: AddOptions): Promise<void> {
595616
);
596617
if (remaining.length === 0) break;
597618

598-
const addMore = await confirm({
619+
const addMore = await confirmPrompt({
599620
message: 'Add rules from another category?',
600-
default: true,
621+
defaultValue: true,
601622
});
602623
if (!addMore) break;
603624
}
@@ -611,15 +632,15 @@ async function runInteractive(cwd: string, options: AddOptions): Promise<void> {
611632
ui.newline();
612633
ui.header('Rules to install:');
613634
for (const rule of allSelected) {
614-
const desc = rule.description ? chalk.dim(` ${ICONS.dash} ${rule.description}`) : '';
635+
const desc = rule.description ? pc.dim(` ${ICONS.dash} ${rule.description}`) : '';
615636
console.log(` ${rule.category}/${rule.name}${desc}`);
616637
}
617638
ui.newline();
618639

619640
try {
620-
const shouldProceed = await confirm({
641+
const shouldProceed = await confirmPrompt({
621642
message: `Install ${pluralRules(allSelected.length)}?`,
622-
default: true,
643+
defaultValue: true,
623644
});
624645
if (!shouldProceed) {
625646
ui.error('Cancelled');
@@ -640,6 +661,8 @@ async function runInteractive(cwd: string, options: AddOptions): Promise<void> {
640661
const { runCompileFromAdd } = await import('./compile.js');
641662
await runCompileFromAdd();
642663
}
664+
665+
outroPrompt('Add flow completed');
643666
}
644667

645668
interface PresetManifest {
@@ -732,7 +755,7 @@ export async function runAdd(ruleArg: string | undefined, options: AddOptions):
732755
}
733756

734757
if (!ruleArg) {
735-
if (!process.stdout.isTTY || !process.stdin.isTTY) {
758+
if (!isInteractiveSession()) {
736759
ui.error('No rule specified', 'Usage: devw add <category>/<rule>');
737760
process.exitCode = 1;
738761
return;
@@ -742,6 +765,10 @@ export async function runAdd(ruleArg: string | undefined, options: AddOptions):
742765
return;
743766
}
744767

768+
if (isInteractiveSession()) {
769+
introPrompt('Adding item');
770+
}
771+
745772
if (!ruleArg.includes('/')) {
746773
const dashIdx = ruleArg.indexOf('-');
747774
const hint =
@@ -792,6 +819,8 @@ export async function runAdd(ruleArg: string | undefined, options: AddOptions):
792819
const { runCompileFromAdd } = await import('./compile.js');
793820
await runCompileFromAdd();
794821
}
822+
823+
outroPrompt('Add command completed');
795824
}
796825

797826
export function registerAddCommand(program: Command): void {

0 commit comments

Comments
 (0)