devvami 1.4.2 → 1.5.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.
Files changed (96) hide show
  1. package/README.md +72 -0
  2. package/oclif.manifest.json +275 -235
  3. package/package.json +2 -1
  4. package/src/commands/auth/login.js +20 -16
  5. package/src/commands/changelog.js +12 -12
  6. package/src/commands/costs/get.js +14 -24
  7. package/src/commands/costs/trend.js +13 -24
  8. package/src/commands/create/repo.js +72 -54
  9. package/src/commands/docs/list.js +29 -25
  10. package/src/commands/docs/projects.js +58 -24
  11. package/src/commands/docs/read.js +56 -39
  12. package/src/commands/docs/search.js +37 -25
  13. package/src/commands/doctor.js +37 -35
  14. package/src/commands/dotfiles/add.js +51 -39
  15. package/src/commands/dotfiles/setup.js +62 -33
  16. package/src/commands/dotfiles/status.js +18 -18
  17. package/src/commands/dotfiles/sync.js +62 -46
  18. package/src/commands/init.js +143 -132
  19. package/src/commands/logs/index.js +10 -16
  20. package/src/commands/open.js +12 -12
  21. package/src/commands/pipeline/logs.js +8 -11
  22. package/src/commands/pipeline/rerun.js +21 -16
  23. package/src/commands/pipeline/status.js +28 -24
  24. package/src/commands/pr/create.js +40 -27
  25. package/src/commands/pr/detail.js +9 -7
  26. package/src/commands/pr/review.js +18 -19
  27. package/src/commands/pr/status.js +27 -21
  28. package/src/commands/prompts/browse.js +15 -15
  29. package/src/commands/prompts/download.js +15 -16
  30. package/src/commands/prompts/install-speckit.js +11 -12
  31. package/src/commands/prompts/list.js +12 -12
  32. package/src/commands/prompts/run.js +16 -19
  33. package/src/commands/repo/list.js +57 -41
  34. package/src/commands/search.js +20 -18
  35. package/src/commands/security/setup.js +38 -34
  36. package/src/commands/sync-config-ai/index.js +257 -0
  37. package/src/commands/tasks/assigned.js +43 -33
  38. package/src/commands/tasks/list.js +43 -33
  39. package/src/commands/tasks/today.js +32 -30
  40. package/src/commands/upgrade.js +18 -17
  41. package/src/commands/vuln/detail.js +8 -8
  42. package/src/commands/vuln/scan.js +39 -20
  43. package/src/commands/vuln/search.js +23 -18
  44. package/src/commands/welcome.js +2 -2
  45. package/src/commands/whoami.js +19 -23
  46. package/src/formatters/ai-config.js +215 -0
  47. package/src/formatters/charts.js +6 -23
  48. package/src/formatters/cost.js +1 -7
  49. package/src/formatters/dotfiles.js +48 -19
  50. package/src/formatters/markdown.js +11 -6
  51. package/src/formatters/openapi.js +7 -9
  52. package/src/formatters/prompts.js +69 -78
  53. package/src/formatters/security.js +2 -2
  54. package/src/formatters/status.js +1 -1
  55. package/src/formatters/table.js +1 -3
  56. package/src/formatters/vuln.js +33 -20
  57. package/src/help.js +162 -164
  58. package/src/hooks/init.js +1 -3
  59. package/src/hooks/postrun.js +5 -7
  60. package/src/index.js +1 -1
  61. package/src/services/ai-config-store.js +349 -0
  62. package/src/services/ai-env-deployer.js +650 -0
  63. package/src/services/ai-env-scanner.js +983 -0
  64. package/src/services/audit-detector.js +2 -2
  65. package/src/services/audit-runner.js +40 -31
  66. package/src/services/auth.js +9 -9
  67. package/src/services/awesome-copilot.js +7 -4
  68. package/src/services/aws-costs.js +22 -22
  69. package/src/services/clickup.js +26 -26
  70. package/src/services/cloudwatch-logs.js +5 -9
  71. package/src/services/config.js +13 -13
  72. package/src/services/docs.js +19 -20
  73. package/src/services/dotfiles.js +149 -51
  74. package/src/services/github.js +22 -24
  75. package/src/services/nvd.js +21 -31
  76. package/src/services/platform.js +2 -2
  77. package/src/services/prompts.js +23 -35
  78. package/src/services/security.js +135 -61
  79. package/src/services/shell.js +4 -4
  80. package/src/services/skills-sh.js +3 -9
  81. package/src/services/speckit.js +4 -7
  82. package/src/services/version-check.js +10 -10
  83. package/src/types.js +117 -0
  84. package/src/utils/aws-vault.js +18 -41
  85. package/src/utils/banner.js +5 -7
  86. package/src/utils/errors.js +42 -46
  87. package/src/utils/frontmatter.js +4 -4
  88. package/src/utils/gradient.js +18 -16
  89. package/src/utils/open-browser.js +3 -3
  90. package/src/utils/tui/form.js +1184 -0
  91. package/src/utils/tui/modal.js +15 -14
  92. package/src/utils/tui/navigable-table.js +16 -16
  93. package/src/utils/tui/tab-tui.js +1089 -0
  94. package/src/utils/typewriter.js +3 -3
  95. package/src/utils/welcome.js +18 -21
  96. package/src/validators/repo-name.js +2 -2
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "devvami",
3
3
  "description": "DevEx CLI for developers and teams — manage repos, PRs, pipelines, tasks, and costs from the terminal",
