Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import tab from '@bomb.sh/tab/citty'
import { add } from '@vuetify/cli-shared/commands/add'
import { diff } from '@vuetify/cli-shared/commands/diff'
import { generate } from '@vuetify/cli-shared/commands/generate'
import { list } from '@vuetify/cli-shared/commands/list'
import { mcp } from '@vuetify/cli-shared/commands/mcp'
import { createPresetsCommand } from '@vuetify/cli-shared/commands/presets'
import { refresh } from '@vuetify/cli-shared/commands/refresh'
import { registry } from '@vuetify/cli-shared/commands/registry'
import { status } from '@vuetify/cli-shared/commands/status'
import { registerProjectArgsCompletion } from '@vuetify/cli-shared/completion'
import { i18n } from '@vuetify/cli-shared/i18n'
import { createBanner } from '@vuetify/cli-shared/utils/banner'
Expand All @@ -26,6 +32,12 @@ export const main = defineCommand({
init,
presets,
add,
generate,
diff,
refresh,
registry,
list,
status,
mcp,
update,
docs,
Expand Down
92 changes: 81 additions & 11 deletions packages/shared/src/commands/add.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { log, select } from '@clack/prompts'
import { intro, log, outro, select, spinner } from '@clack/prompts'
import { defineCommand, runCommand } from 'citty'
import { addEslint } from '../functions'
import { addEslint, addFeature } from '../functions'
import { loadInventory, parseFeatureRef, resolveRegistryUrl } from '../functions/inventory'
import { getIndex } from '../functions/registry'
import { groupedRegistryOptions } from '../functions/registry-options'
import { i18n } from '../i18n'
import { mcp } from './mcp'

/**
* Integrations wire a tool into the project. Everything else typed after `add`
* is looked up in the registry, so `vuetify add dialog` writes a working,
* styled example instead of leaving the user to copy it out of the docs.
*/
const choices = ['eslint']

export const add = defineCommand({
Expand All @@ -14,38 +22,100 @@ export const add = defineCommand({
args: {
integration: {
type: 'positional',
required: false,
description: i18n.t('commands.add.integration.description', { choices: choices.join(', ') }),
},
example: {
type: 'string',
description: i18n.t('commands.add.args.example'),
},
dir: {
type: 'string',
description: i18n.t('commands.add.args.dir'),
},
registry: {
type: 'string',
description: i18n.t('commands.add.args.registry'),
},
overwrite: {
type: 'boolean',
default: false,
description: i18n.t('commands.add.args.overwrite'),
},
yes: {
type: 'boolean',
default: false,
description: i18n.t('commands.add.args.yes'),
},
},
run: async ({ args }) => {
let integration = args.integration
const inventory = await loadInventory()

if (integration === 'mcp') {
log.warning('The "vuetify add mcp" command is deprecated. Redirecting to "vuetify mcp install"...')
await runCommand(mcp, { rawArgs: ['install'] })
return
}

// With no argument, offer integrations and registry items in one list so
// neither surface is hidden behind knowing its name up front.
if (!integration) {
const origin = resolveRegistryUrl(inventory, args.registry)
const loader = spinner()
loader.start(i18n.t('spinners.registry.fetching'))
const index = await getIndex(origin)
loader.stop(i18n.t('spinners.registry.fetched'))

const selected = await select({
message: i18n.t('prompts.add.integration'),
options: choices.map(c => ({ label: c, value: c })),
message: i18n.t('prompts.add.feature'),
options: [
{ label: '── Integrations', value: '__group:Integrations', disabled: true },
...choices.map(choice => ({ label: choice, value: choice, hint: 'integration' })),
...groupedRegistryOptions(index.items),
],
})

if (typeof selected === 'symbol') {
log.warning(i18n.t('commands.add.integration.available', { choices: choices.join(', ') }))
return
}

if (String(selected).startsWith('__group:')) {
log.warning(i18n.t('commands.add.integration.available', { choices: choices.join(', ') }))
return
}

integration = String(selected)
}
if (!choices.includes(integration)) {
log.error(i18n.t('commands.add.integration.invalid', { integration, choices: choices.join(', ') }))

if (choices.includes(integration)) {
switch (integration) {
case 'eslint': {
await addEslint()
break
}
}
return
}
switch (integration) {
case 'eslint': {
await addEslint()
break
}

const ref = parseFeatureRef(integration)
const registry = resolveRegistryUrl(inventory, args.registry ?? ref.registry)

intro(i18n.t('commands.add.intro', { name: ref.name }))

const written = await addFeature({
name: ref.name,
example: args.example,
dir: args.dir,
registry,
overwrite: args.overwrite,
yes: args.yes,
})

// A failed resolution has already said why — don't follow it with "all done".
if (written.length > 0) {
outro(i18n.t('messages.all_done'))
}
},
})
49 changes: 49 additions & 0 deletions packages/shared/src/commands/diff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { intro, log, outro } from '@clack/prompts'
import { defineCommand } from 'citty'
import { dim, green, red, yellow } from 'kolorist'
import { diffComponent } from '../functions/diff'
import { i18n } from '../i18n'

const mark = {
same: () => green('='),
changed: () => yellow('~'),
'missing-local': () => red('!'),
'only-remote': () => red('+'),
'only-local': () => dim('·'),
} as const

export const diff = defineCommand({
meta: {
name: 'diff',
description: i18n.t('commands.diff.description'),
},
args: {
name: {
type: 'positional',
required: true,
description: i18n.t('commands.diff.args.name'),
},
registry: {
type: 'string',
description: i18n.t('commands.add.args.registry'),
},
},
run: async ({ args }) => {
intro(i18n.t('commands.diff.intro', { name: args.name }))
try {
const result = await diffComponent(String(args.name), { registry: args.registry })
if (result.origin) {
log.message(dim(result.origin))
}
for (const line of result.lines) {
log.message(`${mark[line.status]()} ${line.file} ${dim(line.status)}`)
}
const dirty = result.lines.some(l => l.status !== 'same' && l.status !== 'only-local')
outro(i18n.t('commands.diff.done'))
if (dirty) process.exitCode = 1
} catch (error) {
log.error(error instanceof Error ? error.message : String(error))
process.exitCode = 1
}
},
})
44 changes: 44 additions & 0 deletions packages/shared/src/commands/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { intro, log, outro } from '@clack/prompts'
import { defineCommand } from 'citty'
import { underline } from 'kolorist'
import { generateComponent } from '../functions/generate'
import { i18n } from '../i18n'

export const generate = defineCommand({
meta: {
name: 'generate',
description: i18n.t('commands.generate.description'),
},
args: {
name: {
type: 'positional',
required: true,
description: i18n.t('commands.generate.args.name'),
},
dir: {
type: 'string',
description: i18n.t('commands.generate.args.dir'),
},
overwrite: {
type: 'boolean',
default: false,
description: i18n.t('commands.generate.args.overwrite'),
},
},
run: async ({ args }) => {
intro(i18n.t('commands.generate.intro', { name: args.name }))
try {
const result = await generateComponent({
name: String(args.name),
dir: args.dir,
overwrite: args.overwrite,
})
log.success(i18n.t('commands.generate.wrote', { path: underline(result.path) }))
log.message(i18n.t('commands.add.inventory', { file: 'vuetify.json' }))
outro(i18n.t('messages.all_done'))
} catch (error) {
log.error(error instanceof Error ? error.message : String(error))
process.exitCode = 1
}
},
})
6 changes: 6 additions & 0 deletions packages/shared/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
export * from './add'
export * from './diff'
export * from './docs'
export * from './generate'
export * from './list'
export * from './mcp'
export * from './presets'
export * from './refresh'
export * from './registry'
export * from './releaseNotes'
export * from './status'
export * from './update'
export * from './upgrade'
58 changes: 58 additions & 0 deletions packages/shared/src/commands/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { intro, log, outro } from '@clack/prompts'
import { defineCommand } from 'citty'
import { dim, green, yellow } from 'kolorist'
import { componentStatus, loadInventory } from '../functions/inventory'
import { i18n } from '../i18n'

/**
* Print the local component inventory (`vuetify.json`).
*
* Tracking surface for phase 1 of the component-library lifecycle — what was
* seeded via `vuetify add`, and where it lives on disk.
*/
export const list = defineCommand({
meta: {
name: 'list',
description: i18n.t('commands.list.description'),
},
args: {
json: {
type: 'boolean',
default: false,
description: i18n.t('commands.list.args.json'),
},
},
run: async ({ args }) => {
const inventory = await loadInventory()
const names = Object.keys(inventory.components).toSorted()

if (args.json) {
console.log(JSON.stringify(inventory, null, 2))
return
}

intro(i18n.t('commands.list.intro'))

if (names.length === 0) {
log.info(i18n.t('commands.list.empty'))
outro(i18n.t('commands.list.hint'))
return
}

for (const name of names) {
const component = inventory.components[name]!
const { ok, missing } = componentStatus(component)
const origin = component.origin
? dim(` ← ${component.origin.name}/${component.origin.example}`)
: dim(` ← ${i18n.t('commands.list.local')}`)
const mark = ok ? green('✓') : yellow('!')
const label = component.title || name
log.message(`${mark} ${label} ${dim(component.path)}${origin}`)
if (!ok) {
log.warn(i18n.t('commands.list.missing', { name, files: missing.join(', ') }))
}
}

outro(i18n.t('commands.list.count', { count: names.length }))
},
})
45 changes: 45 additions & 0 deletions packages/shared/src/commands/refresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { intro, log, outro } from '@clack/prompts'
import { defineCommand } from 'citty'
import { refreshComponent } from '../functions/refresh'
import { i18n } from '../i18n'

export const refresh = defineCommand({
meta: {
name: 'refresh',
description: i18n.t('commands.refresh.description'),
},
args: {
name: {
type: 'positional',
required: true,
description: i18n.t('commands.refresh.args.name'),
},
registry: {
type: 'string',
description: i18n.t('commands.add.args.registry'),
},
yes: {
type: 'boolean',
default: true,
description: i18n.t('commands.add.args.yes'),
},
},
run: async ({ args }) => {
intro(i18n.t('commands.refresh.intro', { name: args.name }))
try {
const written = await refreshComponent({
name: String(args.name),
registry: args.registry,
yes: args.yes,
overwrite: true,
})
if (written.length === 0) {
log.warn(i18n.t('commands.refresh.noop'))
}
outro(i18n.t('messages.all_done'))
} catch (error) {
log.error(error instanceof Error ? error.message : String(error))
process.exitCode = 1
}
},
})
Loading