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.
Files changed (42) hide show
  1. package/README.md +126 -0
  2. package/build.mjs +5 -0
  3. package/dist/commands/backup.d.ts.map +1 -1
  4. package/dist/commands/check.d.ts.map +1 -1
  5. package/dist/commands/config.d.ts.map +1 -1
  6. package/dist/commands/coverage.d.ts.map +1 -1
  7. package/dist/commands/detect.d.ts.map +1 -1
  8. package/dist/commands/diagnose.d.ts.map +1 -1
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/rename.d.ts.map +1 -1
  11. package/dist/commands/review.d.ts.map +1 -1
  12. package/dist/commands/scaffold-adapter.d.ts.map +1 -1
  13. package/dist/commands/scan.d.ts.map +1 -1
  14. package/dist/commands/sync.d.ts.map +1 -1
  15. package/dist/commands/transform.d.ts.map +1 -1
  16. package/dist/index.cjs +13069 -6794
  17. package/dist/services/index.d.ts +12 -0
  18. package/dist/services/index.d.ts.map +1 -0
  19. package/dist/services/transform-service.d.ts +64 -0
  20. package/dist/services/transform-service.d.ts.map +1 -0
  21. package/dist/utils/bootstrap.d.ts +83 -0
  22. package/dist/utils/bootstrap.d.ts.map +1 -0
  23. package/package.json +1 -1
  24. package/src/commands/backup.ts +28 -10
  25. package/src/commands/check.ts +16 -4
  26. package/src/commands/config.ts +126 -0
  27. package/src/commands/coverage.ts +11 -4
  28. package/src/commands/detect.ts +11 -4
  29. package/src/commands/diagnose.ts +12 -2
  30. package/src/commands/init.test.ts +80 -0
  31. package/src/commands/init.ts +92 -49
  32. package/src/commands/rename.ts +24 -5
  33. package/src/commands/review.ts +10 -3
  34. package/src/commands/scaffold-adapter.ts +11 -2
  35. package/src/commands/scan.ts +20 -7
  36. package/src/commands/sync.ts +91 -23
  37. package/src/commands/transform.ts +8 -1
  38. package/src/index.ts +1 -1
  39. package/src/integration.test.ts +145 -12
  40. package/src/services/index.ts +12 -0
  41. package/src/services/transform-service.ts +203 -0
  42. package/src/utils/bootstrap.ts +221 -0
@@ -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, ProjectIntelligenceService, type ProjectIntelligence, type SuggestedConfig, Scanner, KeyGenerator, LocaleStore, createBackup } from '@i18nsmith/core';
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 service = new ProjectIntelligenceService();
80
- const result = await service.analyze({ workspaceRoot });
81
- return result;
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, confidence } = intelligence;
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 service = new ProjectIntelligenceService();
135
- suggestedConfig = service.applyTemplate(commandOptions.template, intelligence);
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
- keyGeneration: suggestedConfig.keyGeneration,
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 scanner = await Scanner.create(config, { workspaceRoot });
220
- const scanResult = await scanner.scan();
248
+ const container = getServiceContainer({ workspaceRoot });
249
+ const scanResult = await container.scanner.scan(config);
221
250
 
222
- if (scanResult.buckets.highConfidence.length > 0) {
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 keyGenerator = new KeyGenerator({
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.generate(candidate.text, {
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 service = new ProjectIntelligenceService();
605
+ const container = getServiceContainer({ workspaceRoot });
566
606
  // Uses pre-detected intelligence from outer scope
567
- const suggestedConfig = service.applyTemplate(answers.template!, intelligence || {
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
- if (!mergeRequested) {
791
- const { proceed } = await inquirer.prompt<{ proceed: boolean }>([
792
- {
793
- type: 'confirm',
794
- name: 'proceed',
795
- message: 'Merge with the existing setup instead of overwriting?',
796
- default: true,
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 { strategy } = await inquirer.prompt<{ strategy: MergeStrategy }>([
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: 'Choose a merge strategy for existing locale keys',
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
- return { strategy, aborted: false };
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 localeStore = new LocaleStore(localesDirPath, {
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
- try {
845
- sourceData = await localeStore.get(sourceLocale);
846
- } catch {
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
- try {
892
- existingData = await localeStore.get(locale);
893
- } catch {
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);
@@ -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, KeyRenamer, type KeyRenameSummary, type KeyRenameBatchSummary, type KeyRenameMapping } from '@i18nsmith/core';
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
- const renamer = new KeyRenamer(config);
70
- const summary = await renamer.rename(oldKey, newKey, {
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
- const renamer = new KeyRenamer(config);
131
- const summary = await renamer.renameBatch(mappings, { write: options.write, diff: options.diff });
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);
@@ -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, Scanner, type ScanCandidate, type ScanSummary } from '@i18nsmith/core';
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 scanner = await Scanner.create(config, { workspaceRoot: projectRoot });
142
- const summary = scanner.scan({ scanCalls: options.scanCalls }) as BucketedScanSummary;
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 { diagnoseWorkspace, loadConfig } from '@i18nsmith/core';
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 report = await diagnoseWorkspace(config);
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
 
@@ -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, Scanner } from '@i18nsmith/core';
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
- console.log(chalk.blue('Starting scan...'));
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
- // Use factory that registers framework adapters (React/Vue) so
79
- // scans include files handled by adapters. Backwards compatible
80
- // API: Scanner.create will return a scanner with adapters wired.
81
- const scanner = await Scanner.create(config, { workspaceRoot: projectRoot });
82
- const summary = scanner.scan();
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);