tsoft-cli 3.12.4 → 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.
@@ -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
- for (const entry of stores) {
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
- console.log(`${marker} ${name} ${sessionLabel(entry)}`);
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
- showError(t('dev.theme_not_found', { path: themePath }));
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', { slug: forkedSlug })));
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
- // Block başlığı
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
- console.log(chalk.yellow(' ' + t('section.list.sections_label', { count: block.default_sections.length })));
63
-
64
- block.default_sections.forEach((section) => {
65
- console.log(chalk.white(` • ${section.name} ${chalk.gray(`(ID: ${section.id}, v${section.version})`)}`));
66
-
67
- // Meta bilgiler varsa göster
68
- if (section.meta) {
69
- if (section.meta.description) {
70
- console.log(chalk.gray(` ${section.meta.description}`));
71
- }
72
- if (section.meta.tags && section.meta.tags.length > 0) {
73
- console.log(chalk.gray(` ${t('section.list.tags_label')} ${section.meta.tags.join(', ')}`));
74
- }
75
- }
76
-
77
- console.log(chalk.gray(` ${t('section.list.file_label')} ${section.content}`));
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
  }
@@ -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.forEach((theme) => {
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 description = theme.description ? chalk.gray(` - ${theme.description}`) : '';
57
-
58
- console.log(`${indicator} ${decorateThemeName(theme, localTheme)}${description}`);
59
- console.log(chalk.gray(` ${t('theme.folder_label')} ${theme.theme_folder || theme.alias}`));
60
- console.log();
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 { setStoreOverride, applyWorkspaceContext } from './storage.js';
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
+ }