tsoft-cli 3.12.4 → 3.13.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.
@@ -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
+ }
@@ -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
 
@@ -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,82 @@
1
+ import path from 'path';
2
+ import chalk from 'chalk';
3
+ import { normalizeStoreDomain, isThemeUuid } 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 (isThemeUuid(activeTheme)) {
41
+ return null;
42
+ }
43
+
44
+ if (standingInTheme({ context, activeStore, activeTheme })) {
45
+ return null;
46
+ }
47
+
48
+ return {
49
+ store: activeStore,
50
+ theme: activeTheme,
51
+ cd: cdTarget(activeStore, activeTheme, workspaceRoot),
52
+ };
53
+ }
54
+
55
+ export function buildThemeMissingHint({ context, activeStore, activeTheme, workspaceRoot = null }) {
56
+ if (isThemeUuid(activeTheme) || standingInTheme({ context, activeStore, activeTheme })) {
57
+ return { reason: 'not-downloaded', store: activeStore, theme: activeTheme, cd: null };
58
+ }
59
+
60
+ return {
61
+ reason: 'wrong-directory',
62
+ store: activeStore,
63
+ theme: activeTheme,
64
+ cd: activeStore && activeTheme ? cdTarget(activeStore, activeTheme, workspaceRoot) : null,
65
+ };
66
+ }
67
+
68
+ export function formatWorkspaceHint(hint) {
69
+ return [
70
+ chalk.yellow(t('context.outside_theme', { store: hint.store, theme: hint.theme })),
71
+ chalk.gray(' ' + t('context.cd_hint')),
72
+ chalk.white(` cd ${hint.cd}`),
73
+ ].join('\n');
74
+ }
75
+
76
+ export function formatThemeMissingHint(hint, themePath) {
77
+ if (hint.reason === 'not-downloaded') {
78
+ return t('dev.theme_not_downloaded', { path: themePath });
79
+ }
80
+
81
+ return t('dev.theme_wrong_directory', { path: themePath, cd: hint.cd });
82
+ }
@@ -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
+ }