i18nsmith 0.2.0 → 0.3.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.
Files changed (55) hide show
  1. package/dist/commands/audit.d.ts.map +1 -1
  2. package/dist/commands/backup.d.ts.map +1 -1
  3. package/dist/commands/check.d.ts +25 -0
  4. package/dist/commands/check.d.ts.map +1 -1
  5. package/dist/commands/config.d.ts.map +1 -1
  6. package/dist/commands/debug-patterns.d.ts.map +1 -1
  7. package/dist/commands/diagnose.d.ts.map +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/install-hooks.d.ts.map +1 -1
  10. package/dist/commands/preflight.d.ts.map +1 -1
  11. package/dist/commands/rename.d.ts.map +1 -1
  12. package/dist/commands/review.d.ts.map +1 -1
  13. package/dist/commands/scaffold-adapter.d.ts.map +1 -1
  14. package/dist/commands/scan.d.ts.map +1 -1
  15. package/dist/commands/sync.d.ts +1 -1
  16. package/dist/commands/sync.d.ts.map +1 -1
  17. package/dist/commands/transform.d.ts.map +1 -1
  18. package/dist/commands/translate/index.d.ts.map +1 -1
  19. package/dist/index.js +2574 -107704
  20. package/dist/rename-suspicious.test.d.ts +2 -0
  21. package/dist/rename-suspicious.test.d.ts.map +1 -0
  22. package/dist/utils/diff-utils.d.ts +5 -0
  23. package/dist/utils/diff-utils.d.ts.map +1 -1
  24. package/dist/utils/errors.d.ts +8 -0
  25. package/dist/utils/errors.d.ts.map +1 -0
  26. package/dist/utils/locale-audit.d.ts +39 -0
  27. package/dist/utils/locale-audit.d.ts.map +1 -0
  28. package/dist/utils/preview.d.ts.map +1 -1
  29. package/dist/utils/preview.test.d.ts +2 -0
  30. package/dist/utils/preview.test.d.ts.map +1 -0
  31. package/package.json +5 -5
  32. package/src/commands/audit.ts +18 -209
  33. package/src/commands/backup.ts +67 -63
  34. package/src/commands/check.ts +119 -68
  35. package/src/commands/config.ts +117 -95
  36. package/src/commands/debug-patterns.ts +25 -22
  37. package/src/commands/diagnose.ts +29 -26
  38. package/src/commands/init.ts +84 -79
  39. package/src/commands/install-hooks.ts +18 -15
  40. package/src/commands/preflight.ts +21 -13
  41. package/src/commands/rename.ts +86 -81
  42. package/src/commands/review.ts +81 -78
  43. package/src/commands/scaffold-adapter.ts +8 -4
  44. package/src/commands/scan.ts +61 -58
  45. package/src/commands/sync.ts +640 -203
  46. package/src/commands/transform.ts +117 -8
  47. package/src/commands/translate/index.ts +7 -4
  48. package/src/e2e.test.ts +78 -14
  49. package/src/integration.test.ts +86 -0
  50. package/src/rename-suspicious.test.ts +124 -0
  51. package/src/utils/diff-utils.ts +6 -0
  52. package/src/utils/errors.ts +34 -0
  53. package/src/utils/locale-audit.ts +219 -0
  54. package/src/utils/preview.test.ts +137 -0
  55. package/src/utils/preview.ts +2 -8
@@ -7,6 +7,13 @@ import type { CheckSummary } from '@i18nsmith/core';
7
7
  import { printLocaleDiffs } from '../utils/diff-utils.js';
8
8
  import { getDiagnosisExitSignal } from '../utils/diagnostics-exit.js';
9
9
  import { CHECK_EXIT_CODES } from '../utils/exit-codes.js';
10
+ import {
11
+ runLocaleAudit,
12
+ printLocaleAuditResults,
13
+ hasAuditFindings,
14
+ type LocaleAuditSummary,
15
+ } from '../utils/locale-audit.js';
16
+ import { CliError, withErrorHandling } from '../utils/errors.js';
10
17
 
