tsoft-cli 3.12.3 → 3.13.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/CHANGELOG.md +29 -0
- package/README.md +184 -31
- package/locales/en.json +46 -8
- package/locales/tr.json +46 -8
- package/package.json +12 -7
- package/src/cli-settings.js +47 -0
- package/src/commands/config.js +38 -0
- package/src/commands/docs.js +23 -0
- package/src/commands/store.js +10 -3
- package/src/commands/theme-dev.js +9 -2
- package/src/commands/theme-init.js +3 -1
- package/src/commands/theme-section.js +21 -22
- package/src/commands/theme.js +25 -6
- package/src/commands/upgrade.js +116 -0
- package/src/index.js +180 -6
- package/src/theme-download.js +57 -51
- package/src/ui/command-suggestion.js +78 -0
- package/src/ui/completion.js +189 -0
- package/src/ui/help-renderer.js +9 -0
- package/src/ui/table.js +122 -0
- package/src/ui/workspace-hint.js +78 -0
- package/src/upgrade-plan.js +43 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { t } from '../i18n.js';
|
|
2
|
+
|
|
3
|
+
const MAX_DISTANCE = 3;
|
|
4
|
+
|
|
5
|
+
function editDistance(a, b) {
|
|
6
|
+
if (Math.abs(a.length - b.length) > MAX_DISTANCE) {
|
|
7
|
+
return Math.max(a.length, b.length);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const d = [];
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i <= a.length; i++) {
|
|
13
|
+
d[i] = [i];
|
|
14
|
+
}
|
|
15
|
+
for (let j = 0; j <= b.length; j++) {
|
|
16
|
+
d[0][j] = j;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (let i = 1; i <= a.length; i++) {
|
|
20
|
+
for (let j = 1; j <= b.length; j++) {
|
|
21
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
22
|
+
|
|
23
|
+
d[i][j] = Math.min(
|
|
24
|
+
d[i - 1][j] + 1,
|
|
25
|
+
d[i][j - 1] + 1,
|
|
26
|
+
d[i - 1][j - 1] + cost
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
30
|
+
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return d[a.length][b.length];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function suggestCommand(word, candidates) {
|
|
39
|
+
const typed = word.toLowerCase();
|
|
40
|
+
let best = null;
|
|
41
|
+
let bestDistance = MAX_DISTANCE + 1;
|
|
42
|
+
|
|
43
|
+
for (const candidate of candidates) {
|
|
44
|
+
const name = candidate.toLowerCase();
|
|
45
|
+
|
|
46
|
+
if (name === typed) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const isPrefix = name.startsWith(typed) && typed.length > 0;
|
|
51
|
+
const distance = isPrefix ? 0 : editDistance(typed, name);
|
|
52
|
+
|
|
53
|
+
if (distance <= MAX_DISTANCE && distance < bestDistance) {
|
|
54
|
+
best = candidate;
|
|
55
|
+
bestDistance = distance;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return best;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function formatUnknownCommand(word, candidates, parentPath = null) {
|
|
63
|
+
const suggestion = suggestCommand(word, candidates);
|
|
64
|
+
const lines = [
|
|
65
|
+
parentPath
|
|
66
|
+
? t('error.unknown_subcommand', { parent: parentPath, command: word })
|
|
67
|
+
: t('error.unknown_command', { command: word }),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
if (suggestion) {
|
|
71
|
+
const full = parentPath ? `${parentPath} ${suggestion}` : suggestion;
|
|
72
|
+
lines.push('', t('error.did_you_mean', { suggestion: full }));
|
|
73
|
+
} else {
|
|
74
|
+
lines.push('', t('error.see_help'));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
export const SUPPORTED_SHELLS = ['bash', 'zsh', 'fish'];
|
|
2
|
+
|
|
3
|
+
export function collectCommandTree(command) {
|
|
4
|
+
const children = [];
|
|
5
|
+
|
|
6
|
+
for (const sub of command.commands) {
|
|
7
|
+
if (sub._hidden) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
children.push({
|
|
12
|
+
name: sub.name(),
|
|
13
|
+
aliases: sub.aliases().filter(Boolean),
|
|
14
|
+
description: sub.description() ?? '',
|
|
15
|
+
children: collectCommandTree(sub).children,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { name: command.name(), children };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function flatten(tree) {
|
|
23
|
+
const paths = [];
|
|
24
|
+
|
|
25
|
+
const walk = (node, prefix) => {
|
|
26
|
+
for (const child of node.children) {
|
|
27
|
+
for (const name of [child.name, ...child.aliases]) {
|
|
28
|
+
const path = [...prefix, name];
|
|
29
|
+
paths.push({ path, description: child.description });
|
|
30
|
+
}
|
|
31
|
+
walk(child, [...prefix, child.name]);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
walk(tree, []);
|
|
36
|
+
|
|
37
|
+
return paths;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function namesByParent(tree) {
|
|
41
|
+
const groups = new Map();
|
|
42
|
+
|
|
43
|
+
const walk = (node, prefix) => {
|
|
44
|
+
const key = prefix.join(' ');
|
|
45
|
+
const names = [];
|
|
46
|
+
|
|
47
|
+
for (const child of node.children) {
|
|
48
|
+
names.push(child.name, ...child.aliases);
|
|
49
|
+
walk(child, [...prefix, child.name]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (names.length) {
|
|
53
|
+
groups.set(key, names);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
walk(tree, []);
|
|
58
|
+
|
|
59
|
+
return groups;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renderBash(tree) {
|
|
63
|
+
const groups = namesByParent(tree);
|
|
64
|
+
const cases = [];
|
|
65
|
+
|
|
66
|
+
for (const [parent, names] of groups) {
|
|
67
|
+
const pattern = parent === '' ? '' : parent;
|
|
68
|
+
cases.push(` "${pattern}") opts="${names.join(' ')}" ;;`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return `# tsoft completion for bash
|
|
72
|
+
# Install: tsoft completion bash > /etc/bash_completion.d/tsoft
|
|
73
|
+
# or: echo 'source <(tsoft completion bash)' >> ~/.bashrc
|
|
74
|
+
|
|
75
|
+
_tsoft_completions() {
|
|
76
|
+
local cur words path opts
|
|
77
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
78
|
+
words=("\${COMP_WORDS[@]:1:COMP_CWORD-1}")
|
|
79
|
+
path=""
|
|
80
|
+
|
|
81
|
+
for word in "\${words[@]}"; do
|
|
82
|
+
case "$word" in
|
|
83
|
+
-*) continue ;;
|
|
84
|
+
esac
|
|
85
|
+
if [ -z "$path" ]; then path="$word"; else path="$path $word"; fi
|
|
86
|
+
done
|
|
87
|
+
|
|
88
|
+
case "$path" in
|
|
89
|
+
${cases.join('\n')}
|
|
90
|
+
*) opts="" ;;
|
|
91
|
+
esac
|
|
92
|
+
|
|
93
|
+
if [ -z "$opts" ]; then
|
|
94
|
+
COMPREPLY=()
|
|
95
|
+
return 0
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
|
|
99
|
+
return 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
complete -F _tsoft_completions tsoft
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderZsh(tree) {
|
|
107
|
+
const groups = namesByParent(tree);
|
|
108
|
+
const cases = [];
|
|
109
|
+
|
|
110
|
+
for (const [parent, names] of groups) {
|
|
111
|
+
cases.push(` "${parent}") opts=(${names.join(' ')}) ;;`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return `#compdef tsoft
|
|
115
|
+
# tsoft completion for zsh
|
|
116
|
+
# Install: tsoft completion zsh > "\${fpath[1]}/_tsoft"
|
|
117
|
+
# or: echo 'source <(tsoft completion zsh)' >> ~/.zshrc
|
|
118
|
+
|
|
119
|
+
_tsoft() {
|
|
120
|
+
local -a opts
|
|
121
|
+
local path=""
|
|
122
|
+
local -a words_seen
|
|
123
|
+
words_seen=(\${words[2,CURRENT-1]})
|
|
124
|
+
|
|
125
|
+
for word in $words_seen; do
|
|
126
|
+
case "$word" in
|
|
127
|
+
-*) continue ;;
|
|
128
|
+
esac
|
|
129
|
+
if [[ -z "$path" ]]; then path="$word"; else path="$path $word"; fi
|
|
130
|
+
done
|
|
131
|
+
|
|
132
|
+
case "$path" in
|
|
133
|
+
${cases.join('\n')}
|
|
134
|
+
*) opts=() ;;
|
|
135
|
+
esac
|
|
136
|
+
|
|
137
|
+
if (( \${#opts} )); then
|
|
138
|
+
_describe 'command' opts
|
|
139
|
+
else
|
|
140
|
+
_files
|
|
141
|
+
fi
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
compdef _tsoft tsoft
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function renderFish(tree) {
|
|
149
|
+
const lines = [
|
|
150
|
+
'# tsoft completion for fish',
|
|
151
|
+
'# Install: tsoft completion fish > ~/.config/fish/completions/tsoft.fish',
|
|
152
|
+
'',
|
|
153
|
+
'function __tsoft_path',
|
|
154
|
+
' set -l tokens (commandline -opc)',
|
|
155
|
+
' set -l parts',
|
|
156
|
+
' for token in $tokens[2..-1]',
|
|
157
|
+
' string match -q -- "-*" $token; and continue',
|
|
158
|
+
' set -a parts $token',
|
|
159
|
+
' end',
|
|
160
|
+
' string join " " $parts',
|
|
161
|
+
'end',
|
|
162
|
+
'',
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
for (const { path, description } of flatten(tree)) {
|
|
166
|
+
const parent = path.slice(0, -1).join(' ');
|
|
167
|
+
const name = path[path.length - 1];
|
|
168
|
+
const desc = description.replace(/'/g, "\\'");
|
|
169
|
+
|
|
170
|
+
lines.push(
|
|
171
|
+
`complete -c tsoft -f -n 'test (__tsoft_path) = "${parent}"' -a '${name}' -d '${desc}'`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return lines.join('\n') + '\n';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function renderCompletion(shell, tree) {
|
|
179
|
+
switch (shell) {
|
|
180
|
+
case 'bash':
|
|
181
|
+
return renderBash(tree);
|
|
182
|
+
case 'zsh':
|
|
183
|
+
return renderZsh(tree);
|
|
184
|
+
case 'fish':
|
|
185
|
+
return renderFish(tree);
|
|
186
|
+
default:
|
|
187
|
+
throw new Error(`Unsupported shell: ${shell}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
package/src/ui/help-renderer.js
CHANGED
|
@@ -63,6 +63,15 @@ const COMMAND_GROUPS = [
|
|
|
63
63
|
{ display: 'whoami', descKey: 'help.cmd.whoami' },
|
|
64
64
|
{ display: 'org switch', descKey: 'help.cmd.org_switch' },
|
|
65
65
|
]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
groupKey: 'help.group.help',
|
|
69
|
+
commands: [
|
|
70
|
+
{ display: 'docs', descKey: 'help.cmd.docs' },
|
|
71
|
+
{ display: 'completion', descKey: 'help.cmd.completion' },
|
|
72
|
+
{ display: 'upgrade', descKey: 'help.cmd.upgrade' },
|
|
73
|
+
{ display: 'config autoupgrade', descKey: 'help.cmd.config' },
|
|
74
|
+
]
|
|
66
75
|
}
|
|
67
76
|
];
|
|
68
77
|
|
package/src/ui/table.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import stringWidth from 'string-width';
|
|
3
|
+
|
|
4
|
+
const GAP = 2;
|
|
5
|
+
const ELLIPSIS = '…';
|
|
6
|
+
const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
|
|
7
|
+
const RESET = '\u001b[39m';
|
|
8
|
+
const MIN_COLUMN = 6;
|
|
9
|
+
|
|
10
|
+
function truncateVisible(cell, limit) {
|
|
11
|
+
if (limit <= 0) {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (stringWidth(cell) <= limit) {
|
|
16
|
+
return cell;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const target = limit - 1;
|
|
20
|
+
let out = '';
|
|
21
|
+
let width = 0;
|
|
22
|
+
let index = 0;
|
|
23
|
+
|
|
24
|
+
while (index < cell.length) {
|
|
25
|
+
ANSI_PATTERN.lastIndex = index;
|
|
26
|
+
const match = ANSI_PATTERN.exec(cell);
|
|
27
|
+
|
|
28
|
+
if (match && match.index === index) {
|
|
29
|
+
out += match[0];
|
|
30
|
+
index += match[0].length;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const char = cell[index];
|
|
35
|
+
const charWidth = stringWidth(char);
|
|
36
|
+
|
|
37
|
+
if (width + charWidth > target) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
out += char;
|
|
42
|
+
width += charWidth;
|
|
43
|
+
index += 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const closer = hasOpenStyle(out) ? RESET : '';
|
|
47
|
+
|
|
48
|
+
return `${out}${ELLIPSIS}${closer}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hasOpenStyle(text) {
|
|
52
|
+
const codes = text.match(ANSI_PATTERN) ?? [];
|
|
53
|
+
|
|
54
|
+
return codes.length > 0 && codes[codes.length - 1] !== RESET;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function columnWidths(rows, columnCount) {
|
|
58
|
+
const widths = new Array(columnCount).fill(0);
|
|
59
|
+
|
|
60
|
+
for (const row of rows) {
|
|
61
|
+
for (let i = 0; i < columnCount; i++) {
|
|
62
|
+
widths[i] = Math.max(widths[i], stringWidth(row[i] ?? ''));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return widths;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fitToWidth(widths, available) {
|
|
70
|
+
const total = widths.reduce((sum, w) => sum + w, 0) + GAP * (widths.length - 1);
|
|
71
|
+
|
|
72
|
+
if (total <= available) {
|
|
73
|
+
return widths;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fitted = [...widths];
|
|
77
|
+
let overflow = total - available;
|
|
78
|
+
|
|
79
|
+
while (overflow > 0) {
|
|
80
|
+
const widest = fitted.reduce(
|
|
81
|
+
(best, w, i) => (w > fitted[best] ? i : best),
|
|
82
|
+
0
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (fitted[widest] <= MIN_COLUMN) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fitted[widest] -= 1;
|
|
90
|
+
overflow -= 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return fitted;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function renderTable({ head = null, rows, width = null, indent = '' }) {
|
|
97
|
+
if (!rows.length) {
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const body = head ? [head.map((h) => chalk.dim(h)), ...rows] : rows;
|
|
102
|
+
const columnCount = Math.max(...body.map((row) => row.length));
|
|
103
|
+
const available = (width ?? process.stdout.columns ?? 80) - indent.length;
|
|
104
|
+
const widths = fitToWidth(columnWidths(body, columnCount), available);
|
|
105
|
+
|
|
106
|
+
return body
|
|
107
|
+
.map((row) => {
|
|
108
|
+
const cells = [];
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < columnCount; i++) {
|
|
111
|
+
const cell = truncateVisible(row[i] ?? '', widths[i]);
|
|
112
|
+
const padding = i === columnCount - 1
|
|
113
|
+
? ''
|
|
114
|
+
: ' '.repeat(Math.max(0, widths[i] - stringWidth(cell) + GAP));
|
|
115
|
+
|
|
116
|
+
cells.push(cell + padding);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (indent + cells.join('')).trimEnd();
|
|
120
|
+
})
|
|
121
|
+
.join('\n');
|
|
122
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { normalizeStoreDomain } from '../storage.js';
|
|
4
|
+
import { t } from '../i18n.js';
|
|
5
|
+
|
|
6
|
+
const THEME_CONTEXT_COMMANDS = new Set([
|
|
7
|
+
'theme dev',
|
|
8
|
+
'theme push',
|
|
9
|
+
'theme publish',
|
|
10
|
+
'theme submit',
|
|
11
|
+
'theme section list',
|
|
12
|
+
'theme section add',
|
|
13
|
+
'theme section create',
|
|
14
|
+
'theme section fork',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export function commandUsesThemeContext(commandPath) {
|
|
18
|
+
return THEME_CONTEXT_COMMANDS.has(commandPath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cdTarget(store, theme, workspaceRoot) {
|
|
22
|
+
const relative = path.join(normalizeStoreDomain(store), theme);
|
|
23
|
+
|
|
24
|
+
return workspaceRoot ? path.join(workspaceRoot, relative) : relative;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function standingInTheme({ context, activeStore, activeTheme }) {
|
|
28
|
+
return Boolean(
|
|
29
|
+
context?.theme &&
|
|
30
|
+
context.store === activeStore &&
|
|
31
|
+
context.theme === activeTheme
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildWorkspaceHint({ context, activeStore, activeTheme, workspaceRoot = null }) {
|
|
36
|
+
if (!activeStore || !activeTheme) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (standingInTheme({ context, activeStore, activeTheme })) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
store: activeStore,
|
|
46
|
+
theme: activeTheme,
|
|
47
|
+
cd: cdTarget(activeStore, activeTheme, workspaceRoot),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildThemeMissingHint({ context, activeStore, activeTheme, workspaceRoot = null }) {
|
|
52
|
+
if (standingInTheme({ context, activeStore, activeTheme })) {
|
|
53
|
+
return { reason: 'not-downloaded', store: activeStore, theme: activeTheme, cd: null };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
reason: 'wrong-directory',
|
|
58
|
+
store: activeStore,
|
|
59
|
+
theme: activeTheme,
|
|
60
|
+
cd: activeStore && activeTheme ? cdTarget(activeStore, activeTheme, workspaceRoot) : null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function formatWorkspaceHint(hint) {
|
|
65
|
+
return [
|
|
66
|
+
chalk.yellow(t('context.outside_theme', { store: hint.store, theme: hint.theme })),
|
|
67
|
+
chalk.gray(' ' + t('context.cd_hint')),
|
|
68
|
+
chalk.white(` cd ${hint.cd}`),
|
|
69
|
+
].join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function formatThemeMissingHint(hint, themePath) {
|
|
73
|
+
if (hint.reason === 'not-downloaded') {
|
|
74
|
+
return t('dev.theme_not_downloaded', { path: themePath });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return t('dev.theme_wrong_directory', { path: themePath, cd: hint.cd });
|
|
78
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
|
|
3
|
+
export const PACKAGE_NAME = 'tsoft-cli';
|
|
4
|
+
|
|
5
|
+
export function detectInstallMethod({ modulePath, env = process.env }) {
|
|
6
|
+
const normalized = modulePath.replace(/\\/g, '/');
|
|
7
|
+
|
|
8
|
+
if (normalized.includes('/_npx/') || env.npm_command === 'exec') {
|
|
9
|
+
return 'npx';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (normalized.includes('/.volta/') || env.VOLTA_HOME) {
|
|
13
|
+
return 'volta';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (normalized.includes('/lib/node_modules/')) {
|
|
17
|
+
return 'npm-global';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return 'unknown';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function upgradeCommandFor(method) {
|
|
24
|
+
switch (method) {
|
|
25
|
+
case 'npm-global':
|
|
26
|
+
return `npm install -g ${PACKAGE_NAME}@latest`;
|
|
27
|
+
case 'volta':
|
|
28
|
+
return `volta install ${PACKAGE_NAME}@latest`;
|
|
29
|
+
default:
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isNewerAvailable(installed, published) {
|
|
35
|
+
const current = semver.valid(installed);
|
|
36
|
+
const latest = semver.valid(published);
|
|
37
|
+
|
|
38
|
+
if (!current || !latest) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return semver.gt(latest, current);
|
|
43
|
+
}
|