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.
- 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/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 +82 -0
- package/src/upgrade-plan.js +43 -0
package/src/commands/store.js
CHANGED
|
@@ -3,6 +3,7 @@ import { listKnownStores, setActiveStore, getActiveTheme } from '../storage.js';
|
|
|
3
3
|
import { brandedSelect, brandedSearch, showSuccess, showError, showInfo } from '../ui/prompt-wrapper.js';
|
|
4
4
|
import { isJsonMode, jsonOut } from '../output-mode.js';
|
|
5
5
|
import { t } from '../i18n.js';
|
|
6
|
+
import { renderTable } from '../ui/table.js';
|
|
6
7
|
|
|
7
8
|
const SEARCH_THRESHOLD = 7;
|
|
8
9
|
|
|
@@ -43,11 +44,17 @@ export async function storeListCommand() {
|
|
|
43
44
|
|
|
44
45
|
console.log(chalk.bold.cyan(t('store.count', { count: stores.length }) + '\n'));
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
const rows = stores.map((entry) => {
|
|
47
48
|
const marker = entry.active ? chalk.green('●') : chalk.gray('○');
|
|
48
49
|
const name = entry.active ? chalk.bold.green(entry.store) : chalk.white(entry.store);
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
|
|
51
|
+
return [`${marker} ${name}`, sessionLabel(entry)];
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log(renderTable({
|
|
55
|
+
head: [t('table.head.store'), t('table.head.session')],
|
|
56
|
+
rows,
|
|
57
|
+
}));
|
|
51
58
|
|
|
52
59
|
console.log();
|
|
53
60
|
}
|
|
@@ -4,7 +4,8 @@ import path from 'path';
|
|
|
4
4
|
import chokidar from 'chokidar';
|
|
5
5
|
import {createApiClient} from '../api-client.js';
|
|
6
6
|
import open from 'open';
|
|
7
|
-
import {normalizeStoreDomain, slugify, setActiveTheme, getActiveTheme, getActiveStore, getWorkspaceRoot} from '../storage.js';
|
|
7
|
+
import {normalizeStoreDomain, slugify, setActiveTheme, getActiveTheme, getActiveStore, getWorkspaceRoot, getWorkspaceContext} from '../storage.js';
|
|
8
|
+
import {buildThemeMissingHint, formatThemeMissingHint} from '../ui/workspace-hint.js';
|
|
8
9
|
import {decorateThemeName} from '../ui/theme-choice.js';
|
|
9
10
|
import {brandedConfirm, brandedSelect, showWarning, showError, showInfo} from '../ui/prompt-wrapper.js';
|
|
10
11
|
import {themeSectionMenuCommand} from './theme-section.js';
|
|
@@ -471,7 +472,13 @@ export async function themeDevCommand(themeName) {
|
|
|
471
472
|
|
|
472
473
|
const exists = await fs.access(themePath).then(() => true).catch(() => false);
|
|
473
474
|
if (!exists) {
|
|
474
|
-
|
|
475
|
+
const missing = buildThemeMissingHint({
|
|
476
|
+
context: getWorkspaceContext(),
|
|
477
|
+
activeStore: store,
|
|
478
|
+
activeTheme: themeDir,
|
|
479
|
+
workspaceRoot: getWorkspaceRoot(),
|
|
480
|
+
});
|
|
481
|
+
showError(formatThemeMissingHint(missing, themePath));
|
|
475
482
|
return;
|
|
476
483
|
}
|
|
477
484
|
|
|
@@ -130,7 +130,9 @@ export async function themeInitCommand(fromSlug) {
|
|
|
130
130
|
console.log(chalk.cyan(' ' + t('init.source_label') + ' ') + chalk.white(`${forkedData.source.name} v${forkedData.source.version}`));
|
|
131
131
|
console.log(chalk.cyan(' ' + t('init.location_label') + ' ') + chalk.white(`${storeSlug}/${forkedSlug}/`));
|
|
132
132
|
console.log();
|
|
133
|
-
console.log(chalk.gray(t('init.dev_tip'
|
|
133
|
+
console.log(chalk.gray(t('init.dev_tip')));
|
|
134
|
+
console.log(chalk.white(` cd ${storeSlug}/${forkedSlug}`));
|
|
135
|
+
console.log(chalk.white(' tsoft theme dev'));
|
|
134
136
|
console.log();
|
|
135
137
|
|
|
136
138
|
} catch (error) {
|
|
@@ -7,6 +7,7 @@ import {getActiveTheme, getThemeUuidBySlug, normalizeStoreDomain, getActiveStore
|
|
|
7
7
|
import {brandedConfirm, brandedSelect, brandedSearch, brandedInput, showWarning, showSuccess, showInfo} from '../ui/prompt-wrapper.js';
|
|
8
8
|
import { t } from '../i18n.js';
|
|
9
9
|
import {mergeThemeArchive} from '../theme-download.js';
|
|
10
|
+
import {renderTable} from '../ui/table.js';
|
|
10
11
|
|
|
11
12
|
const SECTION_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
12
13
|
|
|
@@ -52,30 +53,28 @@ export async function themeSectionListCommand() {
|
|
|
52
53
|
console.log(chalk.bold.cyan(t('section.list.count', { count: blocks.length }) + '\n'));
|
|
53
54
|
|
|
54
55
|
blocks.forEach((block) => {
|
|
55
|
-
|
|
56
|
-
console.log(chalk.bold.cyan(`📦 ${block.name}`));
|
|
57
|
-
console.log(chalk.gray(` ${t('section.list.id_label')} ${block.id}`));
|
|
58
|
-
console.log(chalk.gray(` ${t('section.list.folder_label')} ${block.folder}`));
|
|
56
|
+
console.log(chalk.bold.cyan(`📦 ${block.name}`) + chalk.gray(` ${block.folder}`));
|
|
59
57
|
|
|
60
|
-
// Section'lar
|
|
61
58
|
if (block.default_sections && block.default_sections.length > 0) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
59
|
+
const rows = block.default_sections.map((section) => [
|
|
60
|
+
chalk.white(section.name),
|
|
61
|
+
chalk.gray(`v${section.version}`),
|
|
62
|
+
chalk.gray(String(section.id)),
|
|
63
|
+
chalk.gray(section.content),
|
|
64
|
+
chalk.gray(section.meta?.description ?? ''),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
console.log(renderTable({
|
|
68
|
+
head: [
|
|
69
|
+
t('table.head.section'),
|
|
70
|
+
t('table.head.version'),
|
|
71
|
+
t('table.head.id'),
|
|
72
|
+
t('table.head.file'),
|
|
73
|
+
t('table.head.description'),
|
|
74
|
+
],
|
|
75
|
+
rows,
|
|
76
|
+
indent: ' ',
|
|
77
|
+
}));
|
|
79
78
|
} else {
|
|
80
79
|
console.log(chalk.red(' ' + t('section.list.no_sections')));
|
|
81
80
|
}
|
package/src/commands/theme.js
CHANGED
|
@@ -7,6 +7,7 @@ import {normalizeStoreDomain, slugify, setActiveTheme, getActiveTheme, getActive
|
|
|
7
7
|
import {brandedConfirm, brandedSelect, brandedSearch, showWarning} from '../ui/prompt-wrapper.js';
|
|
8
8
|
import {fetchAllThemes} from '../theme-list.js';
|
|
9
9
|
import {themeLabels, decorateThemeName} from '../ui/theme-choice.js';
|
|
10
|
+
import {renderTable} from '../ui/table.js';
|
|
10
11
|
import { isJsonMode, jsonOut } from '../output-mode.js';
|
|
11
12
|
import { mapError } from '../errors/error-mapper.js';
|
|
12
13
|
import { t } from '../i18n.js';
|
|
@@ -48,18 +49,32 @@ export async function themeListCommand() {
|
|
|
48
49
|
|
|
49
50
|
const localTheme = await getActiveTheme();
|
|
50
51
|
|
|
51
|
-
themes.
|
|
52
|
+
const rows = themes.map((theme) => {
|
|
52
53
|
const labels = themeLabels(theme, localTheme);
|
|
53
54
|
const indicator = labels.includes('selected')
|
|
54
55
|
? chalk.cyan('●')
|
|
55
56
|
: (labels.includes('published') ? chalk.green('●') : chalk.gray('○'));
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
const name = labels.includes('selected')
|
|
58
|
+
? chalk.bold.cyan(theme.name)
|
|
59
|
+
: chalk.white(theme.name);
|
|
60
|
+
const marks = labels.map(label => label === 'selected'
|
|
61
|
+
? chalk.cyan(t('theme.mark.selected'))
|
|
62
|
+
: chalk.green(t('theme.mark.published')));
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
`${indicator} ${name}`,
|
|
66
|
+
chalk.gray(`v${theme.version}`),
|
|
67
|
+
chalk.gray(theme.theme_folder || theme.alias),
|
|
68
|
+
marks.join(' '),
|
|
69
|
+
];
|
|
61
70
|
});
|
|
62
71
|
|
|
72
|
+
console.log(renderTable({
|
|
73
|
+
head: [t('table.head.theme'), t('table.head.version'), t('table.head.folder'), t('table.head.status')],
|
|
74
|
+
rows,
|
|
75
|
+
}));
|
|
76
|
+
console.log();
|
|
77
|
+
|
|
63
78
|
} catch (error) {
|
|
64
79
|
if (isJsonMode()) {
|
|
65
80
|
const mapped = mapError(error, { command: 'theme list' });
|
|
@@ -301,6 +316,7 @@ export async function themeCreateCommand(themeName) {
|
|
|
301
316
|
console.log(chalk.bold.green(t('theme.create_success')));
|
|
302
317
|
console.log(chalk.gray(' ' + t('theme.create_location', { path: `${storeSlug}/${themeSlug}/` }) + '\n'));
|
|
303
318
|
console.log(chalk.cyan(t('theme.dev_start_tip')));
|
|
319
|
+
console.log(chalk.white(` cd ${storeSlug}/${themeSlug}`));
|
|
304
320
|
console.log(chalk.white(' tsoft theme dev\n'));
|
|
305
321
|
|
|
306
322
|
} catch (error) {
|
|
@@ -385,6 +401,9 @@ export async function themePullCommand() {
|
|
|
385
401
|
|
|
386
402
|
console.log(chalk.bold.green(t('theme.pull.success')));
|
|
387
403
|
console.log(chalk.gray(' ' + t('theme.create_location', { path: `${storeSlug}/${themeSlug}/` }) + '\n'));
|
|
404
|
+
console.log(chalk.cyan(t('theme.dev_start_tip')));
|
|
405
|
+
console.log(chalk.white(` cd ${storeSlug}/${themeSlug}`));
|
|
406
|
+
console.log(chalk.white(' tsoft theme dev\n'));
|
|
388
407
|
} catch (error) {
|
|
389
408
|
console.error(chalk.red('\n❌ Hata: ') + error.message);
|
|
390
409
|
if (error.response?.status === 401) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import axios from 'axios';
|
|
7
|
+
import { isJsonMode, jsonOut } from '../output-mode.js';
|
|
8
|
+
import { showError, showInfo, showSuccess } from '../ui/prompt-wrapper.js';
|
|
9
|
+
import { t } from '../i18n.js';
|
|
10
|
+
import {
|
|
11
|
+
PACKAGE_NAME,
|
|
12
|
+
detectInstallMethod,
|
|
13
|
+
upgradeCommandFor,
|
|
14
|
+
isNewerAvailable,
|
|
15
|
+
} from '../upgrade-plan.js';
|
|
16
|
+
|
|
17
|
+
const run = promisify(execFile);
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
20
|
+
const CHECK_TIMEOUT_MS = 3000;
|
|
21
|
+
|
|
22
|
+
function installedVersion() {
|
|
23
|
+
return require('../../package.json').version;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function modulePath() {
|
|
27
|
+
return fileURLToPath(import.meta.url);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function fetchLatestVersion(timeout = CHECK_TIMEOUT_MS) {
|
|
31
|
+
try {
|
|
32
|
+
const { data } = await axios.get(REGISTRY_URL, { timeout });
|
|
33
|
+
|
|
34
|
+
return typeof data?.version === 'string' ? data.version : null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function upgradeCommand() {
|
|
41
|
+
const current = installedVersion();
|
|
42
|
+
const method = detectInstallMethod({ modulePath: modulePath() });
|
|
43
|
+
const command = upgradeCommandFor(method);
|
|
44
|
+
|
|
45
|
+
if (!isJsonMode()) {
|
|
46
|
+
console.log(chalk.cyan(t('upgrade.checking')));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const latest = await fetchLatestVersion();
|
|
50
|
+
|
|
51
|
+
if (!latest) {
|
|
52
|
+
if (isJsonMode()) {
|
|
53
|
+
jsonOut({ status: 'error', command: 'upgrade', error: { message: t('upgrade.check_failed'), hint: null } });
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
showError(t('upgrade.check_failed'));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!isNewerAvailable(current, latest)) {
|
|
61
|
+
if (isJsonMode()) {
|
|
62
|
+
jsonOut({ status: 'ok', command: 'upgrade', current, latest, upToDate: true });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
showSuccess(t('upgrade.current', { version: current }));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (isJsonMode()) {
|
|
70
|
+
jsonOut({ status: 'ok', command: 'upgrade', current, latest, upToDate: false, installMethod: method, command_hint: command });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(chalk.yellow(t('upgrade.available', { current, latest })));
|
|
75
|
+
console.log();
|
|
76
|
+
|
|
77
|
+
if (!command) {
|
|
78
|
+
showInfo(method === 'npx' ? t('upgrade.npx_notice') : t('upgrade.unknown_install'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(chalk.gray(t('upgrade.running', { command })));
|
|
83
|
+
console.log();
|
|
84
|
+
|
|
85
|
+
const [file, ...args] = command.split(' ');
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const { stdout } = await run(file, args, { timeout: 120000 });
|
|
89
|
+
|
|
90
|
+
if (stdout.trim()) {
|
|
91
|
+
console.log(chalk.gray(stdout.trim()));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
showSuccess(t('upgrade.success', { version: latest }));
|
|
95
|
+
} catch (error) {
|
|
96
|
+
showError(t('upgrade.failed', { error: error.stderr?.trim() || error.message }));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function buildUpgradeNotice() {
|
|
102
|
+
const current = installedVersion();
|
|
103
|
+
const latest = await fetchLatestVersion();
|
|
104
|
+
|
|
105
|
+
if (!isNewerAvailable(current, latest)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const method = detectInstallMethod({ modulePath: modulePath() });
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
current,
|
|
113
|
+
latest,
|
|
114
|
+
command: upgradeCommandFor(method) ?? `npm install -g ${PACKAGE_NAME}@latest`,
|
|
115
|
+
};
|
|
116
|
+
}
|
package/src/index.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import { setJsonMode, setVerboseMode } from './output-mode.js';
|
|
7
|
-
import { initLocale } from './i18n.js';
|
|
6
|
+
import { setJsonMode, setVerboseMode, isJsonMode, jsonOut } from './output-mode.js';
|
|
7
|
+
import { initLocale, t } from './i18n.js';
|
|
8
8
|
import { renderBrandedHelp } from './ui/help-renderer.js';
|
|
9
9
|
import { loginCommand } from './commands/login.js';
|
|
10
10
|
import { logoutCommand } from './commands/logout.js';
|
|
@@ -18,7 +18,15 @@ import { themeSubmitCommand } from './commands/theme-submit.js';
|
|
|
18
18
|
import { themeInitCommand } from './commands/theme-init.js';
|
|
19
19
|
import { orgSwitchCommand } from './commands/org-switch.js';
|
|
20
20
|
import { storeListCommand, storeUseCommand } from './commands/store.js';
|
|
21
|
-
import {
|
|
21
|
+
import { docsCommand } from './commands/docs.js';
|
|
22
|
+
import { upgradeCommand, buildUpgradeNotice } from './commands/upgrade.js';
|
|
23
|
+
import { configAutoupgradeCommand } from './commands/config.js';
|
|
24
|
+
import { getAutoupgrade, getLastCheck, setLastCheck } from './cli-settings.js';
|
|
25
|
+
import { setStoreOverride, applyWorkspaceContext, getWorkspaceContext, getWorkspaceRoot, getActiveStore, getActiveTheme } from './storage.js';
|
|
26
|
+
import { buildWorkspaceHint, formatWorkspaceHint, commandUsesThemeContext } from './ui/workspace-hint.js';
|
|
27
|
+
import { suggestCommand, formatUnknownCommand } from './ui/command-suggestion.js';
|
|
28
|
+
import { collectCommandTree, renderCompletion, SUPPORTED_SHELLS } from './ui/completion.js';
|
|
29
|
+
import { showError, brandedConfirm } from './ui/prompt-wrapper.js';
|
|
22
30
|
|
|
23
31
|
const require = createRequire(import.meta.url);
|
|
24
32
|
const { version } = require('../package.json');
|
|
@@ -38,7 +46,7 @@ program.option('--verbose', 'Verbose debug output');
|
|
|
38
46
|
program.option('--store <domain>', 'Run this command against a specific store');
|
|
39
47
|
|
|
40
48
|
// preAction hook: runs before every command
|
|
41
|
-
program.hook('preAction', async (thisCommand) => {
|
|
49
|
+
program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
42
50
|
const opts = thisCommand.opts();
|
|
43
51
|
if (opts.json) {
|
|
44
52
|
setJsonMode(true);
|
|
@@ -51,10 +59,130 @@ program.hook('preAction', async (thisCommand) => {
|
|
|
51
59
|
setStoreOverride(opts.store);
|
|
52
60
|
}
|
|
53
61
|
await applyWorkspaceContext();
|
|
62
|
+
await warnWhenOutsideThemeDirectory(actionCommand);
|
|
63
|
+
await noticeNewVersion(actionCommand);
|
|
54
64
|
});
|
|
55
65
|
|
|
66
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
67
|
+
const NO_CHECK_COMMANDS = new Set(['upgrade', 'update', 'completion', 'config autoupgrade']);
|
|
68
|
+
|
|
69
|
+
async function noticeNewVersion(actionCommand) {
|
|
70
|
+
if (isJsonMode() || NO_CHECK_COMMANDS.has(commandPathOf(actionCommand))) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
if (!(await getAutoupgrade())) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
|
|
81
|
+
if (now - (await getLastCheck()) < CHECK_INTERVAL_MS) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await setLastCheck(now);
|
|
86
|
+
|
|
87
|
+
const notice = await buildUpgradeNotice();
|
|
88
|
+
|
|
89
|
+
if (notice) {
|
|
90
|
+
console.log(chalk.yellow(t('upgrade.notice', notice)));
|
|
91
|
+
console.log(chalk.gray(' ' + t('upgrade.notice_disable')));
|
|
92
|
+
console.log();
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function commandPathOf(actionCommand) {
|
|
100
|
+
const segments = [];
|
|
101
|
+
|
|
102
|
+
for (let cmd = actionCommand; cmd?.parent; cmd = cmd.parent) {
|
|
103
|
+
segments.unshift(cmd.name());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return segments.join(' ');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function warnWhenOutsideThemeDirectory(actionCommand) {
|
|
110
|
+
if (isJsonMode() || !commandUsesThemeContext(commandPathOf(actionCommand))) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const activeStore = await getActiveStore();
|
|
115
|
+
|
|
116
|
+
if (!activeStore) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const hint = buildWorkspaceHint({
|
|
121
|
+
context: getWorkspaceContext(),
|
|
122
|
+
activeStore,
|
|
123
|
+
activeTheme: await getActiveTheme(activeStore),
|
|
124
|
+
workspaceRoot: getWorkspaceRoot(),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (hint) {
|
|
128
|
+
console.log(formatWorkspaceHint(hint));
|
|
129
|
+
console.log();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function visibleNames(cmd) {
|
|
134
|
+
const names = [];
|
|
135
|
+
|
|
136
|
+
for (const sub of cmd.commands) {
|
|
137
|
+
if (sub._hidden) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
names.push(sub.name());
|
|
141
|
+
if (sub.alias()) {
|
|
142
|
+
names.push(sub.alias());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return names;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function rejectUnknownCommand(cmd, parentPath = null) {
|
|
150
|
+
const [unknown] = cmd.args;
|
|
151
|
+
const message = formatUnknownCommand(unknown, visibleNames(cmd), parentPath);
|
|
152
|
+
|
|
153
|
+
if (isJsonMode()) {
|
|
154
|
+
jsonOut({ status: 'error', error: { message, hint: null } });
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
showError(message);
|
|
159
|
+
|
|
160
|
+
const suggestion = suggestCommand(unknown, visibleNames(cmd));
|
|
161
|
+
|
|
162
|
+
if (suggestion && process.stdin.isTTY) {
|
|
163
|
+
const full = parentPath ? `${parentPath} ${suggestion}` : suggestion;
|
|
164
|
+
const accepted = await brandedConfirm({
|
|
165
|
+
message: t('error.run_suggestion', { command: `tsoft ${full}` }),
|
|
166
|
+
default: true,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (accepted) {
|
|
170
|
+
const rest = cmd.args.slice(1);
|
|
171
|
+
await program.parseAsync([...process.argv.slice(0, 2), ...full.split(' '), ...rest]);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
56
179
|
// Show branded help when no arguments provided
|
|
57
|
-
program.action(async () => {
|
|
180
|
+
program.action(async (_options, cmd) => {
|
|
181
|
+
if (cmd.args.length) {
|
|
182
|
+
await rejectUnknownCommand(cmd);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
58
186
|
await renderBrandedHelp(program);
|
|
59
187
|
});
|
|
60
188
|
|
|
@@ -88,6 +216,47 @@ program
|
|
|
88
216
|
await whoamiCommand();
|
|
89
217
|
});
|
|
90
218
|
|
|
219
|
+
program
|
|
220
|
+
.command('docs')
|
|
221
|
+
.description('Open the developer documentation in your browser')
|
|
222
|
+
.action(async () => {
|
|
223
|
+
await docsCommand();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
program
|
|
227
|
+
.command('upgrade')
|
|
228
|
+
.alias('update')
|
|
229
|
+
.description('Update the CLI to the latest version')
|
|
230
|
+
.action(async () => {
|
|
231
|
+
await upgradeCommand();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const configCmd = program
|
|
235
|
+
.command('config')
|
|
236
|
+
.description('Show and change CLI settings');
|
|
237
|
+
|
|
238
|
+
configCmd
|
|
239
|
+
.command('autoupgrade [action]')
|
|
240
|
+
.description('Turn the version check on or off (on | off | status)')
|
|
241
|
+
.action(async (action) => {
|
|
242
|
+
await configAutoupgradeCommand(action ?? 'status');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
program
|
|
246
|
+
.command('completion <shell>')
|
|
247
|
+
.description(`Generate a shell completion script (${SUPPORTED_SHELLS.join(', ')})`)
|
|
248
|
+
.action((shell) => {
|
|
249
|
+
if (!SUPPORTED_SHELLS.includes(shell)) {
|
|
250
|
+
showError(t('completion.unsupported_shell', {
|
|
251
|
+
shell,
|
|
252
|
+
supported: SUPPORTED_SHELLS.join(', '),
|
|
253
|
+
}));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
process.stdout.write(renderCompletion(shell, collectCommandTree(program)));
|
|
258
|
+
});
|
|
259
|
+
|
|
91
260
|
const theme = program
|
|
92
261
|
.command('theme')
|
|
93
262
|
.description('Theme management commands');
|
|
@@ -195,7 +364,12 @@ org
|
|
|
195
364
|
const section = theme
|
|
196
365
|
.command('section')
|
|
197
366
|
.description('Section management commands')
|
|
198
|
-
.action(async () => {
|
|
367
|
+
.action(async (_options, cmd) => {
|
|
368
|
+
if (cmd.args.length) {
|
|
369
|
+
await rejectUnknownCommand(cmd, 'theme section');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
199
373
|
await themeSectionMenuCommand();
|
|
200
374
|
});
|
|
201
375
|
|
|
@@ -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
|
+
}
|