11
18
  interface CheckCommandOptions {
12
19
  config?: string;
@@ -24,6 +31,12 @@ interface CheckCommandOptions {
24
31
  diff?: boolean;
25
32
  invalidateCache?: boolean;
26
33
  preferDiagnosticsExit?: boolean;
34
+ audit?: boolean;
35
+ auditStrict?: boolean;
36
+ auditLocales?: string[];
37
+ auditDuplicates?: boolean;
38
+ auditInconsistent?: boolean;
39
+ auditOrphaned?: boolean;
27
40
  }
28
41
 
29
42
  const collectAssumedKeys = (value: string, previous: string[]) => {
@@ -114,78 +127,116 @@ export function registerCheck(program: Command) {
114
127
  .option('--invalidate-cache', 'Ignore cached sync analysis and rescan all source files', false)
115
128
  .option('--target <pattern...>', 'Limit translation reference scanning to specific files or patterns', collectTargetPatterns, [])
116
129
  .option('--prefer-diagnostics-exit', 'Prefer diagnostics exit codes when --fail-on=conflicts and blocking conflicts exist', false)
117
- .action(async (options: CheckCommandOptions) => {
118
- console.log(chalk.blue('Running guided repository health check...'));
119
- try {
120
- const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
121
-
122
- // Inform user if config was found in a parent directory
123
- const cwd = process.cwd();
124
- if (projectRoot !== cwd) {
125
- console.log(chalk.gray(`Config found at ${path.relative(cwd, configPath)}`));
126
- console.log(chalk.gray(`Using project root: ${projectRoot}\n`));
127
- }
128
-
129
- // Merge --assume-globs with config
130
- if (options.assumeGlobs?.length) {
131
- config.sync = config.sync ?? {};
132
- config.sync.dynamicKeyGlobs = [
133
- ...(config.sync.dynamicKeyGlobs ?? []),
134
- ...options.assumeGlobs,
135
- ];
136
- }
137
- const runner = new CheckRunner(config, { workspaceRoot: projectRoot });
138
- const summary = await runner.run({
139
- assumedKeys: options.assume,
140
- validateInterpolations: options.validateInterpolations,
141
- emptyValuePolicy: options.emptyValues === false ? 'fail' : undefined,
142
- diff: options.diff,
143
- targets: options.target,
144
- invalidateCache: options.invalidateCache,
145
- });
146
-
147
- if (options.report) {
148
- const outputPath = path.resolve(process.cwd(), options.report);
149
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
150
- await fs.writeFile(outputPath, JSON.stringify(summary, null, 2));
151
- console.log(chalk.green(`Health report written to ${outputPath}`));
152
- }
130
+ .option('--audit', 'Include locale quality audit (duplicates, inconsistent keys, orphaned namespaces)', false)
131
+ .option('--audit-strict', 'Fail if locale audit finds issues (implies --audit)', false)
132
+ .option('--audit-locales <locales...>', 'Limit locale audit to specific locales (comma-separated)', collectTargetPatterns, [])
133
+ .option('--audit-duplicates', 'Include duplicate-value quality check during audit (defaults on when no other audit filters provided)', false)
134
+ .option('--audit-inconsistent', 'Include inconsistent-key quality check during audit', false)
135
+ .option('--audit-orphaned', 'Include orphaned-namespace quality check during audit', false)
136
+ .action(withErrorHandling(async (options: CheckCommandOptions) => runCheck(options)));
137
+ }
153
138
 
154
- if (options.json) {
155
- console.log(JSON.stringify(summary, null, 2));
156
- } else {
157
- printCheckSummary(summary);
158
- if (options.diff) {
159
- printLocaleDiffs(summary.sync.diffs);
160
- }
161
- }
139
+ export async function runCheck(options: CheckCommandOptions): Promise<void> {
140
+ const auditEnabled = Boolean(options.audit || options.auditStrict);
141
+ console.log(chalk.blue('Running guided repository health check...'));
142
+ try {
143
+ const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
162
144
 
163
- // If diagnostics discovered blocking conflicts, prefer the diagnostics' exit
164
- // signal so CI can branch on specific failure modes (missing source locale,
165
- // invalid JSON, etc.). This mirrors `i18nsmith diagnose` behavior.
166
- // Only when --prefer-diagnostics-exit is true and --fail-on=conflicts.
167
- const diagExit = getDiagnosisExitSignal(summary.diagnostics);
168
- if (diagExit && options.preferDiagnosticsExit && options.failOn === 'conflicts') {
169
- console.error(chalk.red(`\nBlocking diagnostic conflict detected: ${diagExit.reason}`));
170
- console.error(chalk.red(`Exit code ${diagExit.code}`));
171
- process.exitCode = diagExit.code;
172
- return;
173
- }
145
+ // Inform user if config was found in a parent directory
146
+ const cwd = process.cwd();
147
+ if (projectRoot !== cwd) {
148
+ console.log(chalk.gray(`Config found at ${path.relative(cwd, configPath)}`));
149
+ console.log(chalk.gray(`Using project root: ${projectRoot}\n`));
150
+ }
174
151
 
175
- const failMode = (options.failOn ?? 'conflicts').toLowerCase();
176
- const hasErrors = summary.actionableItems.some((item) => item.severity === 'error');
177
- const hasWarnings = summary.actionableItems.some((item) => item.severity === 'warn');
152
+ // Merge --assume-globs with config
153
+ if (options.assumeGlobs?.length) {
154
+ config.sync = config.sync ?? {};
155
+ config.sync.dynamicKeyGlobs = [
156
+ ...(config.sync.dynamicKeyGlobs ?? []),
157
+ ...options.assumeGlobs,
158
+ ];
159
+ }
160
+ const runner = new CheckRunner(config, { workspaceRoot: projectRoot });
161
+ const summary = await runner.run({
162
+ assumedKeys: options.assume,
163
+ validateInterpolations: options.validateInterpolations,
164
+ emptyValuePolicy: options.emptyValues === false ? 'fail' : undefined,
165
+ diff: options.diff,
166
+ targets: options.target,
167
+ invalidateCache: options.invalidateCache,
168
+ });
178
169
 
179
- if (failMode === 'conflicts' && hasErrors) {
180
- console.error(chalk.red('\nBlocking issues detected. Resolve the actionable errors above.'));
181
- process.exitCode = CHECK_EXIT_CODES.CONFLICTS;
182
- } else if (failMode === 'warnings' && (hasErrors || hasWarnings)) {
183
- console.error(chalk.red('\nWarnings detected. Use --fail-on conflicts to limit failures to blocking issues.'));
184
- process.exitCode = CHECK_EXIT_CODES.WARNINGS;
170
+ let localeAudit: LocaleAuditSummary | undefined;
171
+ if (auditEnabled) {
172
+ const auditLocales = options.auditLocales?.filter(Boolean) ?? [];
173
+ const auditOverridesProvided = Boolean(options.auditDuplicates) || Boolean(options.auditInconsistent) || Boolean(options.auditOrphaned);
174
+
175
+ localeAudit = await runLocaleAudit(
176
+ { config, projectRoot },
177
+ {
178
+ locales: auditLocales.length ? auditLocales : undefined,
179
+ checkDuplicates: auditOverridesProvided ? Boolean(options.auditDuplicates) : true,
180
+ checkInconsistent: auditOverridesProvided ? Boolean(options.auditInconsistent) : true,
181
+ checkOrphaned: auditOverridesProvided ? Boolean(options.auditOrphaned) : true,
185
182
  }
186
- } catch (error) {
187
- console.error(chalk.red('Check failed:'), (error as Error).message);
188
- process.exitCode = 1;
183
+ );
184
+ }
185
+
186
+ const payload = localeAudit ? { ...summary, audit: localeAudit } : summary;
187
+
188
+ if (options.report) {
189
+ const outputPath = path.resolve(process.cwd(), options.report);
190
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
191
+ await fs.writeFile(outputPath, JSON.stringify(payload, null, 2));
192
+ console.log(chalk.green(`Health report written to ${outputPath}`));
193
+ }
194
+
195
+ if (options.json) {
196
+ console.log(JSON.stringify(payload, null, 2));
197
+ } else {
198
+ printCheckSummary(summary);
199
+ if (options.diff) {
200
+ printLocaleDiffs(summary.sync.diffs);
189
201
  }
190
- });
202
+ if (localeAudit) {
203
+ console.log(chalk.blue('\nLocale quality audit'));
204
+ printLocaleAuditResults(localeAudit);
205
+ }
206
+ }
207
+
208
+ // Diagnostics exit override
209
+ const diagExit = getDiagnosisExitSignal(summary.diagnostics);
210
+ if (diagExit && options.preferDiagnosticsExit && (options.failOn ?? 'conflicts') === 'conflicts') {
211
+ console.error(chalk.red(`\nBlocking diagnostic conflict detected: ${diagExit.reason}`));
212
+ console.error(chalk.red(`Exit code ${diagExit.code}`));
213
+ process.exitCode = diagExit.code;
214
+ return;
215
+ }
216
+
217
+ const failMode = (options.failOn ?? 'conflicts').toLowerCase();
218
+ const hasErrors = summary.actionableItems.some((item) => item.severity === 'error');
219
+ const hasWarnings = summary.actionableItems.some((item) => item.severity === 'warn');
220
+ const auditHasIssues = Boolean(localeAudit && hasAuditFindings(localeAudit));
221
+
222
+ if (options.auditStrict && auditHasIssues) {
223
+ console.error(chalk.red('\nAudit detected locale quality issues (--audit-strict).'));
224
+ process.exitCode = CHECK_EXIT_CODES.WARNINGS;
225
+ return;
226
+ }
227
+
228
+ if (failMode === 'conflicts' && hasErrors) {
229
+ console.error(chalk.red('\nBlocking issues detected. Resolve the actionable errors above.'));
230
+ process.exitCode = CHECK_EXIT_CODES.CONFLICTS;
231
+ } else if (failMode === 'warnings' && (hasErrors || hasWarnings || auditHasIssues)) {
232
+ console.error(chalk.red('\nWarnings detected. Use --fail-on conflicts to limit failures to blocking issues.'));
233
+ process.exitCode = CHECK_EXIT_CODES.WARNINGS;
234
+ }
235
+ } catch (error) {
236
+ if (error instanceof CliError) {
237
+ throw error;
238
+ }
239
+ const message = error instanceof Error ? error.message : String(error);
240
+ throw new CliError(`Check failed: ${message}`);
241
+ }
191
242
  }
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import type { Command } from 'commander';
5
5
  import { loadConfigWithMeta, DEFAULT_CONFIG_FILENAME } from '@i18nsmith/core';
6
+ import { CliError, withErrorHandling } from '../utils/errors.js';
6
7
 
7
8
  interface ConfigCommandOptions {
8
9
  config?: string;
@@ -121,32 +122,33 @@ export function registerConfig(program: Command) {
121
122
  .description('Get a configuration value by key path (e.g., translationAdapter.module)')
122
123
  .option('-c, --config <path>', 'Path to i18nsmith config file', DEFAULT_CONFIG_FILENAME)
123
124
  .option('--json', 'Output as JSON', false)
124
- .action(async (key: string, options: ConfigGetOptions) => {
125
- try {
126
- const { config, configPath } = await loadConfigWithMeta(options.config);
127
- const keyPath = parseKeyPath(key);
128
- const value = getNestedValue(config as unknown as Record<string, unknown>, keyPath);
125
+ .action(
126
+ withErrorHandling(async (key: string, options: ConfigGetOptions) => {
127
+ try {
128
+ const { config } = await loadConfigWithMeta(options.config);
129
+ const keyPath = parseKeyPath(key);
130
+ const value = getNestedValue(config as unknown as Record<string, unknown>, keyPath);
129
131
 
130
- if (value === undefined) {
131
- console.log(chalk.yellow(`Key "${key}" not found in config`));
132
- process.exitCode = 1;
133
- return;
134
- }
132
+ if (value === undefined) {
133
+ throw new CliError(`Key "${key}" not found in config`);
134
+ }
135
135
 
136
- if (options.json) {
137
- console.log(JSON.stringify({ key, value }, null, 2));
138
- } else {
139
- if (typeof value === 'object' && value !== null) {
136
+ if (options.json) {
137
+ console.log(JSON.stringify({ key, value }, null, 2));
138
+ } else if (typeof value === 'object' && value !== null) {
140
139
  console.log(JSON.stringify(value, null, 2));
141
140
  } else {
142
141
  console.log(String(value));
143
142
  }
143
+ } catch (error) {
144
+ if (error instanceof CliError) {
145
+ throw error;
146
+ }
147
+ const message = error instanceof Error ? error.message : String(error);
148
+ throw new CliError(`Failed to get config: ${message}`);
144
149
  }
145
- } catch (error) {
146
- console.error(chalk.red('Failed to get config:'), (error as Error).message);
147
- process.exitCode = 1;
148
- }
149
- });
150
+ })
151
+ );
150
152
 
151
153
  // Subcommand: config set <key> <value>
152
154
  configCmd
@@ -154,29 +156,34 @@ export function registerConfig(program: Command) {
154
156
  .description('Set a configuration value by key path (e.g., translationAdapter.module "src/i18n.ts")')
155
157
  .option('-c, --config <path>', 'Path to i18nsmith config file', DEFAULT_CONFIG_FILENAME)
156
158
  .option('--json', 'Output result as JSON', false)
157
- .action(async (key: string, value: string, options: ConfigSetOptions) => {
158
- try {
159
- const { configPath } = await loadConfigWithMeta(options.config);
160
- const { parsed } = await readRawConfig(configPath);
161
-
162
- const keyPath = parseKeyPath(key);
163
- const parsedValue = parseValue(value);
164
-
165
- setNestedValue(parsed, keyPath, parsedValue);
166
-
167
- await writeConfig(configPath, parsed);
159
+ .action(
160
+ withErrorHandling(async (key: string, value: string, options: ConfigSetOptions) => {
161
+ try {
162
+ const { configPath } = await loadConfigWithMeta(options.config);
163
+ const { parsed } = await readRawConfig(configPath);
164
+
165
+ const keyPath = parseKeyPath(key);
166
+ const parsedValue = parseValue(value);
167
+
168
+ setNestedValue(parsed, keyPath, parsedValue);
168
169
 
169
- if (options.json) {
170
- console.log(JSON.stringify({ key, value: parsedValue, configPath }, null, 2));
171
- } else {
172
- console.log(chalk.green(`✓ Set ${key} = ${JSON.stringify(parsedValue)}`));
173
- console.log(chalk.dim(` Updated: ${configPath}`));
170
+ await writeConfig(configPath, parsed);
171
+
172
+ if (options.json) {
173
+ console.log(JSON.stringify({ key, value: parsedValue, configPath }, null, 2));
174
+ } else {
175
+ console.log(chalk.green(`✓ Set ${key} = ${JSON.stringify(parsedValue)}`));
176
+ console.log(chalk.dim(` Updated: ${configPath}`));
177
+ }
178
+ } catch (error) {
179
+ if (error instanceof CliError) {
180
+ throw error;
181
+ }
182
+ const message = error instanceof Error ? error.message : String(error);
183
+ throw new CliError(`Failed to set config: ${message}`);
174
184
  }
175
- } catch (error) {
176
- console.error(chalk.red('Failed to set config:'), (error as Error).message);
177
- process.exitCode = 1;
178
- }
179
- });
185
+ })
186
+ );
180
187
 
181
188
  // Subcommand: config list
182
189
  configCmd
@@ -184,37 +191,47 @@ export function registerConfig(program: Command) {
184
191
  .description('List all configuration values')
185
192
  .option('-c, --config <path>', 'Path to i18nsmith config file', DEFAULT_CONFIG_FILENAME)
186
193
  .option('--json', 'Output as JSON', false)
187
- .action(async (options: ConfigCommandOptions) => {
188
- try {
189
- const { config, configPath } = await loadConfigWithMeta(options.config);
194
+ .action(
195
+ withErrorHandling(async (options: ConfigCommandOptions) => {
196
+ try {
197
+ const { config, configPath } = await loadConfigWithMeta(options.config);
190
198
 
191
- if (options.json) {
192
- console.log(JSON.stringify(config, null, 2));
193
- } else {
194
- console.log(chalk.blue(`Configuration from: ${configPath}`));
195
- console.log();
196
- console.log(JSON.stringify(config, null, 2));
199
+ if (options.json) {
200
+ console.log(JSON.stringify(config, null, 2));
201
+ } else {
202
+ console.log(chalk.blue(`Configuration from: ${configPath}`));
203
+ console.log();
204
+ console.log(JSON.stringify(config, null, 2));
205
+ }
206
+ } catch (error) {
207
+ if (error instanceof CliError) {
208
+ throw error;
209
+ }
210
+ const message = error instanceof Error ? error.message : String(error);
211
+ throw new CliError(`Failed to read config: ${message}`);
197
212
  }
198
- } catch (error) {
199
- console.error(chalk.red('Failed to read config:'), (error as Error).message);
200
- process.exitCode = 1;
201
- }
202
- });
213
+ })
214
+ );
203
215
 
204
216
  // Subcommand: config path
205
217
  configCmd
206
218
  .command('path')
207
219
  .description('Print the path to the active config file')
208
220
  .option('-c, --config <path>', 'Path to i18nsmith config file', DEFAULT_CONFIG_FILENAME)
209
- .action(async (options: ConfigCommandOptions) => {
210
- try {
211
- const { configPath } = await loadConfigWithMeta(options.config);
212
- console.log(configPath);
213
- } catch (error) {
214
- console.error(chalk.red('Failed to find config:'), (error as Error).message);
215
- process.exitCode = 1;
216
- }
217
- });
221
+ .action(
222
+ withErrorHandling(async (options: ConfigCommandOptions) => {
223
+ try {
224
+ const { configPath } = await loadConfigWithMeta(options.config);
225
+ console.log(configPath);
226
+ } catch (error) {
227
+ if (error instanceof CliError) {
228
+ throw error;
229
+ }
230
+ const message = error instanceof Error ? error.message : String(error);
231
+ throw new CliError(`Failed to find config: ${message}`);
232
+ }
233
+ })
234
+ );
218
235
 
219
236
  // Subcommand: config init-adapter <path>
220
237
  configCmd
@@ -223,41 +240,46 @@ export function registerConfig(program: Command) {
223
240
  .option('-c, --config <path>', 'Path to i18nsmith config file', DEFAULT_CONFIG_FILENAME)
224
241
  .option('--hook <name>', 'Name of the translation hook (default: useTranslation)', 'useTranslation')
225
242
  .option('--json', 'Output result as JSON', false)
226
- .action(async (adapterPath: string, options: { config?: string; hook: string; json?: boolean }) => {
227
- try {
228
- const { configPath } = await loadConfigWithMeta(options.config);
229
- const { parsed } = await readRawConfig(configPath);
243
+ .action(
244
+ withErrorHandling(async (adapterPath: string, options: { config?: string; hook: string; json?: boolean }) => {
245
+ try {
246
+ const { configPath } = await loadConfigWithMeta(options.config);
247
+ const { parsed } = await readRawConfig(configPath);
230
248
 
231
- // Resolve relative path
232
- const projectRoot = path.dirname(configPath);
233
- const relativePath = path.isAbsolute(adapterPath)
234
- ? path.relative(projectRoot, adapterPath)
235
- : adapterPath;
249
+ // Resolve relative path
250
+ const projectRoot = path.dirname(configPath);
251
+ const relativePath = path.isAbsolute(adapterPath)
252
+ ? path.relative(projectRoot, adapterPath)
253
+ : adapterPath;
236
254
 
237
- // Update translationAdapter
238
- if (!parsed.translationAdapter || typeof parsed.translationAdapter !== 'object') {
239
- parsed.translationAdapter = {};
240
- }
241
- const adapter = parsed.translationAdapter as Record<string, unknown>;
242
- adapter.module = relativePath;
243
- adapter.hookName = options.hook;
255
+ // Update translationAdapter
256
+ if (!parsed.translationAdapter || typeof parsed.translationAdapter !== 'object') {
257
+ parsed.translationAdapter = {};
258
+ }
259
+ const adapter = parsed.translationAdapter as Record<string, unknown>;
260
+ adapter.module = relativePath;
261
+ adapter.hookName = options.hook;
244
262
 
245
- await writeConfig(configPath, parsed);
263
+ await writeConfig(configPath, parsed);
246
264
 
247
- if (options.json) {
248
- console.log(JSON.stringify({
249
- translationAdapter: parsed.translationAdapter,
250
- configPath,
251
- }, null, 2));
252
- } else {
253
- console.log(chalk.green('✓ Translation adapter configured:'));
254
- console.log(chalk.dim(` module: ${relativePath}`));
255
- console.log(chalk.dim(` hookName: ${options.hook}`));
256
- console.log(chalk.dim(` Updated: ${configPath}`));
265
+ if (options.json) {
266
+ console.log(JSON.stringify({
267
+ translationAdapter: parsed.translationAdapter,
268
+ configPath,
269
+ }, null, 2));
270
+ } else {
271
+ console.log(chalk.green('✓ Translation adapter configured:'));
272
+ console.log(chalk.dim(` module: ${relativePath}`));
273
+ console.log(chalk.dim(` hookName: ${options.hook}`));
274
+ console.log(chalk.dim(` Updated: ${configPath}`));
275
+ }
276
+ } catch (error) {
277
+ if (error instanceof CliError) {
278
+ throw error;
279
+ }
280
+ const message = error instanceof Error ? error.message : String(error);
281
+ throw new CliError(`Failed to configure adapter: ${message}`);
257
282
  }
258
- } catch (error) {
259
- console.error(chalk.red('Failed to configure adapter:'), (error as Error).message);
260
- process.exitCode = 1;
261
- }
262
- });
283
+ })
284
+ );
263
285
  }
@@ -3,6 +3,7 @@ import chalk from 'chalk';
3
3
  import { Command } from 'commander';
4
4
  import fg from 'fast-glob';
5
5
  import { loadConfigWithMeta } from '@i18nsmith/core';
6
+ import { CliError, withErrorHandling } from '../utils/errors.js';
6
7
 
7
8
  interface DebugPatternsOptions {
8
9
  config?: string;
@@ -34,37 +35,39 @@ export function registerDebugPatterns(program: Command) {
34
35
  .option('-c, --config <path>', 'Path to i18nsmith config file', 'i18n.config.json')
35
36
  .option('--json', 'Print raw JSON results', false)
36
37
  .option('--verbose', 'Show all matched files for each pattern', false)
37
- .action(async (options: DebugPatternsOptions) => {
38
- try {
39
- const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
40
-
41
- console.log(chalk.blue('Debugging glob patterns...'));
42
- console.log(chalk.gray(`Config: ${path.relative(process.cwd(), configPath)}`));
43
- console.log(chalk.gray(`Project root: ${projectRoot}\n`));
38
+ .action(
39
+ withErrorHandling(async (options: DebugPatternsOptions) => {
40
+ try {
41
+ const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
42
+
43
+ console.log(chalk.blue('Debugging glob patterns...'));
44
+ console.log(chalk.gray(`Config: ${path.relative(process.cwd(), configPath)}`));
45
+ console.log(chalk.gray(`Project root: ${projectRoot}\n`));
44
46
 
45
- const includePatterns = config.include ?? ['**/*.tsx', '**/*.ts', '**/*.jsx', '**/*.js'];
46
- const excludePatterns = config.exclude ?? ['**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'];
47
+ const includePatterns = config.include ?? ['**/*.tsx', '**/*.ts', '**/*.jsx', '**/*.js'];
48
+ const excludePatterns = config.exclude ?? ['**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'];
47
49
 
48
- const summary = await analyzePatterns(projectRoot, includePatterns, excludePatterns, options.verbose);
50
+ const summary = await analyzePatterns(projectRoot, includePatterns, excludePatterns, options.verbose);
49
51
 
50
- if (options.json) {
51
- console.log(JSON.stringify(summary, null, 2));
52
- return;
53
- }
52
+ if (options.json) {
53
+ console.log(JSON.stringify(summary, null, 2));
54
+ return;
55
+ }
54
56
 
55
- printPatternAnalysis(summary, options.verbose);
56
- } catch (error) {
57
- console.error(chalk.red('Pattern debug failed:'), (error as Error).message);
58
- process.exitCode = 1;
59
- }
60
- });
57
+ printPatternAnalysis(summary, options.verbose);
58
+ } catch (error) {
59
+ const message = error instanceof Error ? error.message : String(error);
60
+ throw new CliError(`Pattern debug failed: ${message}`);
61
+ }
62
+ })
63
+ );
61
64
  }
62
65
 
63
66
  async function analyzePatterns(
64
67
  projectRoot: string,
65
68
  includePatterns: string[],
66
69
  excludePatterns: string[],
67
- verbose?: boolean
70
+ _verbose?: boolean
68
71
  ): Promise<DebugPatternsSummary> {
69
72
  const includeMatches: PatternMatch[] = [];
70
73
  const excludeMatches: PatternMatch[] = [];
@@ -173,7 +176,7 @@ function generateSuggestions(
173
176
  return suggestions;
174
177
  }
175
178
 
176
- function suggestPatternFix(pattern: string, projectRoot: string): string | null {
179
+ function suggestPatternFix(pattern: string, _projectRoot: string): string | null {
177
180
  // Common fixes for patterns that don't match
178
181
 
179
182
  // If pattern is like "src/**/*.tsx" but files are in "app/**/*.tsx"
@@ -5,6 +5,7 @@ import type { Command } from 'commander';
5
5
  import { loadConfig, diagnoseWorkspace } from '@i18nsmith/core';
6
6
  import type { DiagnosisReport } from '@i18nsmith/core';
7
7
  import { getDiagnosisExitSignal } from '../utils/diagnostics-exit.js';
8
+ import { CliError, withErrorHandling } from '../utils/errors.js';
8
9
 
9
10
  interface DiagnoseCommandOptions {
10
11
  config?: string;
@@ -103,34 +104,36 @@ export function registerDiagnose(program: Command) {
103
104
  .option('-c, --config <path>', 'Path to i18nsmith config file', 'i18n.config.json')
104
105
  .option('--json', 'Print raw JSON results', false)
105
106
  .option('--report <path>', 'Write JSON report to a file (for CI or editors)')
106
- .action(async (options: DiagnoseCommandOptions) => {
107
- console.log(chalk.blue('Running repository diagnostics...'));
108
- try {
109
- const config = await loadConfig(options.config);
110
- const report = await diagnoseWorkspace(config);
107
+ .action(
108
+ withErrorHandling(async (options: DiagnoseCommandOptions) => {
109
+ console.log(chalk.blue('Running repository diagnostics...'));
110
+ try {
111
+ const config = await loadConfig(options.config);
112
+ const report = await diagnoseWorkspace(config);
111
113
 
112
- if (options.report) {
113
- const outputPath = path.resolve(process.cwd(), options.report);
114
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
115
- await fs.writeFile(outputPath, JSON.stringify(report, null, 2));
116
- console.log(chalk.green(`Diagnosis report written to ${outputPath}`));
117
- }
114
+ if (options.report) {
115
+ const outputPath = path.resolve(process.cwd(), options.report);
116
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
117
+ await fs.writeFile(outputPath, JSON.stringify(report, null, 2));
118
+ console.log(chalk.green(`Diagnosis report written to ${outputPath}`));
119
+ }
118
120
 
119
- if (options.json) {
120
- console.log(JSON.stringify(report, null, 2));
121
- } else {
122
- printDiagnosisReport(report);
123
- }
121
+ if (options.json) {
122
+ console.log(JSON.stringify(report, null, 2));
123
+ } else {
124
+ printDiagnosisReport(report);
125
+ }
124
126
 
125
- const exitSignal = getDiagnosisExitSignal(report);
126
- if (exitSignal) {
127
- console.error(chalk.red(`\nBlocking conflicts detected (${report.conflicts.length}).`));
128
- console.error(chalk.red(`Exit code ${exitSignal.code}: ${exitSignal.reason}`));
129
- process.exitCode = exitSignal.code;
127
+ const exitSignal = getDiagnosisExitSignal(report);
128
+ if (exitSignal) {
129
+ console.error(chalk.red(`\nBlocking conflicts detected (${report.conflicts.length}).`));
130
+ console.error(chalk.red(`Exit code ${exitSignal.code}: ${exitSignal.reason}`));
131
+ process.exitCode = exitSignal.code;
132
+ }
133
+ } catch (error) {
134
+ const message = error instanceof Error ? error.message : String(error);
135
+ throw new CliError(`Diagnose failed: ${message}`);
130
136
  }
131
- } catch (error) {
132
- console.error(chalk.red('Diagnose failed:'), (error as Error).message);
133
- process.exitCode = 1;
134
- }
135
- });
137
+ })
138
+ );
136
139
  }