devvami 1.0.0 → 1.1.1

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.
@@ -0,0 +1,164 @@
1
+ import { Command, Args, Flags } from '@oclif/core'
2
+ import ora from 'ora'
3
+ import chalk from 'chalk'
4
+ import { select } from '@inquirer/prompts'
5
+ import { searchSkills } from '../../services/skills-sh.js'
6
+ import { fetchAwesomeEntries, AWESOME_CATEGORIES } from '../../services/awesome-copilot.js'
7
+ import { formatSkillTable, formatAwesomeTable } from '../../formatters/prompts.js'
8
+ import { DvmiError } from '../../utils/errors.js'
9
+
10
+ /** @import { Skill, AwesomeEntry } from '../../types.js' */
11
+
12
+ const VALID_SOURCES = ['skills', 'awesome']
13
+
14
+ export default class PromptsBrowse extends Command {
15
+ static description = 'Browse prompts and skills from external sources (skills.sh, awesome-copilot)'
16
+
17
+ static examples = [
18
+ '<%= config.bin %> prompts browse skills --query refactor',
19
+ '<%= config.bin %> prompts browse skills --query testing --json',
20
+ '<%= config.bin %> prompts browse awesome',
21
+ '<%= config.bin %> prompts browse awesome --category agents',
22
+ '<%= config.bin %> prompts browse awesome --category instructions --json',
23
+ ]
24
+
25
+ static enableJsonFlag = true
26
+
27
+ static args = {
28
+ source: Args.string({
29
+ description: `Source to browse: ${VALID_SOURCES.join(' | ')}`,
30
+ required: true,
31
+ options: VALID_SOURCES,
32
+ }),
33
+ }
34
+
35
+ static flags = {
36
+ query: Flags.string({
37
+ char: 'q',
38
+ description: 'Search query (only applies to skills source)',
39
+ }),
40
+ category: Flags.string({
41
+ char: 'c',
42
+ description: `awesome-copilot category (${AWESOME_CATEGORIES.join(', ')})`,
43
+ default: 'instructions',
44
+ }),
45
+ }
46
+
47
+ async run() {
48
+ const { args, flags } = await this.parse(PromptsBrowse)
49
+ const isJson = flags.json
50
+ const source = args.source
51
+
52
+ if (!VALID_SOURCES.includes(source)) {
53
+ this.error(`Invalid source: "${source}". Must be one of: ${VALID_SOURCES.join(', ')}`, {
54
+ exit: 1,
55
+ suggestions: VALID_SOURCES.map((s) => `dvmi prompts browse ${s}`),
56
+ })
57
+ }
58
+
59
+ const spinner = isJson
60
+ ? null
61
+ : ora({
62
+ spinner: 'arc',
63
+ color: false,
64
+ text: chalk.hex('#FF6B2B')(
65
+ source === 'skills' ? 'Searching skills.sh...' : `Fetching awesome-copilot (${flags.category})...`,
66
+ ),
67
+ }).start()
68
+
69
+ if (source === 'skills') {
70
+ if (!flags.query || flags.query.length < 2) {
71
+ this.error('skills.sh requires a search query (min 2 characters)', {
72
+ exit: 1,
73
+ suggestions: ['dvmi prompts browse skills --query refactor'],
74
+ })
75
+ }
76
+
77
+ /** @type {Skill[]} */
78
+ let skills
79
+ try {
80
+ skills = await searchSkills(flags.query ?? '', 50)
81
+ } catch (err) {
82
+ spinner?.fail()
83
+ if (err instanceof DvmiError) {
84
+ this.error(err.message, { exit: err.exitCode, suggestions: [err.hint] })
85
+ }
86
+ throw err
87
+ }
88
+
89
+ spinner?.stop()
90
+
91
+ if (isJson) {
92
+ return { skills, total: skills.length }
93
+ }
94
+
95
+ this.log(
96
+ chalk.bold('\nSkills') +
97
+ (flags.query ? chalk.dim(` — query: "${flags.query}"`) : '') +
98
+ chalk.dim(` (${skills.length})\n`),
99
+ )
100
+ this.log(formatSkillTable(skills))
101
+
102
+ return { skills, total: skills.length }
103
+ }
104
+
105
+ // source === 'awesome'
106
+ const category = flags.category
107
+
108
+ if (!AWESOME_CATEGORIES.includes(category)) {
109
+ this.error(`Invalid category: "${category}". Valid: ${AWESOME_CATEGORIES.join(', ')}`, {
110
+ exit: 1,
111
+ })
112
+ }
113
+
114
+ /** @type {AwesomeEntry[]} */
115
+ let entries
116
+ try {
117
+ entries = await fetchAwesomeEntries(category)
118
+ } catch (err) {
119
+ spinner?.fail()
120
+ if (err instanceof DvmiError) {
121
+ this.error(err.message, { exit: err.exitCode, suggestions: [err.hint] })
122
+ }
123
+ throw err
124
+ }
125
+
126
+ spinner?.stop()
127
+
128
+ if (isJson) {
129
+ return { entries, total: entries.length, category }
130
+ }
131
+
132
+ this.log(
133
+ chalk.bold('\nAwesome Copilot') +
134
+ chalk.dim(` — category: `) +
135
+ chalk.hex('#4A9EFF')(category) +
136
+ chalk.dim(` (${entries.length})\n`),
137
+ )
138
+ this.log(formatAwesomeTable(entries, category))
139
+ this.log('')
140
+
141
+ if (entries.length > 0) {
142
+ try {
143
+ const choices = entries.map((e) => ({ name: `${e.name} ${chalk.dim(e.url)}`, value: e }))
144
+ choices.push({ name: chalk.dim('← Exit'), value: /** @type {AwesomeEntry} */ (null) })
145
+
146
+ const selected = await select({
147
+ message: 'Select an entry to view its URL (or Exit):',
148
+ choices,
149
+ })
150
+
151
+ if (selected) {
152
+ this.log(`\n${chalk.bold(selected.name)}\n${chalk.hex('#4A9EFF')(selected.url)}\n`)
153
+ if (selected.description) {
154
+ this.log(chalk.white(selected.description) + '\n')
155
+ }
156
+ }
157
+ } catch {
158
+ // User pressed Ctrl+C — exit gracefully
159
+ }
160
+ }
161
+
162
+ return { entries, total: entries.length, category }
163
+ }
164
+ }
@@ -0,0 +1,154 @@
1
+ import { Command, Args, Flags } from '@oclif/core'
2
+ import ora from 'ora'
3
+ import chalk from 'chalk'
4
+ import { select, confirm } from '@inquirer/prompts'
5
+ import { join } from 'node:path'
6
+ import { listPrompts, downloadPrompt } from '../../services/prompts.js'
7
+ import { loadConfig } from '../../services/config.js'
8
+ import { DvmiError } from '../../utils/errors.js'
9
+
10
+ /** @import { Prompt } from '../../types.js' */
11
+
12
+ const DEFAULT_PROMPTS_DIR = '.prompts'
13
+
14
+ export default class PromptsDownload extends Command {
15
+ static description = 'Download a prompt from your personal repository to .prompts/'
16
+
17
+ static examples = [
18
+ '<%= config.bin %> prompts download',
19
+ '<%= config.bin %> prompts download coding/refactor-prompt.md',
20
+ '<%= config.bin %> prompts download coding/refactor-prompt.md --overwrite',
21
+ '<%= config.bin %> prompts download --json',
22
+ ]
23
+
24
+ static enableJsonFlag = true
25
+
26
+ static args = {
27
+ path: Args.string({
28
+ description: 'Relative path of the prompt in the repository (e.g. coding/refactor-prompt.md)',
29
+ required: false,
30
+ }),
31
+ }
32
+
33
+ static flags = {
34
+ overwrite: Flags.boolean({
35
+ description: 'Overwrite existing local file without prompting',
36
+ default: false,
37
+ }),
38
+ }
39
+
40
+ async run() {
41
+ const { args, flags } = await this.parse(PromptsDownload)
42
+ const isJson = flags.json
43
+
44
+ // Determine local prompts directory from config or default to cwd/.prompts
45
+ let config = {}
46
+ try {
47
+ config = await loadConfig()
48
+ } catch {
49
+ /* use defaults */
50
+ }
51
+ const localDir =
52
+ process.env.DVMI_PROMPTS_DIR ?? config.promptsDir ?? join(process.cwd(), DEFAULT_PROMPTS_DIR)
53
+
54
+ // Resolve path interactively if not provided (only in interactive mode)
55
+ let relativePath = args.path
56
+ if (!relativePath) {
57
+ if (isJson) {
58
+ this.error('Prompt path is required in --json mode', {
59
+ exit: 1,
60
+ suggestions: ['Run `dvmi prompts download <path> --json`'],
61
+ })
62
+ }
63
+
64
+ const spinner = ora({
65
+ spinner: 'arc',
66
+ color: false,
67
+ text: chalk.hex('#FF6B2B')('Fetching prompts...'),
68
+ }).start()
69
+
70
+ /** @type {Prompt[]} */
71
+ let prompts
72
+ try {
73
+ prompts = await listPrompts()
74
+ } catch (err) {
75
+ spinner.fail()
76
+ if (err instanceof DvmiError) {
77
+ this.error(err.message, { exit: err.exitCode, suggestions: [err.hint] })
78
+ }
79
+ throw err
80
+ }
81
+ spinner.stop()
82
+
83
+ if (prompts.length === 0) {
84
+ this.log(chalk.yellow('No prompts found in the repository.'))
85
+ return { downloaded: [], skipped: [] }
86
+ }
87
+
88
+ const choices = prompts.map((p) => ({
89
+ name: `${p.path} ${chalk.dim(p.title)}`,
90
+ value: p.path,
91
+ }))
92
+ relativePath = await select({ message: 'Select a prompt to download:', choices })
93
+ }
94
+
95
+ // Attempt download (skips automatically if file exists and --overwrite not set)
96
+ const spinner = isJson
97
+ ? null
98
+ : ora({
99
+ spinner: 'arc',
100
+ color: false,
101
+ text: chalk.hex('#FF6B2B')(`Downloading ${relativePath}...`),
102
+ }).start()
103
+
104
+ let result
105
+ try {
106
+ result = await downloadPrompt(relativePath, localDir, { overwrite: flags.overwrite })
107
+ } catch (err) {
108
+ spinner?.fail()
109
+ if (err instanceof DvmiError) {
110
+ this.error(err.message, { exit: err.exitCode, suggestions: [err.hint] })
111
+ }
112
+ throw err
113
+ }
114
+
115
+ spinner?.stop()
116
+
117
+ // Conflict: file exists and user didn't pass --overwrite → ask interactively
118
+ if (result.skipped && !flags.overwrite && !isJson) {
119
+ const shouldOverwrite = await confirm({
120
+ message: chalk.yellow(`File already exists at ${result.path}. Overwrite?`),
121
+ default: false,
122
+ })
123
+ if (shouldOverwrite) {
124
+ try {
125
+ result = await downloadPrompt(relativePath, localDir, { overwrite: true })
126
+ } catch (err) {
127
+ if (err instanceof DvmiError) {
128
+ this.error(err.message, { exit: err.exitCode, suggestions: [err.hint] })
129
+ }
130
+ throw err
131
+ }
132
+ }
133
+ }
134
+
135
+ if (isJson) {
136
+ return {
137
+ downloaded: result.skipped ? [] : [result.path],
138
+ skipped: result.skipped ? [result.path] : [],
139
+ }
140
+ }
141
+
142
+ if (result.skipped) {
143
+ this.log(chalk.dim(`Skipped (already exists): ${result.path}`))
144
+ this.log(chalk.dim(' Run with --overwrite to replace it.'))
145
+ } else {
146
+ this.log(chalk.green(`✓ Downloaded: ${result.path}`))
147
+ }
148
+
149
+ return {
150
+ downloaded: result.skipped ? [] : [result.path],
151
+ skipped: result.skipped ? [result.path] : [],
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,97 @@
1
+ import { Command, Flags } from '@oclif/core'
2
+ import ora from 'ora'
3
+ import chalk from 'chalk'
4
+ import { loadConfig } from '../../services/config.js'
5
+ import { isUvInstalled, isSpecifyInstalled, installSpecifyCli, runSpecifyInit } from '../../services/speckit.js'
6
+ import { DvmiError } from '../../utils/errors.js'
7
+
8
+ /**
9
+ * Map from dvmi's `aiTool` config values to spec-kit's `--ai` flag values.
10
+ * @type {Record<string, string>}
11
+ */
12
+ const AI_TOOL_MAP = {
13
+ opencode: 'opencode',
14
+ copilot: 'copilot',
15
+ }
16
+
17
+ export default class PromptsInstallSpeckit extends Command {
18
+ static description = 'Install spec-kit (specify-cli) and run `specify init` to set up Spec-Driven Development'
19
+
20
+ static examples = [
21
+ '<%= config.bin %> prompts install-speckit',
22
+ '<%= config.bin %> prompts install-speckit --ai opencode',
23
+ '<%= config.bin %> prompts install-speckit --force',
24
+ '<%= config.bin %> prompts install-speckit --reinstall',
25
+ ]
26
+
27
+ static flags = {
28
+ ai: Flags.string({
29
+ description:
30
+ 'AI agent to pass to `specify init --ai` (defaults to the aiTool set in `dvmi init`)',
31
+ options: ['opencode', 'copilot', 'claude', 'gemini', 'cursor-agent', 'codex', 'windsurf', 'kiro-cli', 'amp'],
32
+ }),
33
+ force: Flags.boolean({
34
+ char: 'f',
35
+ description: 'Pass --force to `specify init` (safe to run in a non-empty directory)',
36
+ default: false,
37
+ }),
38
+ reinstall: Flags.boolean({
39
+ description: 'Reinstall specify-cli even if it is already installed',
40
+ default: false,
41
+ }),
42
+ }
43
+
44
+ async run() {
45
+ const { flags } = await this.parse(PromptsInstallSpeckit)
46
+
47
+ // ── 1. Require uv ────────────────────────────────────────────────────────
48
+ if (!(await isUvInstalled())) {
49
+ this.error('uv is not installed — spec-kit requires the uv Python package manager', {
50
+ exit: 1,
51
+ suggestions: ['Install uv: https://docs.astral.sh/uv/getting-started/installation/'],
52
+ })
53
+ }
54
+
55
+ // ── 2. Install specify-cli (skip if already present unless --reinstall) ──
56
+ const alreadyInstalled = await isSpecifyInstalled()
57
+
58
+ if (!alreadyInstalled || flags.reinstall) {
59
+ const label = alreadyInstalled ? 'Reinstalling specify-cli...' : 'Installing specify-cli...'
60
+ const spinner = ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')(label) }).start()
61
+
62
+ try {
63
+ await installSpecifyCli({ force: flags.reinstall })
64
+ spinner.succeed(chalk.green('specify-cli installed'))
65
+ } catch (err) {
66
+ spinner.fail()
67
+ if (err instanceof DvmiError) {
68
+ this.error(err.message, { exit: 1, suggestions: [err.hint] })
69
+ }
70
+ throw err
71
+ }
72
+ } else {
73
+ this.log(chalk.dim('specify-cli already installed'))
74
+ }
75
+
76
+ // ── 3. Resolve --ai flag (flag > config > let specify prompt) ────────────
77
+ let aiFlag = flags.ai
78
+ if (!aiFlag) {
79
+ const config = await loadConfig()
80
+ aiFlag = config.aiTool ? (AI_TOOL_MAP[config.aiTool] ?? undefined) : undefined
81
+ }
82
+
83
+ // ── 4. Run `specify init --here` (interactive — inherits stdio) ──────────
84
+ this.log('')
85
+ this.log(chalk.bold('Running specify init — follow the prompts to set up your project:'))
86
+ this.log('')
87
+
88
+ try {
89
+ await runSpecifyInit(process.cwd(), { ai: aiFlag, force: flags.force })
90
+ } catch (err) {
91
+ if (err instanceof DvmiError) {
92
+ this.error(err.message, { exit: 1, suggestions: [err.hint] })
93
+ }
94
+ throw err
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,107 @@
1
+ import { Command, Flags } from '@oclif/core'
2
+ import ora from 'ora'
3
+ import chalk from 'chalk'
4
+ import { select } from '@inquirer/prompts'
5
+ import { listPrompts } from '../../services/prompts.js'
6
+ import { formatPromptTable, formatPromptBody } from '../../formatters/prompts.js'
7
+ import { DvmiError } from '../../utils/errors.js'
8
+
9
+ /** @import { Prompt } from '../../types.js' */
10
+
11
+ export default class PromptsList extends Command {
12
+ static description = 'List prompts from your personal prompt repository'
13
+
14
+ static examples = [
15
+ '<%= config.bin %> prompts list',
16
+ '<%= config.bin %> prompts list --filter refactor',
17
+ '<%= config.bin %> prompts list --json',
18
+ ]
19
+
20
+ static enableJsonFlag = true
21
+
22
+ static flags = {
23
+ filter: Flags.string({
24
+ char: 'f',
25
+ description: 'Filter prompts by title, category, description, or tag (case-insensitive)',
26
+ }),
27
+ }
28
+
29
+ async run() {
30
+ const { flags } = await this.parse(PromptsList)
31
+ const isJson = flags.json
32
+
33
+ const spinner = isJson
34
+ ? null
35
+ : ora({
36
+ spinner: 'arc',
37
+ color: false,
38
+ text: chalk.hex('#FF6B2B')('Fetching prompts...'),
39
+ }).start()
40
+
41
+ /** @type {Prompt[]} */
42
+ let prompts
43
+ try {
44
+ prompts = await listPrompts()
45
+ } catch (err) {
46
+ spinner?.fail()
47
+ if (err instanceof DvmiError) {
48
+ this.error(err.message, { exit: err.exitCode, suggestions: [err.hint] })
49
+ }
50
+ throw err
51
+ }
52
+
53
+ spinner?.stop()
54
+
55
+ // Apply filter
56
+ const query = flags.filter?.toLowerCase()
57
+ const filtered = query
58
+ ? prompts.filter(
59
+ (p) =>
60
+ p.title.toLowerCase().includes(query) ||
61
+ p.category?.toLowerCase().includes(query) ||
62
+ p.description?.toLowerCase().includes(query) ||
63
+ p.tags?.some((t) => t.toLowerCase().includes(query)),
64
+ )
65
+ : prompts
66
+
67
+ if (isJson) {
68
+ return { prompts: filtered, total: filtered.length }
69
+ }
70
+
71
+ if (filtered.length === 0) {
72
+ const msg = query
73
+ ? chalk.dim(`No prompts matching "${flags.filter}".`)
74
+ : chalk.yellow('No prompts found in the repository.')
75
+ this.log(msg)
76
+ return { prompts: [], total: 0 }
77
+ }
78
+
79
+ const filterInfo = query ? chalk.dim(` — filter: ${chalk.white(`"${flags.filter}"`)}`) : ''
80
+ this.log(
81
+ chalk.bold(`\nPrompts`) +
82
+ filterInfo +
83
+ chalk.dim(` (${filtered.length}${filtered.length < prompts.length ? `/${prompts.length}` : ''})\n`),
84
+ )
85
+ this.log(formatPromptTable(filtered))
86
+ this.log('')
87
+
88
+ // Interactive selection to view full prompt content
89
+ try {
90
+ const choices = filtered.map((p) => ({ name: p.title, value: p }))
91
+ choices.push({ name: chalk.dim('← Exit'), value: /** @type {Prompt} */ (null) })
92
+
93
+ const selected = await select({
94
+ message: 'Select a prompt to view its content (or Exit):',
95
+ choices,
96
+ })
97
+
98
+ if (selected) {
99
+ this.log('\n' + formatPromptBody(selected) + '\n')
100
+ }
101
+ } catch {
102
+ // User pressed Ctrl+C — exit gracefully
103
+ }
104
+
105
+ return { prompts: filtered, total: filtered.length }
106
+ }
107
+ }