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.
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/oclif.manifest.json +1238 -0
- package/package.json +161 -0
- package/src/commands/auth/login.js +89 -0
- package/src/commands/changelog.js +102 -0
- package/src/commands/costs/get.js +73 -0
- package/src/commands/create/repo.js +196 -0
- package/src/commands/docs/list.js +110 -0
- package/src/commands/docs/projects.js +92 -0
- package/src/commands/docs/read.js +172 -0
- package/src/commands/docs/search.js +103 -0
- package/src/commands/doctor.js +115 -0
- package/src/commands/init.js +222 -0
- package/src/commands/open.js +75 -0
- package/src/commands/pipeline/logs.js +41 -0
- package/src/commands/pipeline/rerun.js +66 -0
- package/src/commands/pipeline/status.js +62 -0
- package/src/commands/pr/create.js +114 -0
- package/src/commands/pr/detail.js +83 -0
- package/src/commands/pr/review.js +51 -0
- package/src/commands/pr/status.js +70 -0
- package/src/commands/repo/list.js +113 -0
- package/src/commands/search.js +62 -0
- package/src/commands/tasks/assigned.js +131 -0
- package/src/commands/tasks/list.js +133 -0
- package/src/commands/tasks/today.js +73 -0
- package/src/commands/upgrade.js +52 -0
- package/src/commands/whoami.js +85 -0
- package/src/formatters/cost.js +54 -0
- package/src/formatters/markdown.js +108 -0
- package/src/formatters/openapi.js +146 -0
- package/src/formatters/status.js +48 -0
- package/src/formatters/table.js +87 -0
- package/src/help.js +312 -0
- package/src/hooks/init.js +9 -0
- package/src/hooks/postrun.js +18 -0
- package/src/index.js +1 -0
- package/src/services/auth.js +83 -0
- package/src/services/aws-costs.js +80 -0
- package/src/services/clickup.js +288 -0
- package/src/services/config.js +59 -0
- package/src/services/docs.js +210 -0
- package/src/services/github.js +377 -0
- package/src/services/platform.js +48 -0
- package/src/services/shell.js +42 -0
- package/src/services/version-check.js +58 -0
- package/src/types.js +228 -0
- package/src/utils/banner.js +48 -0
- package/src/utils/errors.js +61 -0
- package/src/utils/gradient.js +130 -0
- package/src/utils/open-browser.js +29 -0
- package/src/utils/typewriter.js +48 -0
- package/src/validators/repo-name.js +42 -0
package/src/types.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module types
|
|
3
|
+
* Shared JSDoc typedefs for the devvami CLI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} CLIConfig
|
|
8
|
+
* @property {string} org - GitHub org name (e.g. "devvami")
|
|
9
|
+
* @property {string} awsProfile - Default aws-vault profile name
|
|
10
|
+
* @property {string} [awsRegion] - Default AWS region (fallback: eu-west-1)
|
|
11
|
+
* @property {string} [shell] - Detected shell: bash | zsh | fish
|
|
12
|
+
* @property {{ teamId?: string, teamName?: string, authMethod?: 'oauth' | 'personal_token' }} [clickup] - ClickUp workspace config
|
|
13
|
+
* @property {string} [lastVersionCheck] - ISO8601 timestamp of last version check
|
|
14
|
+
* @property {string} [latestVersion] - Latest known CLI version
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {'ok'|'warn'|'fail'} CheckStatus
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} DoctorCheck
|
|
23
|
+
* @property {string} name - Component name (e.g. "Node.js")
|
|
24
|
+
* @property {CheckStatus} status - Check result
|
|
25
|
+
* @property {string|null} version - Found version (if applicable)
|
|
26
|
+
* @property {string|null} required - Minimum required version (if applicable)
|
|
27
|
+
* @property {string|null} hint - Actionable hint to fix the issue
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} BranchName
|
|
32
|
+
* @property {'feature'|'fix'|'chore'|'hotfix'} type - Branch type
|
|
33
|
+
* @property {string} description - kebab-case description
|
|
34
|
+
* @property {string} full - Full branch name: `{type}/{description}`
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} Developer
|
|
39
|
+
* @property {string} githubUsername
|
|
40
|
+
* @property {string} githubName
|
|
41
|
+
* @property {string[]} githubOrgs
|
|
42
|
+
* @property {string[]} githubTeams
|
|
43
|
+
* @property {string} [awsAccountId]
|
|
44
|
+
* @property {string} [awsArn]
|
|
45
|
+
* @property {string} [awsRegion]
|
|
46
|
+
* @property {string} cliVersion
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} Template
|
|
51
|
+
* @property {string} name
|
|
52
|
+
* @property {string} description
|
|
53
|
+
* @property {string} language
|
|
54
|
+
* @property {string} htmlUrl
|
|
55
|
+
* @property {string} updatedAt
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {Object} Repository
|
|
60
|
+
* @property {string} name
|
|
61
|
+
* @property {string} description
|
|
62
|
+
* @property {string} language
|
|
63
|
+
* @property {string} htmlUrl
|
|
64
|
+
* @property {string} pushedAt
|
|
65
|
+
* @property {string[]} topics
|
|
66
|
+
* @property {boolean} isPrivate
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {'pass'|'fail'|'pending'} CIStatus
|
|
71
|
+
* @typedef {'approved'|'changes_requested'|'pending'} ReviewStatus
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {Object} PullRequest
|
|
76
|
+
* @property {number} number
|
|
77
|
+
* @property {string} title
|
|
78
|
+
* @property {string} state
|
|
79
|
+
* @property {string} htmlUrl
|
|
80
|
+
* @property {string} headBranch
|
|
81
|
+
* @property {string} baseBranch
|
|
82
|
+
* @property {boolean} isDraft
|
|
83
|
+
* @property {CIStatus} ciStatus
|
|
84
|
+
* @property {ReviewStatus} reviewStatus
|
|
85
|
+
* @property {boolean} mergeable
|
|
86
|
+
* @property {string} author
|
|
87
|
+
* @property {string[]} reviewers
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @typedef {Object} PRComment
|
|
92
|
+
* @property {number} id
|
|
93
|
+
* @property {string} author - GitHub login dell'autore
|
|
94
|
+
* @property {string} body - Corpo del commento in markdown
|
|
95
|
+
* @property {string} createdAt - ISO8601 timestamp
|
|
96
|
+
* @property {'issue'|'review'} type - Sorgente del commento
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @typedef {Object} QAStep
|
|
101
|
+
* @property {string} text - Testo dello step
|
|
102
|
+
* @property {boolean} checked - true se completato (`[x]`)
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @typedef {Object} PRDetail
|
|
107
|
+
* @property {number} number
|
|
108
|
+
* @property {string} title
|
|
109
|
+
* @property {string} state
|
|
110
|
+
* @property {string} htmlUrl
|
|
111
|
+
* @property {string} author
|
|
112
|
+
* @property {string} headBranch
|
|
113
|
+
* @property {string} baseBranch
|
|
114
|
+
* @property {boolean} isDraft
|
|
115
|
+
* @property {string[]} labels
|
|
116
|
+
* @property {string[]} reviewers
|
|
117
|
+
* @property {PRComment[]} qaComments - Commenti identificati come QA
|
|
118
|
+
* @property {QAStep[]} qaSteps - Step QA estratti dai commenti
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @typedef {'completed'|'in_progress'|'queued'} RunStatus
|
|
123
|
+
* @typedef {'success'|'failure'|'cancelled'|null} RunConclusion
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @typedef {Object} PipelineRun
|
|
128
|
+
* @property {number} id
|
|
129
|
+
* @property {string} name
|
|
130
|
+
* @property {RunStatus} status
|
|
131
|
+
* @property {RunConclusion} conclusion
|
|
132
|
+
* @property {string} branch
|
|
133
|
+
* @property {number} duration - seconds
|
|
134
|
+
* @property {string} actor
|
|
135
|
+
* @property {string} createdAt
|
|
136
|
+
* @property {string} htmlUrl
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @typedef {Object} ClickUpTask
|
|
141
|
+
* @property {string} id
|
|
142
|
+
* @property {string} name
|
|
143
|
+
* @property {string} status
|
|
144
|
+
* @property {string} statusType - ClickUp internal status type: 'open' | 'in_progress' | 'review' | 'custom' | 'closed'
|
|
145
|
+
* @property {number} priority - 1=urgent, 2=high, 3=normal, 4=low
|
|
146
|
+
* @property {string|null} startDate - YYYY-MM-DD local date, null if not set
|
|
147
|
+
* @property {string|null} dueDate - YYYY-MM-DD local date, null if not set
|
|
148
|
+
* @property {string} url
|
|
149
|
+
* @property {string[]} assignees
|
|
150
|
+
* @property {string|null} listId - ClickUp list ID the task belongs to
|
|
151
|
+
* @property {string|null} listName - ClickUp list name the task belongs to
|
|
152
|
+
* @property {string|null} folderId - ClickUp folder ID, null if list is at root level
|
|
153
|
+
* @property {string|null} folderName - ClickUp folder name, null if list is at root level
|
|
154
|
+
*/
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @typedef {Object} AWSCostEntry
|
|
158
|
+
* @property {string} serviceName
|
|
159
|
+
* @property {number} amount
|
|
160
|
+
* @property {string} unit
|
|
161
|
+
* @property {{ start: string, end: string }} period
|
|
162
|
+
*/
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @typedef {Object} ExecResult
|
|
166
|
+
* @property {string} stdout
|
|
167
|
+
* @property {string} stderr
|
|
168
|
+
* @property {number} exitCode
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @typedef {'macos'|'wsl2'|'linux'} Platform
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @typedef {Object} PlatformInfo
|
|
177
|
+
* @property {Platform} platform
|
|
178
|
+
* @property {string} openCommand - Command to open browser
|
|
179
|
+
* @property {string} credentialHelper - Git credential helper
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @typedef {Object} DocumentEntry
|
|
184
|
+
* @property {string} name - File name (e.g. "README.md")
|
|
185
|
+
* @property {string} path - Relative path in repo (e.g. "docs/architecture.md")
|
|
186
|
+
* @property {'readme'|'doc'|'swagger'|'asyncapi'} type - Classified doc type
|
|
187
|
+
* @property {number} size - File size in bytes
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @typedef {Object} RepoDocsIndex
|
|
192
|
+
* @property {string} repo - Repository name
|
|
193
|
+
* @property {boolean} hasReadme - Has at least one README file
|
|
194
|
+
* @property {number} docsCount - Number of files in docs/ folder
|
|
195
|
+
* @property {boolean} hasSwagger - Has at least one OpenAPI/Swagger file
|
|
196
|
+
* @property {boolean} hasAsyncApi - Has at least one AsyncAPI file
|
|
197
|
+
* @property {DocumentEntry[]} entries - Full list of DocumentEntry found
|
|
198
|
+
*/
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @typedef {Object} SearchMatch
|
|
202
|
+
* @property {string} file - File path (e.g. "docs/deploy.md")
|
|
203
|
+
* @property {number} line - Line number (1-based)
|
|
204
|
+
* @property {string} context - Line text containing the match
|
|
205
|
+
* @property {number} occurrences - Total number of occurrences in the file
|
|
206
|
+
*/
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @typedef {Object} APIEndpoint
|
|
210
|
+
* @property {string} method - HTTP method (GET, POST, PUT, PATCH, DELETE…)
|
|
211
|
+
* @property {string} path - Endpoint path (e.g. "/users/{id}")
|
|
212
|
+
* @property {string} summary - Operation summary
|
|
213
|
+
* @property {string} parameters - Comma-separated params; required ones marked with *
|
|
214
|
+
*/
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @typedef {Object} AsyncChannel
|
|
218
|
+
* @property {string} channel - Channel name (e.g. "user/created")
|
|
219
|
+
* @property {string} operation - publish | subscribe | send | receive
|
|
220
|
+
* @property {string} summary - Operation summary
|
|
221
|
+
* @property {string} message - Message name/title or "—"
|
|
222
|
+
*/
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @typedef {Object} DetectedRepo
|
|
226
|
+
* @property {string} owner - GitHub owner (org or user)
|
|
227
|
+
* @property {string} repo - Repository name
|
|
228
|
+
*/
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import figlet from 'figlet'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { BRAND_GRADIENT, animateGradientBanner, isColorEnabled } from './gradient.js'
|
|
4
|
+
|
|
5
|
+
// Brand colors
|
|
6
|
+
export const ORANGE = '#FF6B2B'
|
|
7
|
+
export const BLUE = '#4A9EFF'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Render figlet text as a Promise.
|
|
11
|
+
* @param {string} text
|
|
12
|
+
* @param {figlet.Options} opts
|
|
13
|
+
* @returns {Promise<string>}
|
|
14
|
+
*/
|
|
15
|
+
function figletAsync(text, opts) {
|
|
16
|
+
return new Promise((resolve, reject) =>
|
|
17
|
+
figlet.text(text, opts, (err, result) => (err ? reject(err) : resolve(result ?? ''))),
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Print the devvami welcome banner con gradient animato ciano→blu→indaco.
|
|
23
|
+
* In ambienti non-TTY (CI, pipe, --json) stampa un banner statico senza ANSI.
|
|
24
|
+
* @returns {Promise<void>}
|
|
25
|
+
*/
|
|
26
|
+
export async function printBanner() {
|
|
27
|
+
const art = await figletAsync('DVMI', { font: 'ANSI Shadow' })
|
|
28
|
+
const artLines = art.split('\n').filter((l) => l.trim() !== '')
|
|
29
|
+
const width = Math.max(...artLines.map((l) => l.length)) + 4
|
|
30
|
+
|
|
31
|
+
const tagline = isColorEnabled
|
|
32
|
+
? chalk.hex(BLUE).bold(' Devvami Developer CLI')
|
|
33
|
+
: ' Devvami Developer CLI'
|
|
34
|
+
|
|
35
|
+
const separator = isColorEnabled
|
|
36
|
+
? chalk.hex(BLUE).dim('─'.repeat(Math.min(width, 60)))
|
|
37
|
+
: '─'.repeat(Math.min(width, 60))
|
|
38
|
+
|
|
39
|
+
process.stdout.write('\n')
|
|
40
|
+
|
|
41
|
+
// Anima ogni riga dell'ASCII art con gradient brand
|
|
42
|
+
await animateGradientBanner(artLines, BRAND_GRADIENT)
|
|
43
|
+
|
|
44
|
+
process.stdout.write(separator + '\n')
|
|
45
|
+
process.stdout.write(tagline + '\n')
|
|
46
|
+
process.stdout.write(separator + '\n')
|
|
47
|
+
process.stdout.write('\n')
|
|
48
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base CLI error with an actionable hint for the user.
|
|
3
|
+
*/
|
|
4
|
+
export class DvmiError extends Error {
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} message - Human-readable error message
|
|
7
|
+
* @param {string} hint - Actionable suggestion to resolve the error
|
|
8
|
+
* @param {number} [exitCode] - Process exit code (default: 1)
|
|
9
|
+
*/
|
|
10
|
+
constructor(message, hint, exitCode = 1) {
|
|
11
|
+
super(message)
|
|
12
|
+
this.name = 'DvmiError'
|
|
13
|
+
/** @type {string} */
|
|
14
|
+
this.hint = hint
|
|
15
|
+
/** @type {number} */
|
|
16
|
+
this.exitCode = exitCode
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validation error for invalid user input (exit code 2).
|
|
22
|
+
*/
|
|
23
|
+
export class ValidationError extends DvmiError {
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} message
|
|
26
|
+
* @param {string} hint
|
|
27
|
+
*/
|
|
28
|
+
constructor(message, hint) {
|
|
29
|
+
super(message, hint, 2)
|
|
30
|
+
this.name = 'ValidationError'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Auth error for missing or expired authentication.
|
|
36
|
+
*/
|
|
37
|
+
export class AuthError extends DvmiError {
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} service - Service name (e.g. "GitHub", "AWS")
|
|
40
|
+
*/
|
|
41
|
+
constructor(service) {
|
|
42
|
+
super(
|
|
43
|
+
`${service} authentication required`,
|
|
44
|
+
`Run \`dvmi auth login\` to authenticate`,
|
|
45
|
+
1,
|
|
46
|
+
)
|
|
47
|
+
this.name = 'AuthError'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format an error for display in the terminal.
|
|
53
|
+
* @param {Error} err
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
export function formatError(err) {
|
|
57
|
+
if (err instanceof DvmiError) {
|
|
58
|
+
return `Error: ${err.message}\nHint: ${err.hint}`
|
|
59
|
+
}
|
|
60
|
+
return `Error: ${err.message}`
|
|
61
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import readline from 'node:readline'
|
|
3
|
+
|
|
4
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Brand gradient: ciano → blu vivido → indaco
|
|
6
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {[number, number, number]} GradientStop
|
|
10
|
+
* RGB tuple: [red 0-255, green 0-255, blue 0-255]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** @type {GradientStop[]} */
|
|
14
|
+
export const BRAND_GRADIENT = [
|
|
15
|
+
[0, 212, 255], // #00D4FF — ciano elettrico
|
|
16
|
+
[0, 100, 255], // #0064FF — blu vivido
|
|
17
|
+
[100, 0, 220], // #6400DC — indaco profondo
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Terminal capability flags
|
|
22
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/** @type {boolean} true se chalk ha colori E NO_COLOR non è impostato */
|
|
25
|
+
export const isColorEnabled = chalk.level > 0 && process.env.NO_COLOR === undefined
|
|
26
|
+
|
|
27
|
+
/** @type {boolean} true se isTTY E isColorEnabled */
|
|
28
|
+
export const isAnimationEnabled = process.stdout.isTTY === true && isColorEnabled
|
|
29
|
+
|
|
30
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// gradientText
|
|
32
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Colora ogni carattere del testo con un gradient RGB interpolato.
|
|
36
|
+
* Se chalk.level === 0 o NO_COLOR impostato, ritorna `text` invariato.
|
|
37
|
+
* Gli spazi non vengono colorati per evitare artefatti.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} text - Testo da colorare
|
|
40
|
+
* @param {GradientStop[]} stops - Almeno 2 color stops RGB
|
|
41
|
+
* @param {number} [phase=0] - Offset 0.0–1.0 per shift animato
|
|
42
|
+
* @returns {string} - Stringa ANSI-colorata, o text invariato se no-color
|
|
43
|
+
*/
|
|
44
|
+
export function gradientText(text, stops, phase = 0) {
|
|
45
|
+
if (!isColorEnabled) return text
|
|
46
|
+
if (stops.length < 2) throw new Error('At least 2 gradient stops required')
|
|
47
|
+
|
|
48
|
+
const chars = [...text]
|
|
49
|
+
const len = chars.length
|
|
50
|
+
if (len === 0) return ''
|
|
51
|
+
|
|
52
|
+
const segments = stops.length - 1
|
|
53
|
+
|
|
54
|
+
return chars.map((char, i) => {
|
|
55
|
+
if (char === ' ') return char
|
|
56
|
+
|
|
57
|
+
// Normalise t in [0, 1] with phase shift
|
|
58
|
+
const t = ((i / Math.max(len - 1, 1)) + phase) % 1
|
|
59
|
+
|
|
60
|
+
const seg = Math.min(Math.floor(t * segments), segments - 1)
|
|
61
|
+
const localT = t * segments - seg
|
|
62
|
+
|
|
63
|
+
const [r1, g1, b1] = stops[seg]
|
|
64
|
+
const [r2, g2, b2] = stops[seg + 1]
|
|
65
|
+
|
|
66
|
+
const r = Math.round(r1 + (r2 - r1) * localT)
|
|
67
|
+
const g = Math.round(g1 + (g2 - g1) * localT)
|
|
68
|
+
const b = Math.round(b1 + (b2 - b1) * localT)
|
|
69
|
+
|
|
70
|
+
return chalk.rgb(r, g, b)(char)
|
|
71
|
+
}).join('')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
// animateGradientBanner
|
|
76
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Anima un banner multi-riga con gradient in scrolling, poi si ferma.
|
|
80
|
+
* Se !isTTY o no-color, stampa le righe statiche con gradient e ritorna subito.
|
|
81
|
+
* Nasconde il cursore durante l'animazione e lo ripristina sempre.
|
|
82
|
+
*
|
|
83
|
+
* @param {string[]} lines - Righe del banner (output figlet)
|
|
84
|
+
* @param {GradientStop[]} stops - Color stops
|
|
85
|
+
* @param {number} [durationMs=1500] - Durata animazione in ms
|
|
86
|
+
* @returns {Promise<void>}
|
|
87
|
+
*/
|
|
88
|
+
export async function animateGradientBanner(lines, stops, durationMs = 1500) {
|
|
89
|
+
// Stampa sempre il banner statico prima (per terminali che non supportano animazione)
|
|
90
|
+
const printStatic = (phase = 0) => {
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
process.stdout.write(gradientText(line, stops, phase) + '\n')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!isAnimationEnabled) {
|
|
97
|
+
printStatic()
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Stampa il banner iniziale e prendi nota di quante righe occupa
|
|
102
|
+
printStatic()
|
|
103
|
+
process.stdout.write('\x1B[?25l') // nascondi cursore
|
|
104
|
+
|
|
105
|
+
let phase = 0
|
|
106
|
+
const intervalMs = 80
|
|
107
|
+
const frames = Math.ceil(durationMs / intervalMs)
|
|
108
|
+
let frameCount = 0
|
|
109
|
+
|
|
110
|
+
await new Promise((resolve) => {
|
|
111
|
+
const id = setInterval(() => {
|
|
112
|
+
frameCount++
|
|
113
|
+
phase = (phase + 0.03) % 1
|
|
114
|
+
|
|
115
|
+
// Risali di `lines.length` righe e ridisegna
|
|
116
|
+
readline.moveCursor(process.stdout, 0, -lines.length)
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
readline.clearLine(process.stdout, 0)
|
|
119
|
+
process.stdout.write(gradientText(line, stops, phase) + '\n')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (frameCount >= frames) {
|
|
123
|
+
clearInterval(id)
|
|
124
|
+
resolve(undefined)
|
|
125
|
+
}
|
|
126
|
+
}, intervalMs)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
process.stdout.write('\x1B[?25h') // ripristina cursore
|
|
130
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import open from 'open'
|
|
2
|
+
import { detectPlatform } from '../services/platform.js'
|
|
3
|
+
import { exec } from '../services/shell.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Open a URL in the default browser, using the platform-appropriate command.
|
|
7
|
+
* @param {string} url
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
export async function openBrowser(url) {
|
|
11
|
+
const { platform, openCommand } = await detectPlatform()
|
|
12
|
+
|
|
13
|
+
if (platform === 'macos') {
|
|
14
|
+
await open(url)
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// WSL2: try wslview first, fallback to xdg-open
|
|
19
|
+
if (platform === 'wsl2') {
|
|
20
|
+
const wslview = await exec('wslview', [url])
|
|
21
|
+
if (wslview.exitCode === 0) return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Linux / fallback
|
|
25
|
+
const result = await exec(openCommand, [url])
|
|
26
|
+
if (result.exitCode !== 0) {
|
|
27
|
+
await open(url) // final fallback via open package
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import readline from 'node:readline'
|
|
2
|
+
import { isAnimationEnabled, BRAND_GRADIENT, gradientText } from './gradient.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stampa testo con effetto typewriter (lettera per lettera).
|
|
6
|
+
* Se !isTTY o no-color, stampa tutto in una volta.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} text - Testo da stampare
|
|
9
|
+
* @param {Object} [opts]
|
|
10
|
+
* @param {number} [opts.interval=30] - Ms per carattere
|
|
11
|
+
* @param {import('./gradient.js').GradientStop[]} [opts.gradient] - Se fornito, applica gradient
|
|
12
|
+
* @returns {Promise<void>}
|
|
13
|
+
*/
|
|
14
|
+
export async function typewriter(text, opts = {}) {
|
|
15
|
+
const { interval = 30, gradient } = opts
|
|
16
|
+
|
|
17
|
+
if (!isAnimationEnabled) {
|
|
18
|
+
const out = gradient ? gradientText(text, gradient) : text
|
|
19
|
+
process.stdout.write(out + '\n')
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const chars = [...text]
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i <= chars.length; i++) {
|
|
26
|
+
const partial = chars.slice(0, i).join('')
|
|
27
|
+
const colored = gradient ? gradientText(partial, gradient) : partial
|
|
28
|
+
|
|
29
|
+
readline.cursorTo(process.stdout, 0)
|
|
30
|
+
readline.clearLine(process.stdout, 0)
|
|
31
|
+
process.stdout.write(colored)
|
|
32
|
+
|
|
33
|
+
await new Promise((r) => setTimeout(r, interval))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
process.stdout.write('\n')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Wrapper sintetico per typewriter con BRAND_GRADIENT di default.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} text - Messaggio di completamento
|
|
43
|
+
* @param {import('./gradient.js').GradientStop[]} [gradient=BRAND_GRADIENT]
|
|
44
|
+
* @returns {Promise<void>}
|
|
45
|
+
*/
|
|
46
|
+
export async function typewriterLine(text, gradient = BRAND_GRADIENT) {
|
|
47
|
+
return typewriter(text, { gradient, interval: 25 })
|
|
48
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const REPO_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
|
2
|
+
const MAX_LENGTH = 100
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} ValidationResult
|
|
6
|
+
* @property {boolean} valid
|
|
7
|
+
* @property {string} [error]
|
|
8
|
+
* @property {string} [suggestion]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate a GitHub repository name.
|
|
13
|
+
* @param {string} name
|
|
14
|
+
* @returns {ValidationResult}
|
|
15
|
+
*/
|
|
16
|
+
export function validateRepoName(name) {
|
|
17
|
+
if (!name || name.length === 0) {
|
|
18
|
+
return { valid: false, error: 'Repository name cannot be empty' }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (name.length > MAX_LENGTH) {
|
|
22
|
+
return {
|
|
23
|
+
valid: false,
|
|
24
|
+
error: `Repository name too long (${name.length} chars, max ${MAX_LENGTH})`,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!REPO_NAME_RE.test(name)) {
|
|
29
|
+
const suggested = name
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
32
|
+
.replace(/-+/g, '-')
|
|
33
|
+
.replace(/^-|-$/g, '')
|
|
34
|
+
return {
|
|
35
|
+
valid: false,
|
|
36
|
+
error: `Repository name must be lowercase kebab-case. Got "${name}"`,
|
|
37
|
+
suggestion: suggested,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { valid: true }
|
|
42
|
+
}
|