devvami 1.0.0

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +255 -0
  3. package/bin/dev.cmd +3 -0
  4. package/bin/dev.js +5 -0
  5. package/bin/run.cmd +3 -0
  6. package/bin/run.js +5 -0
  7. package/oclif.manifest.json +1238 -0
  8. package/package.json +161 -0
  9. package/src/commands/auth/login.js +89 -0
  10. package/src/commands/changelog.js +102 -0
  11. package/src/commands/costs/get.js +73 -0
  12. package/src/commands/create/repo.js +196 -0
  13. package/src/commands/docs/list.js +110 -0
  14. package/src/commands/docs/projects.js +92 -0
  15. package/src/commands/docs/read.js +172 -0
  16. package/src/commands/docs/search.js +103 -0
  17. package/src/commands/doctor.js +115 -0
  18. package/src/commands/init.js +222 -0
  19. package/src/commands/open.js +75 -0
  20. package/src/commands/pipeline/logs.js +41 -0
  21. package/src/commands/pipeline/rerun.js +66 -0
  22. package/src/commands/pipeline/status.js +62 -0
  23. package/src/commands/pr/create.js +114 -0
  24. package/src/commands/pr/detail.js +83 -0
  25. package/src/commands/pr/review.js +51 -0
  26. package/src/commands/pr/status.js +70 -0
  27. package/src/commands/repo/list.js +113 -0
  28. package/src/commands/search.js +62 -0
  29. package/src/commands/tasks/assigned.js +131 -0
  30. package/src/commands/tasks/list.js +133 -0
  31. package/src/commands/tasks/today.js +73 -0
  32. package/src/commands/upgrade.js +52 -0
  33. package/src/commands/whoami.js +85 -0
  34. package/src/formatters/cost.js +54 -0
  35. package/src/formatters/markdown.js +108 -0
  36. package/src/formatters/openapi.js +146 -0
  37. package/src/formatters/status.js +48 -0
  38. package/src/formatters/table.js +87 -0
  39. package/src/help.js +312 -0
  40. package/src/hooks/init.js +9 -0
  41. package/src/hooks/postrun.js +18 -0
  42. package/src/index.js +1 -0
  43. package/src/services/auth.js +83 -0
  44. package/src/services/aws-costs.js +80 -0
  45. package/src/services/clickup.js +288 -0
  46. package/src/services/config.js +59 -0
  47. package/src/services/docs.js +210 -0
  48. package/src/services/github.js +377 -0
  49. package/src/services/platform.js +48 -0
  50. package/src/services/shell.js +42 -0
  51. package/src/services/version-check.js +58 -0
  52. package/src/types.js +228 -0
  53. package/src/utils/banner.js +48 -0
  54. package/src/utils/errors.js +61 -0
  55. package/src/utils/gradient.js +130 -0
  56. package/src/utils/open-browser.js +29 -0
  57. package/src/utils/typewriter.js +48 -0
  58. package/src/validators/repo-name.js +42 -0
