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.
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/check.d.ts +25 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/debug-patterns.d.ts.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/install-hooks.d.ts.map +1 -1
- package/dist/commands/preflight.d.ts.map +1 -1
- package/dist/commands/rename.d.ts.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/scaffold-adapter.d.ts.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/sync.d.ts +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/transform.d.ts.map +1 -1
- package/dist/commands/translate/index.d.ts.map +1 -1
- package/dist/index.js +2574 -107704
- package/dist/rename-suspicious.test.d.ts +2 -0
- package/dist/rename-suspicious.test.d.ts.map +1 -0
- package/dist/utils/diff-utils.d.ts +5 -0
- package/dist/utils/diff-utils.d.ts.map +1 -1
- package/dist/utils/errors.d.ts +8 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/locale-audit.d.ts +39 -0
- package/dist/utils/locale-audit.d.ts.map +1 -0
- package/dist/utils/preview.d.ts.map +1 -1
- package/dist/utils/preview.test.d.ts +2 -0
- package/dist/utils/preview.test.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/commands/audit.ts +18 -209
- package/src/commands/backup.ts +67 -63
- package/src/commands/check.ts +119 -68
- package/src/commands/config.ts +117 -95
- package/src/commands/debug-patterns.ts +25 -22
- package/src/commands/diagnose.ts +29 -26
- package/src/commands/init.ts +84 -79
- package/src/commands/install-hooks.ts +18 -15
- package/src/commands/preflight.ts +21 -13
- package/src/commands/rename.ts +86 -81
- package/src/commands/review.ts +81 -78
- package/src/commands/scaffold-adapter.ts +8 -4
- package/src/commands/scan.ts +61 -58
- package/src/commands/sync.ts +640 -203
- package/src/commands/transform.ts +117 -8
- package/src/commands/translate/index.ts +7 -4
- package/src/e2e.test.ts +78 -14
- package/src/integration.test.ts +86 -0
- package/src/rename-suspicious.test.ts +124 -0
- package/src/utils/diff-utils.ts +6 -0
- package/src/utils/errors.ts +34 -0
- package/src/utils/locale-audit.ts +219 -0
- package/src/utils/preview.test.ts +137 -0
- package/src/utils/preview.ts +2 -8
package/src/commands/check.ts
CHANGED
|
@@ -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
|
-
.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
}
|
package/src/commands/config.ts
CHANGED
|
@@ -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(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
132
|
+
if (value === undefined) {
|
|
133
|
+
throw new CliError(`Key "${key}" not found in config`);
|
|
134
|
+
}
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
}
|
|
146
|
-
|
|
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(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
}
|
|
176
|
-
|
|
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(
|
|
188
|
-
|
|
189
|
-
|
|
194
|
+
.action(
|
|
195
|
+
withErrorHandling(async (options: ConfigCommandOptions) => {
|
|
196
|
+
try {
|
|
197
|
+
const { config, configPath } = await loadConfigWithMeta(options.config);
|
|
190
198
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
}
|
|
199
|
-
|
|
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(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
263
|
+
await writeConfig(configPath, parsed);
|
|
246
264
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
}
|
|
259
|
-
|
|
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(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
+
const includePatterns = config.include ?? ['**/*.tsx', '**/*.ts', '**/*.jsx', '**/*.js'];
|
|
48
|
+
const excludePatterns = config.exclude ?? ['**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'];
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
const summary = await analyzePatterns(projectRoot, includePatterns, excludePatterns, options.verbose);
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
if (options.json) {
|
|
53
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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,
|
|
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"
|
package/src/commands/diagnose.ts
CHANGED
|
@@ -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(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
if (options.json) {
|
|
122
|
+
console.log(JSON.stringify(report, null, 2));
|
|
123
|
+
} else {
|
|
124
|
+
printDiagnosisReport(report);
|
|
125
|
+
}
|
|
124
126
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
}
|
|
132
|
-
|
|
133
|
-
process.exitCode = 1;
|
|
134
|
-
}
|
|
135
|
-
});
|
|
137
|
+
})
|
|
138
|
+
);
|
|
136
139
|
}
|