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.
- package/README.md +72 -0
- package/oclif.manifest.json +275 -235
- package/package.json +2 -1
- package/src/commands/auth/login.js +20 -16
- package/src/commands/changelog.js +12 -12
- package/src/commands/costs/get.js +14 -24
- package/src/commands/costs/trend.js +13 -24
- package/src/commands/create/repo.js +72 -54
- package/src/commands/docs/list.js +29 -25
- package/src/commands/docs/projects.js +58 -24
- package/src/commands/docs/read.js +56 -39
- package/src/commands/docs/search.js +37 -25
- package/src/commands/doctor.js +37 -35
- package/src/commands/dotfiles/add.js +51 -39
- package/src/commands/dotfiles/setup.js +62 -33
- package/src/commands/dotfiles/status.js +18 -18
- package/src/commands/dotfiles/sync.js +62 -46
- package/src/commands/init.js +143 -132
- package/src/commands/logs/index.js +10 -16
- package/src/commands/open.js +12 -12
- package/src/commands/pipeline/logs.js +8 -11
- package/src/commands/pipeline/rerun.js +21 -16
- package/src/commands/pipeline/status.js +28 -24
- package/src/commands/pr/create.js +40 -27
- package/src/commands/pr/detail.js +9 -7
- package/src/commands/pr/review.js +18 -19
- package/src/commands/pr/status.js +27 -21
- package/src/commands/prompts/browse.js +15 -15
- package/src/commands/prompts/download.js +15 -16
- package/src/commands/prompts/install-speckit.js +11 -12
- package/src/commands/prompts/list.js +12 -12
- package/src/commands/prompts/run.js +16 -19
- package/src/commands/repo/list.js +57 -41
- package/src/commands/search.js +20 -18
- package/src/commands/security/setup.js +38 -34
- package/src/commands/sync-config-ai/index.js +257 -0
- package/src/commands/tasks/assigned.js +43 -33
- package/src/commands/tasks/list.js +43 -33
- package/src/commands/tasks/today.js +32 -30
- package/src/commands/upgrade.js +18 -17
- package/src/commands/vuln/detail.js +8 -8
- package/src/commands/vuln/scan.js +39 -20
- package/src/commands/vuln/search.js +23 -18
- package/src/commands/welcome.js +2 -2
- package/src/commands/whoami.js +19 -23
- package/src/formatters/ai-config.js +215 -0
- package/src/formatters/charts.js +6 -23
- package/src/formatters/cost.js +1 -7
- package/src/formatters/dotfiles.js +48 -19
- package/src/formatters/markdown.js +11 -6
- package/src/formatters/openapi.js +7 -9
- package/src/formatters/prompts.js +69 -78
- package/src/formatters/security.js +2 -2
- package/src/formatters/status.js +1 -1
- package/src/formatters/table.js +1 -3
- package/src/formatters/vuln.js +33 -20
- package/src/help.js +162 -164
- package/src/hooks/init.js +1 -3
- package/src/hooks/postrun.js +5 -7
- package/src/index.js +1 -1
- package/src/services/ai-config-store.js +349 -0
- package/src/services/ai-env-deployer.js +650 -0
- package/src/services/ai-env-scanner.js +983 -0
- package/src/services/audit-detector.js +2 -2
- package/src/services/audit-runner.js +40 -31
- package/src/services/auth.js +9 -9
- package/src/services/awesome-copilot.js +7 -4
- package/src/services/aws-costs.js +22 -22
- package/src/services/clickup.js +26 -26
- package/src/services/cloudwatch-logs.js +5 -9
- package/src/services/config.js +13 -13
- package/src/services/docs.js +19 -20
- package/src/services/dotfiles.js +149 -51
- package/src/services/github.js +22 -24
- package/src/services/nvd.js +21 -31
- package/src/services/platform.js +2 -2
- package/src/services/prompts.js +23 -35
- package/src/services/security.js +135 -61
- package/src/services/shell.js +4 -4
- package/src/services/skills-sh.js +3 -9
- package/src/services/speckit.js +4 -7
- package/src/services/version-check.js +10 -10
- package/src/types.js +117 -0
- package/src/utils/aws-vault.js +18 -41
- package/src/utils/banner.js +5 -7
- package/src/utils/errors.js +42 -46
- package/src/utils/frontmatter.js +4 -4
- package/src/utils/gradient.js +18 -16
- package/src/utils/open-browser.js +3 -3
- package/src/utils/tui/form.js +1184 -0
- package/src/utils/tui/modal.js +15 -14
- package/src/utils/tui/navigable-table.js +16 -16
- package/src/utils/tui/tab-tui.js +1089 -0
- package/src/utils/typewriter.js +3 -3
- package/src/utils/welcome.js +18 -21
- package/src/validators/repo-name.js +2 -2
package/src/types.js
CHANGED
|
@@ -332,6 +332,123 @@
|
|
|
332
332
|
* @typedef {'macos'|'wsl2'|'linux'} Platform
|
|
333
333
|
*/
|
|
334
334
|
|
|
335
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
336
|
+
// AI Config Sync TUI types
|
|
337
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* @typedef {'mcp'|'command'|'rule'|'skill'|'agent'} CategoryType
|
|
341
|
+
*/
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @typedef {'vscode-copilot'|'claude-code'|'claude-desktop'|'opencode'|'gemini-cli'|'copilot-cli'|'cursor'|'windsurf'|'continue-dev'|'zed'|'amazon-q'} EnvironmentId
|
|
345
|
+
*/
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* @typedef {Object} MCPParams
|
|
349
|
+
* @property {'stdio'|'sse'|'streamable-http'} transport - MCP transport type
|
|
350
|
+
* @property {string} [command] - Command to execute (required for stdio transport)
|
|
351
|
+
* @property {string[]} [args] - Command arguments
|
|
352
|
+
* @property {Record<string, string>} [env] - Environment variables
|
|
353
|
+
* @property {string} [url] - Server URL (required for sse/streamable-http transport)
|
|
354
|
+
*/
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* @typedef {Object} CommandParams
|
|
358
|
+
* @property {string} content - Prompt/command text content (multi-line)
|
|
359
|
+
* @property {string} [description] - Short description of the command
|
|
360
|
+
*/
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* @typedef {Object} RuleParams
|
|
364
|
+
* @property {string} content - Rules/instructions content (multi-line Markdown)
|
|
365
|
+
* @property {string} [description] - Short description of the rule
|
|
366
|
+
*/
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* @typedef {Object} SkillParams
|
|
370
|
+
* @property {string} content - Skill definition content (multi-line)
|
|
371
|
+
* @property {string} [description] - Short description of the skill
|
|
372
|
+
*/
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* @typedef {Object} AgentParams
|
|
376
|
+
* @property {string} instructions - Agent instructions (multi-line)
|
|
377
|
+
* @property {string} [description] - Short description of the agent
|
|
378
|
+
*/
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* @typedef {Object} CategoryEntry
|
|
382
|
+
* @property {string} id - UUID v4, auto-generated
|
|
383
|
+
* @property {string} name - Unique within its type; used as filename/key when deploying
|
|
384
|
+
* @property {CategoryType} type - Category type
|
|
385
|
+
* @property {boolean} active - true = deployed to environments, false = removed but kept in store
|
|
386
|
+
* @property {EnvironmentId[]} environments - Target environments for deployment
|
|
387
|
+
* @property {MCPParams|CommandParams|RuleParams|SkillParams|AgentParams} params - Type-specific parameters
|
|
388
|
+
* @property {string} createdAt - ISO 8601 timestamp
|
|
389
|
+
* @property {string} updatedAt - ISO 8601 timestamp
|
|
390
|
+
*/
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* @typedef {Object} AIConfigStore
|
|
394
|
+
* @property {number} version - Schema version
|
|
395
|
+
* @property {CategoryEntry[]} entries - All managed configuration entries
|
|
396
|
+
*/
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* @typedef {Object} PathStatus
|
|
400
|
+
* @property {string} path - Absolute path
|
|
401
|
+
* @property {boolean} exists - Whether the path exists on disk
|
|
402
|
+
* @property {boolean} readable - Whether the file could be parsed (for JSON/TOML files)
|
|
403
|
+
*/
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* @typedef {Object} CategoryCounts
|
|
407
|
+
* @property {number} mcp
|
|
408
|
+
* @property {number} command
|
|
409
|
+
* @property {number} rule
|
|
410
|
+
* @property {number} skill
|
|
411
|
+
* @property {number} agent
|
|
412
|
+
*/
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* @typedef {Object} NativeEntry
|
|
416
|
+
* Runtime only — not persisted. Represents an item found in an environment's config
|
|
417
|
+
* file that is NOT managed by dvmi.
|
|
418
|
+
* @property {string} name - Entry name (extracted from config key or filename)
|
|
419
|
+
* @property {CategoryType} type - Category type
|
|
420
|
+
* @property {EnvironmentId} environmentId - Source environment
|
|
421
|
+
* @property {'project'|'global'} level - Whether from project-level or global-level config
|
|
422
|
+
* @property {string} sourcePath - Absolute path to the source config file
|
|
423
|
+
* @property {object} params - Normalized parameters (same structure as managed entry params)
|
|
424
|
+
*/
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* @typedef {Object} DriftInfo
|
|
428
|
+
* Runtime only — not persisted. Describes a managed entry whose deployed state
|
|
429
|
+
* diverges from dvmi's stored expected state.
|
|
430
|
+
* @property {string} entryId - ID of the managed CategoryEntry that drifted
|
|
431
|
+
* @property {EnvironmentId} environmentId - Environment where drift was detected
|
|
432
|
+
* @property {object} expected - What dvmi expects (from store)
|
|
433
|
+
* @property {object} actual - What was found in the file
|
|
434
|
+
*/
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* @typedef {Object} DetectedEnvironment
|
|
438
|
+
* @property {EnvironmentId} id - Environment identifier
|
|
439
|
+
* @property {string} name - Display name (e.g. "Claude Code")
|
|
440
|
+
* @property {boolean} detected - Whether any config files were found
|
|
441
|
+
* @property {PathStatus[]} projectPaths - Project-level paths and their existence status
|
|
442
|
+
* @property {PathStatus[]} globalPaths - Global-level paths and their existence status
|
|
443
|
+
* @property {string[]} unreadable - Paths that exist but failed to parse
|
|
444
|
+
* @property {CategoryType[]} supportedCategories - Category types this environment supports
|
|
445
|
+
* @property {CategoryCounts} counts - Per-category item counts from dvmi-managed entries
|
|
446
|
+
* @property {CategoryCounts} nativeCounts - Per-category native item counts (items in config files)
|
|
447
|
+
* @property {NativeEntry[]} nativeEntries - All native entries found for this environment
|
|
448
|
+
* @property {DriftInfo[]} driftedEntries - Managed entries that have drifted from expected state
|
|
449
|
+
* @property {'project'|'global'|'both'} scope - Where detection occurred
|
|
450
|
+
*/
|
|
451
|
+
|
|
335
452
|
/**
|
|
336
453
|
* @typedef {Object} PlatformInfo
|
|
337
454
|
* @property {Platform} platform
|
package/src/utils/aws-vault.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {loadConfigSync} from '../services/config.js'
|
|
2
|
+
import {execa} from 'execa'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Returns the aws-vault exec prefix to prepend to AWS CLI commands.
|
|
@@ -44,10 +44,7 @@ export function awsVaultPrefix(config = null) {
|
|
|
44
44
|
* @returns {boolean}
|
|
45
45
|
*/
|
|
46
46
|
export function hasAwsCredentialEnv() {
|
|
47
|
-
return Boolean(
|
|
48
|
-
process.env.AWS_ACCESS_KEY_ID ||
|
|
49
|
-
process.env.AWS_SESSION_TOKEN,
|
|
50
|
-
)
|
|
47
|
+
return Boolean(process.env.AWS_ACCESS_KEY_ID || process.env.AWS_SESSION_TOKEN)
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
/**
|
|
@@ -79,24 +76,14 @@ export async function reexecCurrentCommandWithAwsVault(config = null) {
|
|
|
79
76
|
if (process.env.DVMI_AWS_VAULT_REEXEC === '1') return null
|
|
80
77
|
|
|
81
78
|
try {
|
|
82
|
-
const child = await execa(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
'
|
|
88
|
-
process.execPath,
|
|
89
|
-
...process.argv.slice(1),
|
|
90
|
-
],
|
|
91
|
-
{
|
|
92
|
-
reject: false,
|
|
93
|
-
stdio: 'inherit',
|
|
94
|
-
env: {
|
|
95
|
-
...process.env,
|
|
96
|
-
DVMI_AWS_VAULT_REEXEC: '1',
|
|
97
|
-
},
|
|
79
|
+
const child = await execa('aws-vault', ['exec', profile, '--', process.execPath, ...process.argv.slice(1)], {
|
|
80
|
+
reject: false,
|
|
81
|
+
stdio: 'inherit',
|
|
82
|
+
env: {
|
|
83
|
+
...process.env,
|
|
84
|
+
DVMI_AWS_VAULT_REEXEC: '1',
|
|
98
85
|
},
|
|
99
|
-
)
|
|
86
|
+
})
|
|
100
87
|
|
|
101
88
|
return child.exitCode ?? 1
|
|
102
89
|
} catch {
|
|
@@ -117,25 +104,15 @@ export async function reexecCurrentCommandWithAwsVaultProfile(profile, extraEnv
|
|
|
117
104
|
if (!profile) return null
|
|
118
105
|
|
|
119
106
|
try {
|
|
120
|
-
const child = await execa(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
'
|
|
126
|
-
|
|
127
|
-
...process.argv.slice(1),
|
|
128
|
-
],
|
|
129
|
-
{
|
|
130
|
-
reject: false,
|
|
131
|
-
stdio: 'inherit',
|
|
132
|
-
env: {
|
|
133
|
-
...process.env,
|
|
134
|
-
DVMI_AWS_VAULT_REEXEC: '1',
|
|
135
|
-
...extraEnv,
|
|
136
|
-
},
|
|
107
|
+
const child = await execa('aws-vault', ['exec', profile, '--', process.execPath, ...process.argv.slice(1)], {
|
|
108
|
+
reject: false,
|
|
109
|
+
stdio: 'inherit',
|
|
110
|
+
env: {
|
|
111
|
+
...process.env,
|
|
112
|
+
DVMI_AWS_VAULT_REEXEC: '1',
|
|
113
|
+
...extraEnv,
|
|
137
114
|
},
|
|
138
|
-
)
|
|
115
|
+
})
|
|
139
116
|
|
|
140
117
|
return child.exitCode ?? 1
|
|
141
118
|
} catch {
|
package/src/utils/banner.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import figlet from 'figlet'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
|
-
import {
|
|
3
|
+
import {BRAND_GRADIENT, animateGradientBanner, isColorEnabled} from './gradient.js'
|
|
4
4
|
|
|
5
5
|
// Brand colors
|
|
6
6
|
export const ORANGE = '#FF6B2B'
|
|
@@ -24,13 +24,11 @@ function figletAsync(text, opts) {
|
|
|
24
24
|
* @returns {Promise<void>}
|
|
25
25
|
*/
|
|
26
26
|
export async function printBanner() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
30
|
|
|
31
|
-
|
|
32
|
-
? chalk.hex(BLUE).bold(' Devvami Developer CLI')
|
|
33
|
-
: ' Devvami Developer CLI'
|
|
31
|
+
const tagline = isColorEnabled ? chalk.hex(BLUE).bold(' Devvami Developer CLI') : ' Devvami Developer CLI'
|
|
34
32
|
|
|
35
33
|
const separator = isColorEnabled
|
|
36
34
|
? chalk.hex(BLUE).dim('─'.repeat(Math.min(width, 60)))
|
package/src/utils/errors.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* Base CLI error with an actionable hint for the user.
|
|
3
3
|
*/
|
|
4
4
|
export class DvmiError extends Error {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
13
|
/** @type {string} */
|
|
14
14
|
this.hint = hint
|
|
15
15
|
/** @type {number} */
|
|
@@ -21,43 +21,39 @@ export class DvmiError extends Error {
|
|
|
21
21
|
* Validation error for invalid user input (exit code 2).
|
|
22
22
|
*/
|
|
23
23
|
export class ValidationError extends DvmiError {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} message
|
|
26
|
+
* @param {string} hint
|
|
27
|
+
*/
|
|
28
|
+
constructor(message, hint) {
|
|
29
|
+
super(message, hint, 2)
|
|
30
|
+
this.name = 'ValidationError'
|
|
31
|
+
// oclif reads this.oclif.exit to determine the process exit code
|
|
32
|
+
this.oclif = {exit: 2}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
49
|
-
this.name = 'AuthError'
|
|
50
|
-
}
|
|
51
|
-
}
|
|
36
|
+
/**
|
|
37
|
+
* Auth error for missing or expired authentication.
|
|
38
|
+
*/
|
|
39
|
+
export class AuthError extends DvmiError {
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} service - Service name (e.g. "GitHub", "AWS")
|
|
42
|
+
*/
|
|
43
|
+
constructor(service) {
|
|
44
|
+
super(`${service} authentication required`, `Run \`dvmi auth login\` to authenticate`, 1)
|
|
45
|
+
this.name = 'AuthError'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
52
48
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Format an error for display in the terminal.
|
|
51
|
+
* @param {Error} err
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function formatError(err) {
|
|
55
|
+
if (err instanceof DvmiError) {
|
|
56
|
+
return `Error: ${err.message}\nHint: ${err.hint}`
|
|
57
|
+
}
|
|
58
|
+
return `Error: ${err.message}`
|
|
59
|
+
}
|
package/src/utils/frontmatter.js
CHANGED
|
@@ -18,7 +18,7 @@ import yaml from 'js-yaml'
|
|
|
18
18
|
export function parseFrontmatter(content) {
|
|
19
19
|
const match = content.match(/^---\r?\n([\s\S]*?)---\r?\n?([\s\S]*)$/)
|
|
20
20
|
if (!match) {
|
|
21
|
-
return {
|
|
21
|
+
return {frontmatter: {}, body: content}
|
|
22
22
|
}
|
|
23
23
|
const rawYaml = match[1]
|
|
24
24
|
const body = match[2] ?? ''
|
|
@@ -28,9 +28,9 @@ export function parseFrontmatter(content) {
|
|
|
28
28
|
parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
29
29
|
? /** @type {Record<string, unknown>} */ (parsed)
|
|
30
30
|
: {}
|
|
31
|
-
return {
|
|
31
|
+
return {frontmatter, body}
|
|
32
32
|
} catch {
|
|
33
|
-
return {
|
|
33
|
+
return {frontmatter: {}, body: content}
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -47,6 +47,6 @@ export function serializeFrontmatter(frontmatter, body) {
|
|
|
47
47
|
if (!frontmatter || Object.keys(frontmatter).length === 0) {
|
|
48
48
|
return body
|
|
49
49
|
}
|
|
50
|
-
const yamlStr = yaml.dump(frontmatter, {
|
|
50
|
+
const yamlStr = yaml.dump(frontmatter, {lineWidth: -1}).trimEnd()
|
|
51
51
|
return `---\n${yamlStr}\n---\n${body}`
|
|
52
52
|
}
|
package/src/utils/gradient.js
CHANGED
|
@@ -12,9 +12,9 @@ import readline from 'node:readline'
|
|
|
12
12
|
|
|
13
13
|
/** @type {GradientStop[]} */
|
|
14
14
|
export const BRAND_GRADIENT = [
|
|
15
|
-
[0,
|
|
16
|
-
[0,
|
|
17
|
-
[100,
|
|
15
|
+
[0, 212, 255], // #00D4FF — ciano elettrico
|
|
16
|
+
[0, 100, 255], // #0064FF — blu vivido
|
|
17
|
+
[100, 0, 220], // #6400DC — indaco profondo
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -51,24 +51,26 @@ export function gradientText(text, stops, phase = 0) {
|
|
|
51
51
|
|
|
52
52
|
const segments = stops.length - 1
|
|
53
53
|
|
|
54
|
-
return chars
|
|
55
|
-
|
|
54
|
+
return chars
|
|
55
|
+
.map((char, i) => {
|
|
56
|
+
if (char === ' ') return char
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
// Normalise t in [0, 1] with phase shift
|
|
59
|
+
const t = (i / Math.max(len - 1, 1) + phase) % 1
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
const seg = Math.min(Math.floor(t * segments), segments - 1)
|
|
62
|
+
const localT = t * segments - seg
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
const [r1, g1, b1] = stops[seg]
|
|
65
|
+
const [r2, g2, b2] = stops[seg + 1]
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const r = Math.round(r1 + (r2 - r1) * localT)
|
|
68
|
+
const g = Math.round(g1 + (g2 - g1) * localT)
|
|
69
|
+
const b = Math.round(b1 + (b2 - b1) * localT)
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
return chalk.rgb(r, g, b)(char)
|
|
72
|
+
})
|
|
73
|
+
.join('')
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import open from 'open'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import {detectPlatform} from '../services/platform.js'
|
|
3
|
+
import {exec} from '../services/shell.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Open a URL in the default browser, using the platform-appropriate command.
|
|
@@ -8,7 +8,7 @@ import { exec } from '../services/shell.js'
|
|
|
8
8
|
* @returns {Promise<void>}
|
|
9
9
|
*/
|
|
10
10
|
export async function openBrowser(url) {
|
|
11
|
-
const {
|
|
11
|
+
const {platform, openCommand} = await detectPlatform()
|
|
12
12
|
|
|
13
13
|
if (platform === 'macos') {
|
|
14
14
|
await open(url)
|