package/package.json ADDED
@@ -0,0 +1,161 @@
1
+ {
2
+ "name": "devvami",
3
+ "description": "DevEx CLI for developers and teams — manage repos, PRs, pipelines, tasks, and costs from the terminal",
4
+ "version": "1.0.0",
5
+ "author": "",
6
+ "type": "module",
7
+ "bin": {
8
+ "dvmi": "./bin/run.js"
9
+ },
10
+ "packageManager": "pnpm@10.32.1",
11
+ "engines": {
12
+ "node": ">=24.0.0",
13
+ "pnpm": ">=10"
14
+ },
15
+ "license": "MIT",
16
+ "files": [
17
+ "./bin",
18
+ "./src",
19
+ "./oclif.manifest.json"
20
+ ],
21
+ "main": "src/index.js",
22
+ "oclif": {
23
+ "bin": "dvmi",
24
+ "dirname": "dvmi",
25
+ "commands": "./src/commands",
26
+ "helpClass": "./src/help.js",
27
+ "plugins": [
28
+ "@oclif/plugin-help",
29
+ "@oclif/plugin-autocomplete"
30
+ ],
31
+ "hooks": {
32
+ "init": "./src/hooks/init.js",
33
+ "postrun": "./src/hooks/postrun.js"
34
+ },
35
+ "topicSeparator": " ",
36
+ "theme": {
37
+ "sectionHeader": "#FF6B2B",
38
+ "sectionDescription": "#FFFFFF",
39
+ "bin": "#FF9A5C",
40
+ "command": "#FF9A5C",
41
+ "commandSummary": "#CCCCCC",
42
+ "topic": "#FF9A5C",
43
+ "version": "#4A9EFF",
44
+ "dollarSign": "#FF6B2B",
45
+ "flag": "#4A9EFF",
46
+ "flagSeparator": "#888888",
47
+ "flagDefaultValue": "#888888",
48
+ "flagOptions": "#888888",
49
+ "flagRequired": "#FF6B2B",
50
+ "alias": "#888888"
51
+ },
52
+ "topics": {
53
+ "auth": {
54
+ "description": "🔑 GitHub and AWS authentication"
55
+ },
56
+ "create": {
57
+ "description": "🏗️ Create projects from templates"
58
+ },
59
+ "branch": {
60
+ "description": "🌿 Branch management with naming conventions"
61
+ },
62
+ "pr": {
63
+ "description": "🔀 Pull Request workflow"
64
+ },
65
+ "repo": {
66
+ "description": "📁 GitHub repository management"
67
+ },
68
+ "pipeline": {
69
+ "description": "⚙️ GitHub Actions monitoring"
70
+ },
71
+ "tasks": {
72
+ "description": "✅ Task management"
73
+ },
74
+ "costs": {
75
+ "description": "💰 AWS cost analysis"
76
+ },
77
+ "docs": {
78
+ "description": "📖 Repository documentation"
79
+ }
80
+ }
81
+ },
82
+ "dependencies": {
83
+ "@aws-sdk/client-cost-explorer": "^3",
84
+ "@inquirer/prompts": "^7",
85
+ "@oclif/core": "^4",
86
+ "@oclif/plugin-autocomplete": "^3",
87
+ "@oclif/plugin-help": "^6",
88
+ "chalk": "^5",
89
+ "execa": "^9",
90
+ "figlet": "^1",
91
+ "js-yaml": "^4.1.1",
92
+ "keytar": "^7",
93
+ "marked": "^9.1.6",
94
+ "octokit": "^4",
95
+ "open": "^10",
96
+ "ora": "^8",
97
+ "pako": "^2.1.0"
98
+ },
99
+ "devDependencies": {
100
+ "@commitlint/cli": "^19",
101
+ "@commitlint/config-conventional": "^19",
102
+ "@commitlint/types": "^19",
103
+ "@eslint/compat": "^1",
104
+ "@semantic-release/changelog": "^6",
105
+ "@semantic-release/git": "^10",
106
+ "@semantic-release/github": "^10",
107
+ "@vitest/coverage-v8": "^3",
108
+ "commitizen": "^4",
109
+ "conventional-changelog-conventionalcommits": "^8",
110
+ "cz-git": "^1",
111
+ "eslint": "^9",
112
+ "eslint-config-prettier": "^10",
113
+ "eslint-plugin-jsdoc": "^50",
114
+ "lefthook": "^1",
115
+ "memfs": "^4",
116
+ "msw": "^2",
117
+ "oclif": "^4",
118
+ "prettier": "^3",
119
+ "semantic-release": "^24",
120
+ "shx": "^0.4.0",
121
+ "strip-ansi": "^7",
122
+ "vitest": "^3"
123
+ },
124
+ "scripts": {
125
+ "commit": "git-cz",
126
+ "release": "semantic-release",
127
+ "release:dry-run": "semantic-release --dry-run",
128
+ "lint": "eslint src/ tests/",
129
+ "lint:fix": "eslint src/ tests/ --fix",
130
+ "format": "prettier --write src/ tests/",
131
+ "test": "vitest run",
132
+ "test:unit": "vitest run --project unit",
133
+ "test:services": "vitest run --project services",
134
+ "test:integration": "vitest run --project integration",
135
+ "test:watch": "vitest",
136
+ "test:coverage": "vitest run --coverage",
137
+ "prepack": "oclif manifest",
138
+ "postpack": "shx rm -f oclif.manifest.json",
139
+ "prepare": "lefthook install"
140
+ },
141
+ "config": {
142
+ "commitizen": {
143
+ "path": "cz-git"
144
+ }
145
+ },
146
+ "repository": {
147
+ "type": "git",
148
+ "url": "https://github.com/savez/devvami"
149
+ },
150
+ "publishConfig": {
151
+ "access": "public",
152
+ "registry": "https://registry.npmjs.org"
153
+ },
154
+ "pnpm": {
155
+ "onlyBuiltDependencies": [
156
+ "esbuild",
157
+ "keytar",
158
+ "msw"
159
+ ]
160
+ }
161
+ }
@@ -0,0 +1,89 @@
1
+ import { Command, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import { checkGitHubAuth, loginGitHub, checkAWSAuth, loginAWS } from '../../services/auth.js'
5
+ import { loadConfig } from '../../services/config.js'
6
+
7
+ export default class AuthLogin extends Command {
8
+ static description = 'Autenticazione centralizzata GitHub + AWS'
9
+
10
+ static examples = [
11
+ '<%= config.bin %> auth login',
12
+ '<%= config.bin %> auth login --github',
13
+ '<%= config.bin %> auth login --aws',
14
+ ]
15
+
16
+ static enableJsonFlag = true
17
+
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 }),
22
+ }
23
+
24
+ async run() {
25
+ const { flags } = await this.parse(AuthLogin)
26
+ const isJson = flags.json
27
+ const doGitHub = !flags.aws || flags.github
28
+ const doAWS = !flags.github || flags.aws
29
+
30
+ const result = { github: null, aws: null }
31
+
32
+ // GitHub auth
33
+ if (doGitHub) {
34
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking GitHub auth...') }).start()
35
+ let ghStatus = await checkGitHubAuth()
36
+
37
+ if (ghStatus.authenticated) {
38
+ spinner?.succeed(`GitHub: already authenticated as @${ghStatus.username}`)
39
+ result.github = { status: 'ok', username: ghStatus.username, org: '' }
40
+ } else {
41
+ if (spinner) spinner.text = 'Logging in to GitHub...'
42
+ ghStatus = await loginGitHub()
43
+ if (ghStatus.authenticated) {
44
+ spinner?.succeed(`GitHub: authenticated as @${ghStatus.username}`)
45
+ result.github = { status: 'ok', username: ghStatus.username, org: '' }
46
+ } else {
47
+ spinner?.fail('GitHub authentication failed')
48
+ result.github = { status: 'error', error: ghStatus.error }
49
+ }
50
+ }
51
+ }
52
+
53
+ // AWS auth
54
+ if (doAWS) {
55
+ const config = await loadConfig()
56
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking AWS auth...') }).start()
57
+ let awsStatus = await checkAWSAuth()
58
+
59
+ if (awsStatus.authenticated) {
60
+ spinner?.succeed(`AWS: session active for account ${awsStatus.account}`)
61
+ result.aws = { status: 'ok', account: awsStatus.account, role: awsStatus.role }
62
+ } else {
63
+ if (spinner) spinner.text = 'Logging in to AWS via aws-vault...'
64
+ awsStatus = await loginAWS(config.awsProfile || 'default')
65
+ if (awsStatus.authenticated) {
66
+ spinner?.succeed(`AWS: logged in to account ${awsStatus.account}`)
67
+ result.aws = { status: 'ok', account: awsStatus.account, role: awsStatus.role }
68
+ } else {
69
+ spinner?.fail('AWS authentication failed')
70
+ result.aws = { status: 'error', error: awsStatus.error }
71
+ }
72
+ }
73
+ }
74
+
75
+ if (isJson) return result
76
+
77
+ this.log('\n' + chalk.green('Authentication complete'))
78
+ if (result.github) {
79
+ const icon = result.github.status === 'ok' ? chalk.green('✓') : chalk.red('✗')
80
+ this.log(` ${icon} GitHub: @${result.github.username ?? 'error'}`)
81
+ }
82
+ if (result.aws) {
83
+ const icon = result.aws.status === 'ok' ? chalk.green('✓') : chalk.red('✗')
84
+ this.log(` ${icon} AWS: account ${result.aws.account ?? 'error'}`)
85
+ }
86
+
87
+ return result
88
+ }
89
+ }
@@ -0,0 +1,102 @@
1
+ import { Command, Flags } from '@oclif/core'
2
+ import { writeFile } from 'node:fs/promises'
3
+ import { exec } from '../services/shell.js'
4
+
5
+ /**
6
+ * Parse a conventional commit message.
7
+ * @param {string} message
8
+ * @returns {{ type: string, scope: string, description: string }|null}
9
+ */
10
+ function parseConventionalCommit(message) {
11
+ const match = message.match(/^(\w+)(?:\(([^)]+)\))?!?: (.+)/)
12
+ if (!match) return null
13
+ return { type: match[1], scope: match[2] ?? '', description: match[3] }
14
+ }
15
+
16
+ export default class Changelog extends Command {
17
+ static description = 'Genera changelog da Conventional Commits'
18
+
19
+ static examples = [
20
+ '<%= config.bin %> changelog',
21
+ '<%= config.bin %> changelog --from v1.0.0',
22
+ '<%= config.bin %> changelog --output CHANGELOG.md',
23
+ ]
24
+
25
+ static enableJsonFlag = true
26
+
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)' }),
31
+ }
32
+
33
+ async run() {
34
+ const { flags } = await this.parse(Changelog)
35
+ const isJson = flags.json
36
+
37
+ // Determine from ref
38
+ let from = flags.from
39
+ if (!from) {
40
+ const tagResult = await exec('git', ['describe', '--tags', '--abbrev=0'])
41
+ from = tagResult.exitCode === 0 ? tagResult.stdout : ''
42
+ }
43
+
44
+ // Get commits
45
+ const range = from ? `${from}..${flags.to}` : flags.to
46
+ const logResult = await exec('git', ['log', range, '--format=%s|%H'])
47
+ if (logResult.exitCode !== 0) {
48
+ this.error('Failed to read git log. Are you in a git repository?')
49
+ }
50
+
51
+ const lines = logResult.stdout.split('\n').filter(Boolean)
52
+
53
+ /** @type {Record<string, Array<{ message: string, hash: string }>>} */
54
+ const sections = { feat: [], fix: [], chore: [], docs: [], refactor: [], test: [], other: [] }
55
+
56
+ for (const line of lines) {
57
+ const [message, hash] = line.split('|')
58
+ const parsed = parseConventionalCommit(message)
59
+ const type = parsed?.type ?? 'other'
60
+ const entry = { message: message.trim(), hash: hash?.slice(0, 7) ?? '' }
61
+ if (type in sections) {
62
+ sections[type].push(entry)
63
+ } else {
64
+ sections.other.push(entry)
65
+ }
66
+ }
67
+
68
+ if (isJson) return { from: from || 'beginning', to: flags.to, sections }
69
+
70
+ // Build markdown
71
+ const title = `## [Unreleased]${from ? ` (since ${from})` : ''}`
72
+ const parts = [title, '']
73
+
74
+ const sectionTitles = {
75
+ feat: '### Features',
76
+ fix: '### Bug Fixes',
77
+ chore: '### Chores',
78
+ docs: '### Documentation',
79
+ refactor: '### Refactoring',
80
+ test: '### Tests',
81
+ other: '### Other',
82
+ }
83
+
84
+ for (const [type, entries] of Object.entries(sections)) {
85
+ if (entries.length === 0) continue
86
+ parts.push(sectionTitles[type])
87
+ for (const e of entries) parts.push(`- ${e.message}`)
88
+ parts.push('')
89
+ }
90
+
91
+ const output = parts.join('\n')
92
+
93
+ if (flags.output) {
94
+ await writeFile(flags.output, output, 'utf8')
95
+ this.log(`Changelog written to ${flags.output}`)
96
+ } else {
97
+ this.log(output)
98
+ }
99
+
100
+ return { from: from || 'beginning', to: flags.to, sections }
101
+ }
102
+ }
@@ -0,0 +1,73 @@
1
+ import { Command, Args, Flags } from '@oclif/core'
2
+ import ora from 'ora'
3
+ import { getServiceCosts } from '../../services/aws-costs.js'
4
+ import { loadConfig } from '../../services/config.js'
5
+ import { formatCostTable, calculateTotal } from '../../formatters/cost.js'
6
+
7
+ export default class CostsGet extends Command {
8
+ static description = 'Stima costi AWS per un servizio (via Cost Explorer API)'
9
+
10
+ static examples = [
11
+ '<%= config.bin %> costs get my-service',
12
+ '<%= config.bin %> costs get my-api --period mtd',
13
+ '<%= config.bin %> costs get my-service --json',
14
+ ]
15
+
16
+ static enableJsonFlag = true
17
+
18
+ static args = {
19
+ service: Args.string({ description: 'Nome del servizio', required: true }),
20
+ }
21
+
22
+ static flags = {
23
+ period: Flags.string({
24
+ description: 'Periodo: last-month, last-week, mtd',
25
+ default: 'last-month',
26
+ options: ['last-month', 'last-week', 'mtd'],
27
+ }),
28
+ }
29
+
30
+ async run() {
31
+ const { args, flags } = await this.parse(CostsGet)
32
+ const isJson = flags.json
33
+
34
+ const spinner = isJson ? null : ora(`Fetching costs for ${args.service}...`).start()
35
+
36
+ // Get project tags from config
37
+ const config = await loadConfig()
38
+ const tags = config.projectTags ?? { project: args.service }
39
+
40
+ try {
41
+ const { entries, period } = await getServiceCosts(args.service, tags, /** @type {any} */ (flags.period))
42
+ spinner?.stop()
43
+
44
+ const total = calculateTotal(entries)
45
+ const result = {
46
+ service: args.service,
47
+ period,
48
+ items: entries,
49
+ total: { amount: total, unit: 'USD' },
50
+ }
51
+
52
+ if (isJson) return result
53
+
54
+ if (entries.length === 0) {
55
+ this.log(`No costs found for service "${args.service}".`)
56
+ this.log('Check service name and tagging convention.')
57
+ return result
58
+ }
59
+
60
+ this.log(formatCostTable(entries, args.service))
61
+ return result
62
+ } catch (err) {
63
+ spinner?.stop()
64
+ if (String(err).includes('AccessDenied') || String(err).includes('UnauthorizedAccess')) {
65
+ this.error('Missing IAM permission: ce:GetCostAndUsage\nContact your AWS admin to grant Cost Explorer access.')
66
+ }
67
+ if (String(err).includes('CredentialsProviderError') || String(err).includes('No credentials')) {
68
+ this.error('No AWS credentials. Use: aws-vault exec <profile> -- dvmi costs get ' + args.service)
69
+ }
70
+ throw err
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,196 @@
1
+ import { Command, Args, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
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'
10
+
11
+ /**
12
+ * @param {string} lang
13
+ * @returns {string}
14
+ */
15
+ function langColor(lang) {
16
+ const map = {
17
+ javascript: chalk.yellow,
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,
28
+ }
29
+ const fn = map[lang.toLowerCase()]
30
+ return fn ? fn(lang) : chalk.dim(lang)
31
+ }
32
+
33
+ export default class CreateRepo extends Command {
34
+ static description = 'Crea nuovo progetto da template GitHub o lista i template disponibili'
35
+
36
+ static examples = [
37
+ '<%= config.bin %> create repo --list',
38
+ '<%= config.bin %> create repo --list --search "lambda"',
39
+ '<%= config.bin %> create repo template-lambda',
40
+ '<%= config.bin %> create repo template-lambda --name my-service --dry-run',
41
+ ]
42
+
43
+ static enableJsonFlag = true
44
+
45
+ static args = {
46
+ template: Args.string({ description: 'Nome del template', required: false }),
47
+ }
48
+
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 }),
57
+ }
58
+
59
+ async run() {
60
+ const { args, flags } = await this.parse(CreateRepo)
61
+ const isJson = flags.json
62
+ const isDryRun = flags['dry-run']
63
+ const config = await loadConfig()
64
+
65
+ if (!config.org) {
66
+ this.error('GitHub org not configured. Run `dvmi init` to set up your environment.')
67
+ }
68
+
69
+ // --list mode
70
+ if (flags.list || !args.template) {
71
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching templates...') }).start()
72
+ const templates = await listTemplates(config.org)
73
+ spinner?.stop()
74
+
75
+ // Search filter
76
+ const searchQuery = flags.search?.toLowerCase()
77
+ const filtered = searchQuery
78
+ ? templates.filter((t) =>
79
+ t.name.toLowerCase().includes(searchQuery) ||
80
+ t.description.toLowerCase().includes(searchQuery),
81
+ )
82
+ : templates
83
+
84
+ if (isJson) return { templates: filtered }
85
+
86
+ if (templates.length === 0) {
87
+ this.log(chalk.yellow('No templates found in the organization.'))
88
+ this.log(chalk.dim('Templates are GitHub repos marked as "Template repository".'))
89
+ return { templates: [] }
90
+ }
91
+
92
+ if (filtered.length === 0) {
93
+ this.log(chalk.dim(`No templates matching "${flags.search}".`))
94
+ return { templates: [] }
95
+ }
96
+
97
+ const filterInfo = flags.search
98
+ ? chalk.dim(' — search: ') + chalk.white(`"${flags.search}"`)
99
+ : ''
100
+
101
+ this.log(
102
+ chalk.bold('\nAvailable templates') +
103
+ filterInfo +
104
+ chalk.dim(` (${filtered.length}${filtered.length < templates.length ? `/${templates.length}` : ''})`) +
105
+ '\n',
106
+ )
107
+
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
+ ]))
113
+
114
+ this.log('')
115
+ return { templates: filtered }
116
+ }
117
+
118
+ // Create mode
119
+ const templates = await listTemplates(config.org)
120
+ const template = templates.find((t) => t.name === args.template)
121
+ if (!template) {
122
+ const names = templates.map((t) => t.name).join(', ')
123
+ this.error(`Template "${args.template}" not found. Available: ${names}`)
124
+ }
125
+
126
+ // Get repo name
127
+ let repoName = flags.name
128
+ if (!repoName && !isJson) {
129
+ repoName = await input({ message: 'Repository name:' })
130
+ } else if (!repoName) {
131
+ this.error('--name is required in non-interactive mode')
132
+ }
133
+
134
+ const validation = validateRepoName(repoName)
135
+ if (!validation.valid) {
136
+ this.error(`${validation.error}${validation.suggestion ? `\nSuggestion: ${validation.suggestion}` : ''}`)
137
+ }
138
+
139
+ const isPrivate = !flags.public
140
+
141
+ if (!isJson && !isDryRun) {
142
+ const ok = await confirm({
143
+ message: `Create ${isPrivate ? 'private' : 'public'} repo "${config.org}/${repoName}" from "${args.template}"?`,
144
+ })
145
+ if (!ok) { this.log('Aborted.'); return }
146
+ }
147
+
148
+ if (isDryRun) {
149
+ 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' },
152
+ }
153
+ if (isJson) return preview
154
+ this.log(chalk.bold('\nDry run preview:'))
155
+ this.log(JSON.stringify(preview, null, 2))
156
+ return preview
157
+ }
158
+
159
+ // Create repo
160
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Creating repository...') }).start()
161
+ const repo = await createFromTemplate({
162
+ templateOwner: config.org,
163
+ templateRepo: args.template,
164
+ name: repoName,
165
+ org: config.org,
166
+ description: flags.description,
167
+ isPrivate,
168
+ })
169
+ spinner?.succeed(`Repository created: ${repo.htmlUrl}`)
170
+
171
+ // Post-scaffolding
172
+ const bpSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Configuring branch protection...') }).start()
173
+ await setBranchProtection(config.org, repoName).catch(() => null)
174
+ bpSpinner?.succeed('Branch protection configured')
175
+
176
+ const depSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Enabling Dependabot...') }).start()
177
+ await enableDependabot(config.org, repoName).catch(() => null)
178
+ depSpinner?.succeed('Dependabot enabled')
179
+
180
+ // Clone
181
+ const cloneSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Cloning repository...') }).start()
182
+ await exec('gh', ['repo', 'clone', `${config.org}/${repoName}`])
183
+ cloneSpinner?.succeed(`Cloned to ./${repoName}`)
184
+
185
+ const result = {
186
+ repository: { name: repoName, url: repo.htmlUrl, localPath: `./${repoName}` },
187
+ postScaffolding: { branchProtection: 'ok', dependabot: 'ok', codeowners: 'ok' },
188
+ }
189
+
190
+ if (!isJson) {
191
+ this.log('\n' + chalk.green('✓') + ` cd ${repoName} to start working`)
192
+ }
193
+
194
+ return result
195
+ }
196
+ }