4
- "version": "1.4.2",
4
+ "version": "1.5.1",
5
5
  "author": "",
6
6
  "type": "module",
7
7
  "bin": {
@@ -139,6 +139,7 @@
139
139
  "test:integration": "vitest run --project integration",
140
140
  "test:watch": "vitest",
141
141
  "test:coverage": "vitest run --coverage",
142
+ "pretest": "oclif manifest",
142
143
  "prepack": "oclif manifest",
143
144
  "postpack": "shx rm -f oclif.manifest.json",
144
145
  "prepare": "lefthook install"
@@ -1,8 +1,8 @@
1
- import { Command, Flags } from '@oclif/core'
1
+ import {Command, Flags} from '@oclif/core'
2
2
  import chalk from 'chalk'
3
3
  import ora from 'ora'
4
- import { checkGitHubAuth, loginGitHub, checkAWSAuth, loginAWS } from '../../services/auth.js'
5
- import { loadConfig } from '../../services/config.js'
4
+ import {checkGitHubAuth, loginGitHub, checkAWSAuth, loginAWS} from '../../services/auth.js'
5
+ import {loadConfig} from '../../services/config.js'
6
6
 
7
7
  export default class AuthLogin extends Command {
8
8
  static description = 'Autenticazione centralizzata GitHub + AWS'
@@ -16,36 +16,38 @@ export default class AuthLogin extends Command {
16
16
  static enableJsonFlag = true
17
17
 
18
18
  static flags = {
19
- github: Flags.boolean({ description: 'Solo autenticazione GitHub', default: false }),
20
- aws: Flags.boolean({ description: 'Solo autenticazione AWS', default: false }),
21
- verbose: Flags.boolean({ description: 'Output dettagliato', default: false }),
19
+ github: Flags.boolean({description: 'Solo autenticazione GitHub', default: false}),
20
+ aws: Flags.boolean({description: 'Solo autenticazione AWS', default: false}),
21
+ verbose: Flags.boolean({description: 'Output dettagliato', default: false}),
22
22
  }
23
23
 
24
24
  async run() {
25
- const { flags } = await this.parse(AuthLogin)
25
+ const {flags} = await this.parse(AuthLogin)
26
26
  const isJson = flags.json
27
27
  const doGitHub = !flags.aws || flags.github
28
28
  const doAWS = !flags.github || flags.aws
29
29
 
30
- const result = { github: null, aws: null }
30
+ const result = {github: null, aws: null}
31
31
 
32
32
  // GitHub auth
33
33
  if (doGitHub) {
34
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking GitHub auth...') }).start()
34
+ const spinner = isJson
35
+ ? null
36
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking GitHub auth...')}).start()
35
37
  let ghStatus = await checkGitHubAuth()
36
38
 
37
39
  if (ghStatus.authenticated) {
38
40
  spinner?.succeed(`GitHub: already authenticated as @${ghStatus.username}`)
39
- result.github = { status: 'ok', username: ghStatus.username, org: '' }
41
+ result.github = {status: 'ok', username: ghStatus.username, org: ''}
40
42
  } else {
41
43
  if (spinner) spinner.text = 'Logging in to GitHub...'
42
44
  ghStatus = await loginGitHub()
43
45
  if (ghStatus.authenticated) {
44
46
  spinner?.succeed(`GitHub: authenticated as @${ghStatus.username}`)
45
- result.github = { status: 'ok', username: ghStatus.username, org: '' }
47
+ result.github = {status: 'ok', username: ghStatus.username, org: ''}
46
48
  } else {
47
49
  spinner?.fail('GitHub authentication failed')
48
- result.github = { status: 'error', error: ghStatus.error }
50
+ result.github = {status: 'error', error: ghStatus.error}
49
51
  }
50
52
  }
51
53
  }
@@ -53,21 +55,23 @@ export default class AuthLogin extends Command {
53
55
  // AWS auth
54
56
  if (doAWS) {
55
57
  const config = await loadConfig()
56
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking AWS auth...') }).start()
58
+ const spinner = isJson
59
+ ? null
60
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking AWS auth...')}).start()
57
61
  let awsStatus = await checkAWSAuth()
58
62
 
59
63
  if (awsStatus.authenticated) {
60
64
  spinner?.succeed(`AWS: session active for account ${awsStatus.account}`)
61
- result.aws = { status: 'ok', account: awsStatus.account, role: awsStatus.role }
65
+ result.aws = {status: 'ok', account: awsStatus.account, role: awsStatus.role}
62
66
  } else {
63
67
  if (spinner) spinner.text = 'Logging in to AWS via aws-vault...'
64
68
  awsStatus = await loginAWS(config.awsProfile || 'default')
65
69
  if (awsStatus.authenticated) {
66
70
  spinner?.succeed(`AWS: logged in to account ${awsStatus.account}`)
67
- result.aws = { status: 'ok', account: awsStatus.account, role: awsStatus.role }
71
+ result.aws = {status: 'ok', account: awsStatus.account, role: awsStatus.role}
68
72
  } else {
69
73
  spinner?.fail('AWS authentication failed')
70
- result.aws = { status: 'error', error: awsStatus.error }
74
+ result.aws = {status: 'error', error: awsStatus.error}
71
75
  }
72
76
  }
73
77
  }
@@ -1,6 +1,6 @@
1
- import { Command, Flags } from '@oclif/core'
2
- import { writeFile } from 'node:fs/promises'
3
- import { exec } from '../services/shell.js'
1
+ import {Command, Flags} from '@oclif/core'
2
+ import {writeFile} from 'node:fs/promises'
3
+ import {exec} from '../services/shell.js'
4
4
 
5
5
  /**
6
6
  * Parse a conventional commit message.
@@ -10,7 +10,7 @@ import { exec } from '../services/shell.js'
10
10
  function parseConventionalCommit(message) {
11
11
  const match = message.match(/^(\w+)(?:\(([^)]+)\))?!?: (.+)/)
12
12
  if (!match) return null
13
- return { type: match[1], scope: match[2] ?? '', description: match[3] }
13
+ return {type: match[1], scope: match[2] ?? '', description: match[3]}
14
14
  }
15
15
 
16
16
  export default class Changelog extends Command {
@@ -25,13 +25,13 @@ export default class Changelog extends Command {
25
25
  static enableJsonFlag = true
26
26
 
27
27
  static flags = {
28
- from: Flags.string({ description: 'Tag o commit di partenza (default: ultimo tag)' }),
29
- to: Flags.string({ description: 'Commit finale (default: HEAD)', default: 'HEAD' }),
30
- output: Flags.string({ description: 'Scrivi su file (default: stdout)' }),
28
+ from: Flags.string({description: 'Tag o commit di partenza (default: ultimo tag)'}),
29
+ to: Flags.string({description: 'Commit finale (default: HEAD)', default: 'HEAD'}),
30
+ output: Flags.string({description: 'Scrivi su file (default: stdout)'}),
31
31
  }
32
32
 
33
33
  async run() {
34
- const { flags } = await this.parse(Changelog)
34
+ const {flags} = await this.parse(Changelog)
35
35
  const isJson = flags.json
36
36
 
37
37
  // Determine from ref
@@ -51,13 +51,13 @@ export default class Changelog extends Command {
51
51
  const lines = logResult.stdout.split('\n').filter(Boolean)
52
52
 
53
53
  /** @type {Record<string, Array<{ message: string, hash: string }>>} */
54
- const sections = { feat: [], fix: [], chore: [], docs: [], refactor: [], test: [], other: [] }
54
+ const sections = {feat: [], fix: [], chore: [], docs: [], refactor: [], test: [], other: []}
55
55
 
56
56
  for (const line of lines) {
57
57
  const [message, hash] = line.split('|')
58
58
  const parsed = parseConventionalCommit(message)
59
59
  const type = parsed?.type ?? 'other'
60
- const entry = { message: message.trim(), hash: hash?.slice(0, 7) ?? '' }
60
+ const entry = {message: message.trim(), hash: hash?.slice(0, 7) ?? ''}
61
61
  if (type in sections) {
62
62
  sections[type].push(entry)
63
63
  } else {
@@ -65,7 +65,7 @@ export default class Changelog extends Command {
65
65
  }
66
66
  }
67
67
 
68
- if (isJson) return { from: from || 'beginning', to: flags.to, sections }
68
+ if (isJson) return {from: from || 'beginning', to: flags.to, sections}
69
69
 
70
70
  // Build markdown
71
71
  const title = `## [Unreleased]${from ? ` (since ${from})` : ''}`
@@ -97,6 +97,6 @@ export default class Changelog extends Command {
97
97
  this.log(output)
98
98
  }
99
99
 
100
- return { from: from || 'beginning', to: flags.to, sections }
100
+ return {from: from || 'beginning', to: flags.to, sections}
101
101
  }
102
102
  }
@@ -1,10 +1,10 @@
1
- import { Command, Args, Flags } from '@oclif/core'
2
- import { input } from '@inquirer/prompts'
1
+ import {Command, Args, Flags} from '@oclif/core'
2
+ import {input} from '@inquirer/prompts'
3
3
  import ora from 'ora'
4
- import { getServiceCosts } from '../../services/aws-costs.js'
5
- import { loadConfig } from '../../services/config.js'
6
- import { formatCostTable, calculateTotal } from '../../formatters/cost.js'
7
- import { DvmiError } from '../../utils/errors.js'
4
+ import {getServiceCosts} from '../../services/aws-costs.js'
5
+ import {loadConfig} from '../../services/config.js'
6
+ import {formatCostTable, calculateTotal} from '../../formatters/cost.js'
7
+ import {DvmiError} from '../../utils/errors.js'
8
8
  import {
9
9
  awsVaultPrefix,
10
10
  isAwsVaultSession,
@@ -28,7 +28,7 @@ export default class CostsGet extends Command {
28
28
  static enableJsonFlag = true
29
29
 
30
30
  static args = {
31
- service: Args.string({ description: 'Service name (used to derive tag filter from config)', required: false }),
31
+ service: Args.string({description: 'Service name (used to derive tag filter from config)', required: false}),
32
32
  }
33
33
 
34
34
  static flags = {
@@ -48,18 +48,14 @@ export default class CostsGet extends Command {
48
48
  }
49
49
 
50
50
  async run() {
51
- const { args, flags } = await this.parse(CostsGet)
51
+ const {args, flags} = await this.parse(CostsGet)
52
52
  const isJson = flags.json
53
53
  const isInteractive = !isJson && process.stdout.isTTY && process.env.CI !== 'true'
54
54
  const groupBy = /** @type {'service'|'tag'|'both'} */ (flags['group-by'])
55
55
 
56
56
  const config = await loadConfig()
57
57
 
58
- if (
59
- isInteractive &&
60
- !isAwsVaultSession() &&
61
- process.env.DVMI_AWS_VAULT_REEXEC !== '1'
62
- ) {
58
+ if (isInteractive && !isAwsVaultSession() && process.env.DVMI_AWS_VAULT_REEXEC !== '1') {
63
59
  const profile = await input({
64
60
  message: 'AWS profile (aws-vault):',
65
61
  default: config.awsProfile || process.env.AWS_VAULT || 'default',
@@ -91,19 +87,16 @@ export default class CostsGet extends Command {
91
87
 
92
88
  // Validate: tag key required when grouping by tag or both
93
89
  if ((groupBy === 'tag' || groupBy === 'both') && !tagKey) {
94
- throw new DvmiError(
95
- 'No tag key available.',
96
- 'Pass --tag-key or configure projectTags in dvmi config.',
97
- )
90
+ throw new DvmiError('No tag key available.', 'Pass --tag-key or configure projectTags in dvmi config.')
98
91
  }
99
92
 
100
93
  const serviceArg = args.service ?? 'all'
101
- const tags = config.projectTags ?? (args.service ? { project: args.service } : {})
94
+ const tags = config.projectTags ?? (args.service ? {project: args.service} : {})
102
95
 
103
96
  const spinner = isJson ? null : ora(`Fetching costs...`).start()
104
97
 
105
98
  try {
106
- const { entries, period } = await getServiceCosts(
99
+ const {entries, period} = await getServiceCosts(
107
100
  serviceArg,
108
101
  tags,
109
102
  /** @type {any} */ (flags.period),
@@ -119,7 +112,7 @@ export default class CostsGet extends Command {
119
112
  tagKey: tagKey ?? null,
120
113
  period,
121
114
  items: entries,
122
- total: { amount: total, unit: 'USD' },
115
+ total: {amount: total, unit: 'USD'},
123
116
  }
124
117
 
125
118
  if (isJson) return result
@@ -156,10 +149,7 @@ export default class CostsGet extends Command {
156
149
  }
157
150
 
158
151
  const prefix = awsVaultPrefix(config)
159
- this.error(
160
- `No AWS credentials. Use: ${prefix}dvmi costs get` +
161
- (args.service ? ` ${args.service}` : ''),
162
- )
152
+ this.error(`No AWS credentials. Use: ${prefix}dvmi costs get` + (args.service ? ` ${args.service}` : ''))
163
153
  }
164
154
  throw err
165
155
  }
@@ -1,10 +1,10 @@
1
- import { Command, Flags } from '@oclif/core'
2
- import { input } from '@inquirer/prompts'
1
+ import {Command, Flags} from '@oclif/core'
2
+ import {input} from '@inquirer/prompts'
3
3
  import ora from 'ora'
4
- import { getTrendCosts, getTwoMonthPeriod } from '../../services/aws-costs.js'
5
- import { loadConfig } from '../../services/config.js'
6
- import { barChart, lineChart } from '../../formatters/charts.js'
7
- import { DvmiError } from '../../utils/errors.js'
4
+ import {getTrendCosts, getTwoMonthPeriod} from '../../services/aws-costs.js'
5
+ import {loadConfig} from '../../services/config.js'
6
+ import {barChart, lineChart} from '../../formatters/charts.js'
7
+ import {DvmiError} from '../../utils/errors.js'
8
8
  import {
9
9
  awsVaultPrefix,
10
10
  isAwsVaultSession,
@@ -42,18 +42,14 @@ export default class CostsTrend extends Command {
42
42
  }
43
43
 
44
44
  async run() {
45
- const { flags } = await this.parse(CostsTrend)
45
+ const {flags} = await this.parse(CostsTrend)
46
46
  const isJson = flags.json
47
47
  const isInteractive = !isJson && process.stdout.isTTY && process.env.CI !== 'true'
48
48
  const groupBy = /** @type {'service'|'tag'|'both'} */ (flags['group-by'])
49
49
 
50
50
  const config = await loadConfig()
51
51
 
52
- if (
53
- isInteractive &&
54
- !isAwsVaultSession() &&
55
- process.env.DVMI_AWS_VAULT_REEXEC !== '1'
56
- ) {
52
+ if (isInteractive && !isAwsVaultSession() && process.env.DVMI_AWS_VAULT_REEXEC !== '1') {
57
53
  const profile = await input({
58
54
  message: 'AWS profile (aws-vault):',
59
55
  default: config.awsProfile || process.env.AWS_VAULT || 'default',
@@ -83,10 +79,7 @@ export default class CostsTrend extends Command {
83
79
  const tagKey = flags['tag-key'] ?? configTagKey
84
80
 
85
81
  if ((groupBy === 'tag' || groupBy === 'both') && !tagKey) {
86
- throw new DvmiError(
87
- 'No tag key available.',
88
- 'Pass --tag-key or configure projectTags in dvmi config.',
89
- )
82
+ throw new DvmiError('No tag key available.', 'Pass --tag-key or configure projectTags in dvmi config.')
90
83
  }
91
84
 
92
85
  const spinner = isJson ? null : ora('Fetching cost trend data...').start()
@@ -95,13 +88,13 @@ export default class CostsTrend extends Command {
95
88
  const trendSeries = await getTrendCosts(groupBy, tagKey)
96
89
  spinner?.stop()
97
90
 
98
- const { start, end } = getTwoMonthPeriod()
91
+ const {start, end} = getTwoMonthPeriod()
99
92
 
100
93
  if (isJson) {
101
94
  return {
102
95
  groupBy,
103
96
  tagKey: tagKey ?? null,
104
- period: { start, end },
97
+ period: {start, end},
105
98
  series: trendSeries,
106
99
  }
107
100
  }
@@ -113,9 +106,7 @@ export default class CostsTrend extends Command {
113
106
 
114
107
  // Convert CostTrendSeries[] → ChartSeries[]
115
108
  // All series must share the same label (date) axis — use the union of all dates
116
- const allDates = Array.from(
117
- new Set(trendSeries.flatMap((s) => s.points.map((p) => p.date))),
118
- ).sort()
109
+ const allDates = Array.from(new Set(trendSeries.flatMap((s) => s.points.map((p) => p.date)))).sort()
119
110
 
120
111
  /** @type {import('../../formatters/charts.js').ChartSeries[]} */
121
112
  const chartSeries = trendSeries.map((s) => {
@@ -128,9 +119,7 @@ export default class CostsTrend extends Command {
128
119
  })
129
120
 
130
121
  const title = `AWS Cost Trend — last 2 months (${start} → ${end})`
131
- const rendered = flags.line
132
- ? lineChart(chartSeries, { title })
133
- : barChart(chartSeries, { title })
122
+ const rendered = flags.line ? lineChart(chartSeries, {title}) : barChart(chartSeries, {title})
134
123
 
135
124
  this.log(rendered)
136
125
  } catch (err) {
@@ -1,12 +1,12 @@
1
- import { Command, Args, Flags } from '@oclif/core'
1
+ import {Command, Args, Flags} from '@oclif/core'
2
2
  import chalk from 'chalk'
3
3
  import ora from 'ora'
4
- import { input, confirm } from '@inquirer/prompts'
5
- import { listTemplates, createFromTemplate, setBranchProtection, enableDependabot } from '../../services/github.js'
6
- import { loadConfig } from '../../services/config.js'
7
- import { validateRepoName } from '../../validators/repo-name.js'
8
- import { renderTable } from '../../formatters/table.js'
9
- import { exec } from '../../services/shell.js'
4
+ import {input, confirm} from '@inquirer/prompts'
5
+ import {listTemplates, createFromTemplate, setBranchProtection, enableDependabot} from '../../services/github.js'
6
+ import {loadConfig} from '../../services/config.js'
7
+ import {validateRepoName} from '../../validators/repo-name.js'
8
+ import {renderTable} from '../../formatters/table.js'
9
+ import {exec} from '../../services/shell.js'
10
10
 
11
11
  /**
12
12
  * @param {string} lang
@@ -16,15 +16,15 @@ function langColor(lang) {
16
16
  const map = {
17
17
  javascript: chalk.yellow,
18
18
  typescript: chalk.blue,
19
- python: chalk.green,
20
- java: chalk.red,
21
- go: chalk.cyan,
22
- ruby: chalk.magenta,
23
- rust: chalk.hex('#CE422B'),
24
- kotlin: chalk.hex('#7F52FF'),
25
- swift: chalk.hex('#F05138'),
26
- php: chalk.hex('#777BB4'),
27
- shell: chalk.greenBright,
19
+ python: chalk.green,
20
+ java: chalk.red,
21
+ go: chalk.cyan,
22
+ ruby: chalk.magenta,
23
+ rust: chalk.hex('#CE422B'),
24
+ kotlin: chalk.hex('#7F52FF'),
25
+ swift: chalk.hex('#F05138'),
26
+ php: chalk.hex('#777BB4'),
27
+ shell: chalk.greenBright,
28
28
  }
29
29
  const fn = map[lang.toLowerCase()]
30
30
  return fn ? fn(lang) : chalk.dim(lang)
@@ -43,21 +43,21 @@ export default class CreateRepo extends Command {
43
43
  static enableJsonFlag = true
44
44
 
45
45
  static args = {
46
- template: Args.string({ description: 'Nome del template', required: false }),
46
+ template: Args.string({description: 'Nome del template', required: false}),
47
47
  }
48
48
 
49
49
  static flags = {
50
- list: Flags.boolean({ description: 'Lista template disponibili', default: false }),
51
- search: Flags.string({ char: 's', description: 'Cerca in nome e descrizione dei template (case-insensitive)' }),
52
- name: Flags.string({ description: 'Nome del nuovo repository' }),
53
- description: Flags.string({ description: 'Descrizione del repository', default: '' }),
54
- private: Flags.boolean({ description: 'Repository privato (default)', default: true }),
55
- public: Flags.boolean({ description: 'Repository pubblico', default: false }),
56
- 'dry-run': Flags.boolean({ description: 'Preview senza eseguire', default: false }),
50
+ list: Flags.boolean({description: 'Lista template disponibili', default: false}),
51
+ search: Flags.string({char: 's', description: 'Cerca in nome e descrizione dei template (case-insensitive)'}),
52
+ name: Flags.string({description: 'Nome del nuovo repository'}),
53
+ description: Flags.string({description: 'Descrizione del repository', default: ''}),
54
+ private: Flags.boolean({description: 'Repository privato (default)', default: true}),
55
+ public: Flags.boolean({description: 'Repository pubblico', default: false}),
56
+ 'dry-run': Flags.boolean({description: 'Preview senza eseguire', default: false}),
57
57
  }
58
58
 
59
59
  async run() {
60
- const { args, flags } = await this.parse(CreateRepo)
60
+ const {args, flags} = await this.parse(CreateRepo)
61
61
  const isJson = flags.json
62
62
  const isDryRun = flags['dry-run']
63
63
  const config = await loadConfig()
@@ -68,51 +68,58 @@ export default class CreateRepo extends Command {
68
68
 
69
69
  // --list mode
70
70
  if (flags.list || !args.template) {
71
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching templates...') }).start()
71
+ const spinner = isJson
72
+ ? null
73
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching templates...')}).start()
72
74
  const templates = await listTemplates(config.org)
73
75
  spinner?.stop()
74
76
 
75
77
  // Search filter
76
78
  const searchQuery = flags.search?.toLowerCase()
77
79
  const filtered = searchQuery
78
- ? templates.filter((t) =>
79
- t.name.toLowerCase().includes(searchQuery) ||
80
- t.description.toLowerCase().includes(searchQuery),
80
+ ? templates.filter(
81
+ (t) => t.name.toLowerCase().includes(searchQuery) || t.description.toLowerCase().includes(searchQuery),
81
82
  )
82
83
  : templates
83
84
 
84
- if (isJson) return { templates: filtered }
85
+ if (isJson) return {templates: filtered}
85
86
 
86
87
  if (templates.length === 0) {
87
88
  this.log(chalk.yellow('No templates found in the organization.'))
88
89
  this.log(chalk.dim('Templates are GitHub repos marked as "Template repository".'))
89
- return { templates: [] }
90
+ return {templates: []}
90
91
  }
91
92
 
92
93
  if (filtered.length === 0) {
93
94
  this.log(chalk.dim(`No templates matching "${flags.search}".`))
94
- return { templates: [] }
95
+ return {templates: []}
95
96
  }
96
97
 
97
- const filterInfo = flags.search
98
- ? chalk.dim(' — search: ') + chalk.white(`"${flags.search}"`)
99
- : ''
98
+ const filterInfo = flags.search ? chalk.dim(' — search: ') + chalk.white(`"${flags.search}"`) : ''
100
99
 
101
100
  this.log(
102
101
  chalk.bold('\nAvailable templates') +
103
- filterInfo +
104
- chalk.dim(` (${filtered.length}${filtered.length < templates.length ? `/${templates.length}` : ''})`) +
105
- '\n',
102
+ filterInfo +
103
+ chalk.dim(` (${filtered.length}${filtered.length < templates.length ? `/${templates.length}` : ''})`) +
104
+ '\n',
106
105
  )
107
106
 
108
- this.log(renderTable(filtered, [
109
- { header: 'Name', key: 'name', width: 35 },
110
- { header: 'Language', key: 'language', width: 14, format: (v) => v || '—', colorize: (v) => v === '—' ? chalk.dim(v) : langColor(v) },
111
- { header: 'Description', key: 'description', width: 60, format: (v) => String(v || '—') },
112
- ]))
107
+ this.log(
108
+ renderTable(filtered, [
109
+ {header: 'Name', key: 'name', width: 35},
110
+ {
111
+ header: 'Language',
112
+ key: 'language',
113
+ width: 14,
114
+ format: (v) => v || '—',
115
+ colorize: (v) => (v === '—' ? chalk.dim(v) : langColor(v)),
116
+ },
117
+ {header: 'Description', key: 'description', width: 60, format: (v) => String(v || '—')},
118
+ ]),
119
+ )
113
120
 
114
121
  this.log('')
115
- return { templates: filtered }
122
+ return {templates: filtered}
116
123
  }
117
124
 
118
125
  // Create mode
@@ -126,7 +133,7 @@ export default class CreateRepo extends Command {
126
133
  // Get repo name
127
134
  let repoName = flags.name
128
135
  if (!repoName && !isJson) {
129
- repoName = await input({ message: 'Repository name:' })
136
+ repoName = await input({message: 'Repository name:'})
130
137
  } else if (!repoName) {
131
138
  this.error('--name is required in non-interactive mode')
132
139
  }
@@ -142,13 +149,16 @@ export default class CreateRepo extends Command {
142
149
  const ok = await confirm({
143
150
  message: `Create ${isPrivate ? 'private' : 'public'} repo "${config.org}/${repoName}" from "${args.template}"?`,
144
151
  })
145
- if (!ok) { this.log('Aborted.'); return }
152
+ if (!ok) {
153
+ this.log('Aborted.')
154
+ return
155
+ }
146
156
  }
147
157
 
148
158
  if (isDryRun) {
149
159
  const preview = {
150
- repository: { name: repoName, org: config.org, template: args.template, private: isPrivate },
151
- postScaffolding: { branchProtection: 'would configure', dependabot: 'would enable', codeowners: 'would create' },
160
+ repository: {name: repoName, org: config.org, template: args.template, private: isPrivate},
161
+ postScaffolding: {branchProtection: 'would configure', dependabot: 'would enable', codeowners: 'would create'},
152
162
  }
153
163
  if (isJson) return preview
154
164
  this.log(chalk.bold('\nDry run preview:'))
@@ -157,7 +167,9 @@ export default class CreateRepo extends Command {
157
167
  }
158
168
 
159
169
  // Create repo
160
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Creating repository...') }).start()
170
+ const spinner = isJson
171
+ ? null
172
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Creating repository...')}).start()
161
173
  const repo = await createFromTemplate({
162
174
  templateOwner: config.org,
163
175
  templateRepo: args.template,
@@ -169,22 +181,28 @@ export default class CreateRepo extends Command {
169
181
  spinner?.succeed(`Repository created: ${repo.htmlUrl}`)
170
182
 
171
183
  // Post-scaffolding
172
- const bpSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Configuring branch protection...') }).start()
184
+ const bpSpinner = isJson
185
+ ? null
186
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Configuring branch protection...')}).start()
173
187
  await setBranchProtection(config.org, repoName).catch(() => null)
174
188
  bpSpinner?.succeed('Branch protection configured')
175
189
 
176
- const depSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Enabling Dependabot...') }).start()
190
+ const depSpinner = isJson
191
+ ? null
192
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Enabling Dependabot...')}).start()
177
193
  await enableDependabot(config.org, repoName).catch(() => null)
178
194
  depSpinner?.succeed('Dependabot enabled')
179
195
 
180
196
  // Clone
181
- const cloneSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Cloning repository...') }).start()
197
+ const cloneSpinner = isJson
198
+ ? null
199
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Cloning repository...')}).start()
182
200
  await exec('gh', ['repo', 'clone', `${config.org}/${repoName}`])
183
201
  cloneSpinner?.succeed(`Cloned to ./${repoName}`)
184
202
 
185
203
  const result = {
186
- repository: { name: repoName, url: repo.htmlUrl, localPath: `./${repoName}` },
187
- postScaffolding: { branchProtection: 'ok', dependabot: 'ok', codeowners: 'ok' },
204
+ repository: {name: repoName, url: repo.htmlUrl, localPath: `./${repoName}`},
205
+ postScaffolding: {branchProtection: 'ok', dependabot: 'ok', codeowners: 'ok'},
188
206
  }
189
207
 
190
208
  if (!isJson) {