11import { join } from 'node:path' ;
22import { readFile , writeFile , mkdir } from 'node:fs/promises' ;
33import type { Command } from 'commander' ;
4- import chalk from 'chalk ' ;
4+ import pc from 'picocolors ' ;
55import { stringify , parse } from 'yaml' ;
6- import { select , checkbox , confirm } from '@inquirer/prompts' ;
76import { fetchRawContent , fetchContent , listDirectory , listContentDirectory } from '../utils/github.js' ;
87import { convert } from '../core/converter.js' ;
98import { isAssetType , parseAssetFrontmatter } from '../core/assets.js' ;
109import { fileExists } from '../utils/fs.js' ;
1110import { 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' ;
1220import * as cache from '../utils/cache.js' ;
1321import * as ui from '../utils/ui.js' ;
1422import { 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
424441async 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
492512async 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
645668interface 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
797826export function registerAddCommand ( program : Command ) : void {
0 commit comments