i18nsmith 0.4.3 → 0.6.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/README.md +126 -0
- package/build.mjs +5 -0
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/coverage.d.ts.map +1 -1
- package/dist/commands/detect.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/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.map +1 -1
- package/dist/commands/transform.d.ts.map +1 -1
- package/dist/index.cjs +13069 -6794
- package/dist/services/index.d.ts +12 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/transform-service.d.ts +64 -0
- package/dist/services/transform-service.d.ts.map +1 -0
- package/dist/utils/bootstrap.d.ts +83 -0
- package/dist/utils/bootstrap.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/commands/backup.ts +28 -10
- package/src/commands/check.ts +16 -4
- package/src/commands/config.ts +126 -0
- package/src/commands/coverage.ts +11 -4
- package/src/commands/detect.ts +11 -4
- package/src/commands/diagnose.ts +12 -2
- package/src/commands/init.test.ts +80 -0
- package/src/commands/init.ts +92 -49
- package/src/commands/rename.ts +24 -5
- package/src/commands/review.ts +10 -3
- package/src/commands/scaffold-adapter.ts +11 -2
- package/src/commands/scan.ts +20 -7
- package/src/commands/sync.ts +91 -23
- package/src/commands/transform.ts +8 -1
- package/src/index.ts +1 -1
- package/src/integration.test.ts +145 -12
- package/src/services/index.ts +12 -0
- package/src/services/transform-service.ts +203 -0
- package/src/utils/bootstrap.ts +221 -0
package/src/commands/init.ts
CHANGED
|
@@ -3,7 +3,8 @@ import inquirer from 'inquirer';
|
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import { diagnoseWorkspace, I18nConfig, TranslationConfig, ensureGitignore,
|
|
6
|
+
import { diagnoseWorkspace, I18nConfig, TranslationConfig, ensureGitignore, type ProjectIntelligence, type SuggestedConfig, createBackup } from '@i18nsmith/core';
|
|
7
|
+
import { getServiceContainer } from '../utils/bootstrap.js';
|
|
7
8
|
import { scaffoldTranslationContext, scaffoldI18next } from '../utils/scaffold.js';
|
|
8
9
|
import { hasDependency, readPackageJson } from '../utils/pkg.js';
|
|
9
10
|
import { detectPackageManager, installDependencies } from '../utils/package-manager.js';
|
|
@@ -76,9 +77,12 @@ interface InitAnswers {
|
|
|
76
77
|
*/
|
|
77
78
|
async function detectProjectIntelligence(workspaceRoot: string): Promise<ProjectIntelligence | null> {
|
|
78
79
|
try {
|
|
79
|
-
const
|
|
80
|
-
const result = await
|
|
81
|
-
|
|
80
|
+
const container = getServiceContainer({ workspaceRoot });
|
|
81
|
+
const result = await container.projectIntelligence.analyze({ workspaceRoot });
|
|
82
|
+
if (!result.success) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return result.data;
|
|
82
86
|
} catch {
|
|
83
87
|
return null;
|
|
84
88
|
}
|
|
@@ -112,7 +116,7 @@ async function runNonInteractiveInit(commandOptions: InitCommandOptions): Promis
|
|
|
112
116
|
let suggestedConfig: SuggestedConfig;
|
|
113
117
|
|
|
114
118
|
if (intelligence) {
|
|
115
|
-
const { framework, locales, filePatterns
|
|
119
|
+
const { framework, locales, filePatterns } = intelligence;
|
|
116
120
|
|
|
117
121
|
// Report detection results
|
|
118
122
|
if (framework.type !== 'unknown') {
|
|
@@ -131,8 +135,8 @@ async function runNonInteractiveInit(commandOptions: InitCommandOptions): Promis
|
|
|
131
135
|
|
|
132
136
|
// Use template if specified, otherwise use detected config
|
|
133
137
|
if (commandOptions.template) {
|
|
134
|
-
const
|
|
135
|
-
suggestedConfig =
|
|
138
|
+
const container = getServiceContainer({ workspaceRoot });
|
|
139
|
+
suggestedConfig = container.projectIntelligence.applyTemplate(commandOptions.template, intelligence);
|
|
136
140
|
console.log(chalk.green(` ✓ Template: ${commandOptions.template}`));
|
|
137
141
|
} else {
|
|
138
142
|
suggestedConfig = intelligence.suggestedConfig;
|
|
@@ -164,7 +168,7 @@ async function runNonInteractiveInit(commandOptions: InitCommandOptions): Promis
|
|
|
164
168
|
throw new CliError('Project analysis failed. Use --template or run interactively.');
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
// Build config from suggested values
|
|
171
|
+
// Build config from suggested values (now includes all Phase 1-6 features)
|
|
168
172
|
const config: I18nConfig = {
|
|
169
173
|
version: 1 as const,
|
|
170
174
|
sourceLanguage: suggestedConfig.sourceLanguage,
|
|
@@ -174,11 +178,31 @@ async function runNonInteractiveInit(commandOptions: InitCommandOptions): Promis
|
|
|
174
178
|
exclude: suggestedConfig.exclude,
|
|
175
179
|
minTextLength: 1,
|
|
176
180
|
translation: { provider: 'manual' },
|
|
181
|
+
// Include preset if present
|
|
182
|
+
...(suggestedConfig.preset && { preset: suggestedConfig.preset }),
|
|
177
183
|
translationAdapter: {
|
|
178
184
|
module: suggestedConfig.translationAdapter.module,
|
|
179
185
|
hookName: suggestedConfig.translationAdapter.hookName,
|
|
186
|
+
// Include new options from Phase 4
|
|
187
|
+
skipUnusedImports: suggestedConfig.translationAdapter.skipUnusedImports,
|
|
188
|
+
preferGlobalT: suggestedConfig.translationAdapter.preferGlobalT,
|
|
189
|
+
vueUseGlobalOnly: suggestedConfig.translationAdapter.vueUseGlobalOnly,
|
|
190
|
+
},
|
|
191
|
+
// Include enhanced key generation from Phase 1
|
|
192
|
+
keyGeneration: {
|
|
193
|
+
namespace: suggestedConfig.keyGeneration.namespace,
|
|
194
|
+
shortHashLen: suggestedConfig.keyGeneration.shortHashLen,
|
|
195
|
+
strategy: suggestedConfig.keyGeneration.strategy,
|
|
196
|
+
maxKeyLength: suggestedConfig.keyGeneration.maxKeyLength,
|
|
197
|
+
caseStyle: suggestedConfig.keyGeneration.caseStyle,
|
|
198
|
+
deduplicateByValue: suggestedConfig.keyGeneration.deduplicateByValue,
|
|
180
199
|
},
|
|
181
|
-
|
|
200
|
+
// Include extraction options from Phase 2
|
|
201
|
+
extraction: suggestedConfig.extraction,
|
|
202
|
+
// Include sync options from Phase 3
|
|
203
|
+
sync: suggestedConfig.sync,
|
|
204
|
+
// Include merge strategy from Phase 3
|
|
205
|
+
mergeStrategy: suggestedConfig.mergeStrategy,
|
|
182
206
|
seedTargetLocales: false,
|
|
183
207
|
};
|
|
184
208
|
|
|
@@ -190,6 +214,11 @@ async function runNonInteractiveInit(commandOptions: InitCommandOptions): Promis
|
|
|
190
214
|
console.log(chalk.dim(' Target languages: ' + config.targetLanguages.join(', ')));
|
|
191
215
|
}
|
|
192
216
|
console.log(chalk.dim(' Adapter: ' + (config.translationAdapter?.module ?? 'react-i18next')));
|
|
217
|
+
if (config.preset) {
|
|
218
|
+
console.log(chalk.dim(' Preset: ' + config.preset));
|
|
219
|
+
}
|
|
220
|
+
console.log(chalk.dim(' Key strategy: ' + (config.keyGeneration?.strategy ?? 'hash')));
|
|
221
|
+
console.log(chalk.dim(' Merge strategy: ' + (config.mergeStrategy ?? 'keep-source')));
|
|
193
222
|
|
|
194
223
|
// Ensure .gitignore has i18nsmith artifacts
|
|
195
224
|
const gitignoreResult = await ensureGitignore(workspaceRoot);
|
|
@@ -216,10 +245,14 @@ async function runNonInteractiveInit(commandOptions: InitCommandOptions): Promis
|
|
|
216
245
|
// Seed source locale with detected keys to avoid immediate "missing-key" diagnostics
|
|
217
246
|
try {
|
|
218
247
|
console.log(chalk.blue('🔍 Scanning for existing hardcoded text...'));
|
|
219
|
-
const
|
|
220
|
-
const scanResult = await scanner.scan();
|
|
248
|
+
const container = getServiceContainer({ workspaceRoot });
|
|
249
|
+
const scanResult = await container.scanner.scan(config);
|
|
221
250
|
|
|
222
|
-
if (scanResult.
|
|
251
|
+
if (!scanResult.success) {
|
|
252
|
+
throw new Error(scanResult.error.message);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (scanResult.data.buckets.highConfidence.length > 0) {
|
|
223
256
|
const sourceLocalePath = path.join(workspaceRoot, config.localesDir || 'locales', `${config.sourceLanguage}.json`);
|
|
224
257
|
|
|
225
258
|
// Read existing content or create empty object
|
|
@@ -231,16 +264,16 @@ async function runNonInteractiveInit(commandOptions: InitCommandOptions): Promis
|
|
|
231
264
|
// File doesn't exist or is invalid, start with empty
|
|
232
265
|
}
|
|
233
266
|
|
|
234
|
-
// Generate keys for high-confidence candidates
|
|
235
|
-
const
|
|
267
|
+
// Generate keys for high-confidence candidates using service with custom options
|
|
268
|
+
const keyGenOptions = {
|
|
236
269
|
namespace: config.keyGeneration?.namespace || 'common',
|
|
237
270
|
hashLength: config.keyGeneration?.shortHashLen || 6,
|
|
238
271
|
workspaceRoot,
|
|
239
|
-
}
|
|
272
|
+
};
|
|
240
273
|
|
|
241
274
|
let keysAdded = 0;
|
|
242
|
-
for (const candidate of scanResult.buckets.highConfidence) {
|
|
243
|
-
const generated = keyGenerator.
|
|
275
|
+
for (const candidate of scanResult.data.buckets.highConfidence) {
|
|
276
|
+
const generated = container.keyGenerator.generateWithOptions(candidate.text, keyGenOptions, {
|
|
244
277
|
filePath: candidate.filePath,
|
|
245
278
|
kind: candidate.kind,
|
|
246
279
|
context: candidate.context,
|
|
@@ -510,6 +543,13 @@ export function registerInit(program: Command) {
|
|
|
510
543
|
return !isNaN(num) && num > 0 ? true : 'Please enter a positive number';
|
|
511
544
|
},
|
|
512
545
|
},
|
|
546
|
+
{
|
|
547
|
+
type: 'confirm',
|
|
548
|
+
name: 'seedTargetLocales',
|
|
549
|
+
message: 'Seed target locale files with placeholders for missing keys?',
|
|
550
|
+
when: () => true,
|
|
551
|
+
default: false,
|
|
552
|
+
},
|
|
513
553
|
]);
|
|
514
554
|
|
|
515
555
|
|
|
@@ -562,9 +602,9 @@ export function registerInit(program: Command) {
|
|
|
562
602
|
if (answers.setupMode === 'template') {
|
|
563
603
|
// Use template
|
|
564
604
|
console.log(chalk.blue(`📋 Applying ${answers.template} template...`));
|
|
565
|
-
const
|
|
605
|
+
const container = getServiceContainer({ workspaceRoot });
|
|
566
606
|
// Uses pre-detected intelligence from outer scope
|
|
567
|
-
const suggestedConfig =
|
|
607
|
+
const suggestedConfig = container.projectIntelligence.applyTemplate(answers.template!, intelligence || {
|
|
568
608
|
framework: { type: 'unknown', adapter: 'react-i18next', hookName: 'useTranslation', features: [], confidence: 0, evidence: [] },
|
|
569
609
|
locales: { sourceLanguage: 'en', targetLanguages: [], localesDir: 'locales', format: 'flat', existingFiles: [], existingKeyCount: 0, confidence: 0 },
|
|
570
610
|
filePatterns: { include: ['**/*.{ts,tsx,js,jsx}'], exclude: ['node_modules/**', 'dist/**'], sourceDirectories: [], hasTypeScript: false, hasJsx: false, hasVue: false, hasSvelte: false, sourceFileCount: 0, confidence: 0 },
|
|
@@ -645,6 +685,12 @@ export function registerInit(program: Command) {
|
|
|
645
685
|
};
|
|
646
686
|
}
|
|
647
687
|
|
|
688
|
+
// Honor explicit interactive answer for seeding target locales regardless
|
|
689
|
+
// of which setup mode (auto/template/manual) produced the initial config.
|
|
690
|
+
if (typeof (answers as InitAnswers).seedTargetLocales === 'boolean' && config) {
|
|
691
|
+
config.seedTargetLocales = (answers as InitAnswers).seedTargetLocales;
|
|
692
|
+
}
|
|
693
|
+
|
|
648
694
|
const mergeDecision = await maybePromptMergeStrategy(config, workspaceRoot, Boolean(commandOptions.merge));
|
|
649
695
|
if (mergeDecision?.aborted) {
|
|
650
696
|
console.log(chalk.yellow('Aborting init to avoid overwriting existing i18n assets. Re-run with --merge to bypass.'));
|
|
@@ -787,35 +833,33 @@ async function maybePromptMergeStrategy(
|
|
|
787
833
|
}
|
|
788
834
|
}
|
|
789
835
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
]);
|
|
799
|
-
if (!proceed) {
|
|
800
|
-
return { strategy: null, aborted: true };
|
|
801
|
-
}
|
|
802
|
-
}
|
|
836
|
+
// Single consolidated prompt for both --merge and interactive flows. When
|
|
837
|
+
// `--merge` is provided we omit the 'Abort' choice and default to
|
|
838
|
+
// 'keep-source'; otherwise include an explicit 'Abort' option (last).
|
|
839
|
+
const baseChoices = [
|
|
840
|
+
{ name: 'Keep source values (append new keys only)', value: 'keep-source' },
|
|
841
|
+
{ name: 'Overwrite with placeholders (backup first)', value: 'overwrite' },
|
|
842
|
+
{ name: 'Prompt during sync to review changes interactively', value: 'interactive' },
|
|
843
|
+
];
|
|
803
844
|
|
|
804
|
-
const
|
|
845
|
+
const choices = mergeRequested ? baseChoices : [...baseChoices, { name: 'Abort (do not touch existing files)', value: 'abort' }];
|
|
846
|
+
|
|
847
|
+
const { strategy } = await inquirer.prompt<{
|
|
848
|
+
strategy: 'abort' | MergeStrategy;
|
|
849
|
+
}>([
|
|
805
850
|
{
|
|
806
851
|
type: 'list',
|
|
807
852
|
name: 'strategy',
|
|
808
|
-
message: '
|
|
809
|
-
choices
|
|
810
|
-
{ name: 'Keep source values (append new keys only)', value: 'keep-source' },
|
|
811
|
-
{ name: 'Overwrite with placeholders (backup first)', value: 'overwrite' },
|
|
812
|
-
{ name: 'Interactive review during sync', value: 'interactive' },
|
|
813
|
-
],
|
|
853
|
+
message: 'Existing i18n assets detected — choose how to proceed',
|
|
854
|
+
choices,
|
|
814
855
|
default: 'keep-source',
|
|
815
856
|
},
|
|
816
857
|
]);
|
|
817
858
|
|
|
818
|
-
|
|
859
|
+
if (strategy === 'abort') {
|
|
860
|
+
return { strategy: null, aborted: true };
|
|
861
|
+
}
|
|
862
|
+
return { strategy: strategy as MergeStrategy, aborted: false };
|
|
819
863
|
} catch (error) {
|
|
820
864
|
console.warn(chalk.gray(`Skipping merge diagnostics: ${(error as Error).message}`));
|
|
821
865
|
return { strategy: null, aborted: false };
|
|
@@ -833,7 +877,8 @@ async function applyMergeStrategy(
|
|
|
833
877
|
}
|
|
834
878
|
|
|
835
879
|
const localesDirPath = path.join(workspaceRoot, config.localesDir || 'locales');
|
|
836
|
-
const
|
|
880
|
+
const container = getServiceContainer({ workspaceRoot });
|
|
881
|
+
const localeStore = container.localeStoreFactory.create(localesDirPath, {
|
|
837
882
|
format: config.locales?.format ?? 'auto',
|
|
838
883
|
delimiter: config.locales?.delimiter ?? '.',
|
|
839
884
|
sortKeys: config.locales?.sortKeys ?? 'alphabetical',
|
|
@@ -841,10 +886,9 @@ async function applyMergeStrategy(
|
|
|
841
886
|
|
|
842
887
|
const sourceLocale = config.sourceLanguage ?? 'en';
|
|
843
888
|
let sourceData: Record<string, string> = {};
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
sourceData = {};
|
|
889
|
+
const sourceResult = await localeStore.get(sourceLocale);
|
|
890
|
+
if (sourceResult.success) {
|
|
891
|
+
sourceData = sourceResult.data;
|
|
848
892
|
}
|
|
849
893
|
|
|
850
894
|
const sourceKeys = Object.keys(sourceData);
|
|
@@ -888,10 +932,9 @@ async function applyMergeStrategy(
|
|
|
888
932
|
const seedValue = config.sync?.seedValue ?? '[TODO]';
|
|
889
933
|
for (const locale of localesToOverwrite) {
|
|
890
934
|
let existingData: Record<string, string> = {};
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
existingData = {};
|
|
935
|
+
const existingResult = await localeStore.get(locale);
|
|
936
|
+
if (existingResult.success) {
|
|
937
|
+
existingData = existingResult.data;
|
|
895
938
|
}
|
|
896
939
|
for (const key of Object.keys(existingData)) {
|
|
897
940
|
await localeStore.remove(locale, key);
|
package/src/commands/rename.ts
CHANGED
|
@@ -2,10 +2,11 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { promises as fs } from 'node:fs';
|
|
5
|
-
import { loadConfig,
|
|
5
|
+
import { loadConfig, type KeyRenameSummary, type KeyRenameBatchSummary, type KeyRenameMapping } from '@i18nsmith/core';
|
|
6
6
|
import { applyPreviewFile, writePreviewFile } from '../utils/preview.js';
|
|
7
7
|
import { runAdapterPreflight } from '../utils/adapter-preflight.js';
|
|
8
8
|
import { CliError, withErrorHandling } from '../utils/errors.js';
|
|
9
|
+
import { getServiceContainer } from '../utils/bootstrap.js';
|
|
9
10
|
|
|
10
11
|
interface ScanOptions {
|
|
11
12
|
config: string;
|
|
@@ -66,12 +67,19 @@ export function registerRename(program: Command): void {
|
|
|
66
67
|
await runAdapterPreflight();
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
const
|
|
70
|
+
// Use ServiceContainer for rename service
|
|
71
|
+
const container = getServiceContainer();
|
|
72
|
+
const renameResult = await container.keyRenamer.rename(config, oldKey, newKey, {
|
|
71
73
|
write: options.write,
|
|
72
74
|
diff: options.diff || previewMode,
|
|
73
75
|
});
|
|
74
76
|
|
|
77
|
+
if (!renameResult.success) {
|
|
78
|
+
throw new CliError(`Rename failed: ${renameResult.error.message}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const summary = renameResult.data;
|
|
82
|
+
|
|
75
83
|
if (previewMode && options.previewOutput) {
|
|
76
84
|
const savedPath = await writePreviewFile('rename-key', summary, options.previewOutput);
|
|
77
85
|
console.log(chalk.green(`Preview written to ${path.relative(process.cwd(), savedPath)}`));
|
|
@@ -127,8 +135,19 @@ export function registerRename(program: Command): void {
|
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
const mappings = await loadRenameMappings(options.map);
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
|
|
139
|
+
// Use ServiceContainer for batch rename
|
|
140
|
+
const container = getServiceContainer();
|
|
141
|
+
const batchResult = await container.keyRenamer.renameBatch(config, mappings, {
|
|
142
|
+
write: options.write,
|
|
143
|
+
diff: options.diff
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!batchResult.success) {
|
|
147
|
+
throw new CliError(`Batch rename failed: ${batchResult.error.message}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const summary = batchResult.data;
|
|
132
151
|
|
|
133
152
|
if (options.report) {
|
|
134
153
|
const outputPath = path.resolve(process.cwd(), options.report);
|
package/src/commands/review.ts
CHANGED
|
@@ -3,7 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
5
|
import type { Command } from 'commander';
|
|
6
|
-
import { loadConfigWithMeta,
|
|
6
|
+
import { loadConfigWithMeta, type ScanCandidate, type ScanSummary } from '@i18nsmith/core';
|
|
7
|
+
import { getServiceContainer } from '../utils/bootstrap.js';
|
|
7
8
|
import { CliError, withErrorHandling } from '../utils/errors.js';
|
|
8
9
|
|
|
9
10
|
interface ReviewCommandOptions {
|
|
@@ -138,8 +139,14 @@ export function registerReview(program: Command) {
|
|
|
138
139
|
withErrorHandling(async (options: ReviewCommandOptions) => {
|
|
139
140
|
try {
|
|
140
141
|
const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
|
|
141
|
-
const
|
|
142
|
-
const
|
|
142
|
+
const container = getServiceContainer({ workspaceRoot: projectRoot });
|
|
143
|
+
const result = await container.scanner.scan(config, { scanCalls: options.scanCalls });
|
|
144
|
+
|
|
145
|
+
if (!result.success) {
|
|
146
|
+
throw new CliError(result.error.message);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const summary = result.data as BucketedScanSummary;
|
|
143
150
|
const buckets = summary.buckets ?? {};
|
|
144
151
|
const needsReview = buckets.needsReview ?? [];
|
|
145
152
|
const skipped = buckets.skipped ?? [];
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import {
|
|
4
|
+
import { loadConfig } from '@i18nsmith/core';
|
|
5
|
+
import { getServiceContainer } from '../utils/bootstrap.js';
|
|
5
6
|
import { scaffoldTranslationContext, scaffoldI18next } from '../utils/scaffold.js';
|
|
6
7
|
import { readPackageJson, hasDependency } from '../utils/pkg.js';
|
|
7
8
|
import { detectPackageManager, installDependencies } from '../utils/package-manager.js';
|
|
@@ -233,7 +234,15 @@ export function registerScaffoldAdapter(program: Command) {
|
|
|
233
234
|
async function detectExistingRuntime(): Promise<string | null> {
|
|
234
235
|
try {
|
|
235
236
|
const config = await loadConfig();
|
|
236
|
-
const
|
|
237
|
+
const container = getServiceContainer();
|
|
238
|
+
const result = await container.diagnostics.diagnose(config);
|
|
239
|
+
|
|
240
|
+
if (!result.success) {
|
|
241
|
+
console.warn(chalk.gray(`Skipping adapter detection: ${result.error.message}`));
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const report = result.data;
|
|
237
246
|
type AdapterInfo = (typeof report.adapterFiles)[number];
|
|
238
247
|
type ProviderInfo = (typeof report.providerFiles)[number];
|
|
239
248
|
|
package/src/commands/scan.ts
CHANGED
|
@@ -2,9 +2,10 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import type { Command } from 'commander';
|
|
5
|
-
import { loadConfigWithMeta
|
|
5
|
+
import { loadConfigWithMeta } from '@i18nsmith/core';
|
|
6
6
|
import type { ScanCandidate } from '@i18nsmith/core';
|
|
7
7
|
import { CliError, withErrorHandling } from '../utils/errors.js';
|
|
8
|
+
import { getServiceContainer } from '../utils/bootstrap.js';
|
|
8
9
|
|
|
9
10
|
interface ScanOptions {
|
|
10
11
|
config?: string;
|
|
@@ -57,7 +58,14 @@ export function registerScan(program: Command) {
|
|
|
57
58
|
.option('--exclude <patterns...>', 'Override exclude globs from config (comma or space separated)', collectTargetPatterns, [])
|
|
58
59
|
.action(
|
|
59
60
|
withErrorHandling(async (options: ScanOptions) => {
|
|
60
|
-
|
|
61
|
+
// When JSON output is requested, avoid human-readable preamble on stdout
|
|
62
|
+
// so callers can reliably parse the JSON summary. Send banner to stderr
|
|
63
|
+
// when --json is used.
|
|
64
|
+
if (options.json) {
|
|
65
|
+
console.error(chalk.blue('Starting scan...'));
|
|
66
|
+
} else {
|
|
67
|
+
console.log(chalk.blue('Starting scan...'));
|
|
68
|
+
}
|
|
61
69
|
|
|
62
70
|
try {
|
|
63
71
|
const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
|
|
@@ -75,11 +83,16 @@ export function registerScan(program: Command) {
|
|
|
75
83
|
if (options.exclude?.length) {
|
|
76
84
|
config.exclude = options.exclude;
|
|
77
85
|
}
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
86
|
+
|
|
87
|
+
// Use ServiceContainer for scanning - provides consistent DI
|
|
88
|
+
const container = getServiceContainer({ workspaceRoot: projectRoot });
|
|
89
|
+
const result = await container.scanner.scan(config);
|
|
90
|
+
|
|
91
|
+
if (!result.success) {
|
|
92
|
+
throw new CliError(`Scan failed: ${result.error.message}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const summary = result.data;
|
|
83
96
|
|
|
84
97
|
if (options.report) {
|
|
85
98
|
const outputPath = path.resolve(process.cwd(), options.report);
|