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
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @fileoverview
3
+ * CLI Services Module
4
+ *
5
+ * CLI-level service implementations that cannot live in @i18nsmith/core
6
+ * due to dependency constraints.
7
+ *
8
+ * @module @i18nsmith/cli/services
9
+ */
10
+ export { CliTransformService, getTransformService } from './transform-service.js';
11
+ export type { CliTransformServiceConfig } from './transform-service.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAClF,YAAY,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @fileoverview
3
+ * CLI Transform Service - Implements ITransformService for CLI usage
4
+ *
5
+ * This service wraps @i18nsmith/transformer's Transformer class and provides
6
+ * the ITransformService interface. It lives in CLI because:
7
+ * - @i18nsmith/core should NOT depend on @i18nsmith/transformer
8
+ * - CLI depends on both packages, making this the natural location
9
+ *
10
+ * @module @i18nsmith/cli/services/transform-service
11
+ */
12
+ import { type I18nConfig, type ITransformService, type TransformOptions, type TransformSummaryBase, type ScanCandidate, type Result } from '@i18nsmith/core';
13
+ /**
14
+ * Configuration for the CLI transform service.
15
+ */
16
+ export interface CliTransformServiceConfig {
17
+ /** Workspace root directory */
18
+ readonly workspaceRoot: string;
19
+ }
20
+ /**
21
+ * CLI implementation of ITransformService.
22
+ *
23
+ * Wraps the @i18nsmith/transformer Transformer class with the standard
24
+ * Result-based error handling pattern used across the service layer.
25
+ */
26
+ export declare class CliTransformService implements ITransformService {
27
+ private readonly workspaceRoot;
28
+ constructor(config: CliTransformServiceConfig);
29
+ /**
30
+ * Transforms source files to use translation functions.
31
+ */
32
+ transform(config: I18nConfig, options?: TransformOptions): Promise<Result<TransformSummaryBase>>;
33
+ /**
34
+ * Transforms specific candidates (from a previous scan).
35
+ *
36
+ * Note: The current Transformer doesn't have a direct method for this,
37
+ * so we run a full transform with targets limited to the candidate files.
38
+ */
39
+ transformCandidates(config: I18nConfig, candidates: readonly ScanCandidate[], options?: TransformOptions): Promise<Result<TransformSummaryBase>>;
40
+ /**
41
+ * Generates a preview of transformation without applying.
42
+ */
43
+ preview(config: I18nConfig, options?: Omit<TransformOptions, 'write'>): Promise<Result<TransformSummaryBase>>;
44
+ }
45
+ /**
46
+ * Creates a CLI transform service instance.
47
+ *
48
+ * @param workspaceRoot - The workspace root directory
49
+ * @returns ITransformService instance
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * import { getTransformService } from '../services/transform-service.js';
54
+ *
55
+ * const transformService = getTransformService(projectRoot);
56
+ * const result = await transformService.transform(config, { write: true });
57
+ *
58
+ * if (!result.success) {
59
+ * throw new CliError(result.error.message);
60
+ * }
61
+ * ```
62
+ */
63
+ export declare function getTransformService(workspaceRoot: string): ITransformService;
64
+ //# sourceMappingURL=transform-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform-service.d.ts","sourceRoot":"","sources":["../../src/services/transform-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EACL,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,MAAM,EAIZ,MAAM,iBAAiB,CAAC;AAMzB;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,+BAA+B;IAC/B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAqDD;;;;;GAKG;AACH,qBAAa,mBAAoB,YAAW,iBAAiB;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,MAAM,EAAE,yBAAyB;IAI7C;;OAEG;IACG,SAAS,CACb,MAAM,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAuBxC;;;;;OAKG;IACG,mBAAmB,CACvB,MAAM,EAAE,UAAU,EAClB,UAAU,EAAE,SAAS,aAAa,EAAE,EACpC,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IA0BxC;;OAEG;IACG,OAAO,CACX,MAAM,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC,GACxC,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;CAGzC;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,iBAAiB,CAE5E"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @fileoverview
3
+ * CLI Bootstrap - Service Container Setup for CLI
4
+ *
5
+ * This module provides a pre-configured ServiceContainer with Node.js
6
+ * platform adapters. CLI commands can import this to use the service layer.
7
+ *
8
+ * Following Hexagonal Architecture:
9
+ * - The ServiceContainer is the "Application Layer"
10
+ * - This bootstrap module configures "Adapters" for CLI (Node.js) environment
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { getServiceContainer } from '../utils/bootstrap.js';
15
+ *
16
+ * async function myCommand() {
17
+ * const container = getServiceContainer();
18
+ * const result = await container.scanner.scan(config);
19
+ * // ...
20
+ * }
21
+ * ```
22
+ *
23
+ * @module @i18nsmith/cli/utils/bootstrap
24
+ */
25
+ import { ServiceContainer, type ServiceContainerConfig } from '@i18nsmith/core';
26
+ import type { ILogger, LogLevel, ILoggerFactory } from '@i18nsmith/core';
27
+ /**
28
+ * CLI-specific logger that outputs with chalk formatting.
29
+ */
30
+ declare class CliLogger implements ILogger {
31
+ private currentLevel;
32
+ private readonly scope;
33
+ private readonly levelOrder;
34
+ constructor(scope?: string, level?: LogLevel);
35
+ private shouldLog;
36
+ trace(message: string, data?: Record<string, unknown>): void;
37
+ debug(message: string, data?: Record<string, unknown>): void;
38
+ info(message: string, data?: Record<string, unknown>): void;
39
+ warn(message: string, data?: Record<string, unknown>): void;
40
+ error(message: string, error?: Error, data?: Record<string, unknown>): void;
41
+ child(scope: string): ILogger;
42
+ getLevel(): LogLevel;
43
+ setLevel(level: LogLevel): void;
44
+ }
45
+ /**
46
+ * Factory for creating CLI loggers.
47
+ */
48
+ declare class CliLoggerFactory implements ILoggerFactory {
49
+ private readonly level;
50
+ private readonly rootLogger;
51
+ constructor(level?: LogLevel);
52
+ createLogger(scope: string): ILogger;
53
+ getRootLogger(): ILogger;
54
+ setGlobalLevel(level: LogLevel): void;
55
+ }
56
+ /**
57
+ * Sets the global log level for CLI operations.
58
+ */
59
+ export declare function setLogLevel(level: LogLevel): void;
60
+ /**
61
+ * Gets the global service container, creating it if needed.
62
+ *
63
+ * @param options - Optional configuration overrides
64
+ * @returns Configured ServiceContainer
65
+ */
66
+ export declare function getServiceContainer(options?: {
67
+ workspaceRoot?: string;
68
+ logLevel?: LogLevel;
69
+ }): ServiceContainer;
70
+ /**
71
+ * Creates a fresh service container (useful for testing or scoped operations).
72
+ *
73
+ * @param config - Container configuration
74
+ * @returns New ServiceContainer instance
75
+ */
76
+ export declare function createCliServiceContainer(config?: ServiceContainerConfig): ServiceContainer;
77
+ /**
78
+ * Resets the global container (useful for testing).
79
+ */
80
+ export declare function resetServiceContainer(): void;
81
+ export { CliLogger, CliLoggerFactory };
82
+ export type { LogLevel };
83
+ //# sourceMappingURL=bootstrap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../src/utils/bootstrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,EACL,gBAAgB,EAIhB,KAAK,sBAAsB,EAE5B,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAMzE;;GAEG;AACH,cAAM,SAAU,YAAW,OAAO;IAChC,OAAO,CAAC,YAAY,CAAW;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAMzB;gBAEU,KAAK,GAAE,MAAc,EAAE,KAAK,GAAE,QAAiB;IAK3D,OAAO,CAAC,SAAS;IAIjB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM5D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM5D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM3D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM3D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAY3E,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAI7B,QAAQ,IAAI,QAAQ;IAIpB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;CAGhC;AAED;;GAEG;AACH,cAAM,gBAAiB,YAAW,cAAc;IAC9C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAW;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAY;gBAE3B,KAAK,GAAE,QAAiB;IAKpC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIpC,aAAa,IAAI,OAAO;IAIxB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;CAGtC;AASD;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAMjD;AAYD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAC5C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB,GAAG,gBAAgB,CAgBnB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,CAAC,EAAE,sBAAsB,GAAG,gBAAgB,CAQ3F;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAMD,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,CAAC;AACvC,YAAY,EAAE,QAAQ,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18nsmith",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "description": "CLI for i18nsmith",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -1,8 +1,8 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import inquirer from 'inquirer';
4
- import { listBackups, restoreBackup } from '@i18nsmith/core';
5
4
  import { CliError, withErrorHandling } from '../utils/errors.js';
5
+ import { getServiceContainer } from '../utils/bootstrap.js';
6
6
 
7
7
  /**
8
8
  * Registers backup-related commands (backup-list, backup-restore)
@@ -13,10 +13,16 @@ export function registerBackup(program: Command): void {
13
13
  .description('List available locale file backups')
14
14
  .option('--backup-dir <path>', 'Custom backup directory (default: .i18nsmith-backup)')
15
15
  .action(
16
- withErrorHandling(async (options: { backupDir?: string }) => {
16
+ withErrorHandling(async (_options: { backupDir?: string }) => {
17
17
  try {
18
- const workspaceRoot = process.cwd();
19
- const backups = await listBackups(workspaceRoot, { backupDir: options.backupDir });
18
+ const container = getServiceContainer();
19
+ const result = await container.backup.list();
20
+
21
+ if (!result.success) {
22
+ throw new CliError(`Error listing backups: ${result.error.message}`);
23
+ }
24
+
25
+ const backups = result.data;
20
26
 
21
27
  if (backups.length === 0) {
22
28
  console.log(chalk.yellow('No backups found.'));
@@ -34,6 +40,7 @@ export function registerBackup(program: Command): void {
34
40
 
35
41
  console.log(chalk.gray(`\nRestore a backup with: i18nsmith backup-restore <timestamp>`));
36
42
  } catch (error) {
43
+ if (error instanceof CliError) throw error;
37
44
  const message = error instanceof Error ? error.message : String(error);
38
45
  throw new CliError(`Error listing backups: ${message}`);
39
46
  }
@@ -46,10 +53,16 @@ export function registerBackup(program: Command): void {
46
53
  .argument('<timestamp>', 'Backup timestamp (from backup-list) or "latest" for most recent')
47
54
  .option('--backup-dir <path>', 'Custom backup directory (default: .i18nsmith-backup)')
48
55
  .action(
49
- withErrorHandling(async (timestamp: string, options: { backupDir?: string }) => {
56
+ withErrorHandling(async (timestamp: string, _options: { backupDir?: string }) => {
50
57
  try {
51
- const workspaceRoot = process.cwd();
52
- const backups = await listBackups(workspaceRoot, { backupDir: options.backupDir });
58
+ const container = getServiceContainer();
59
+ const listResult = await container.backup.list();
60
+
61
+ if (!listResult.success) {
62
+ throw new CliError(`Error listing backups: ${listResult.error.message}`);
63
+ }
64
+
65
+ const backups = listResult.data;
53
66
 
54
67
  if (backups.length === 0) {
55
68
  throw new CliError('No backups found.');
@@ -85,13 +98,18 @@ export function registerBackup(program: Command): void {
85
98
  return;
86
99
  }
87
100
 
88
- const result = await restoreBackup(targetBackup.path, workspaceRoot);
101
+ const result = await container.backup.restore(targetBackup.path);
102
+
103
+ if (!result.success) {
104
+ throw new CliError(`Error restoring backup: ${result.error.message}`);
105
+ }
89
106
 
90
- console.log(chalk.green(`\n✅ ${result.summary}`));
91
- for (const file of result.restored) {
107
+ console.log(chalk.green(`\n✅ ${result.data.summary}`));
108
+ for (const file of result.data.restored) {
92
109
  console.log(chalk.gray(` Restored: ${file}`));
93
110
  }
94
111
  } catch (error) {
112
+ if (error instanceof CliError) throw error;
95
113
  const message = error instanceof Error ? error.message : String(error);
96
114
  throw new CliError(`Error restoring backup: ${message}`);
97
115
  }
@@ -2,7 +2,7 @@ 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, CheckRunner, isPackageResolvable } from '@i18nsmith/core';
5
+ import { loadConfigWithMeta, isPackageResolvable } from '@i18nsmith/core';
6
6
  import type { CheckSummary, I18nConfig } from '@i18nsmith/core';
7
7
  import { printLocaleDiffs } from '../utils/diff-utils.js';
8
8
  import { getDiagnosisExitSignal } from '../utils/diagnostics-exit.js';
@@ -14,6 +14,7 @@ import {
14
14
  type LocaleAuditSummary,
15
15
  } from '../utils/locale-audit.js';
16
16
  import { CliError, withErrorHandling } from '../utils/errors.js';
17
+ import { getServiceContainer } from '../utils/bootstrap.js';
17
18
 
18
19
  interface CheckCommandOptions {
19
20
  config?: string;
@@ -186,7 +187,11 @@ export function registerCheck(program: Command) {
186
187
 
187
188
  export async function runCheck(options: CheckCommandOptions): Promise<void> {
188
189
  const auditEnabled = Boolean(options.audit || options.auditStrict);
189
- console.log(chalk.blue('Running guided repository health check...'));
190
+ if (options.json) {
191
+ console.error(chalk.blue('Running guided repository health check...'));
192
+ } else {
193
+ console.log(chalk.blue('Running guided repository health check...'));
194
+ }
190
195
  try {
191
196
  const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
192
197
 
@@ -218,8 +223,9 @@ export async function runCheck(options: CheckCommandOptions): Promise<void> {
218
223
  console.log(chalk.yellow(' Some Vue template references may be skipped.'));
219
224
  }
220
225
 
221
- const runner = new CheckRunner(config, { workspaceRoot: projectRoot });
222
- const summary = await runner.run({
226
+ // Use ServiceContainer for check service
227
+ const container = getServiceContainer({ workspaceRoot: projectRoot });
228
+ const checkResult = await container.check.run(config, {
223
229
  assumedKeys: options.assume,
224
230
  validateInterpolations: options.validateInterpolations,
225
231
  emptyValuePolicy: options.emptyValues === false ? 'fail' : undefined,
@@ -228,6 +234,12 @@ export async function runCheck(options: CheckCommandOptions): Promise<void> {
228
234
  invalidateCache: options.invalidateCache,
229
235
  });
230
236
 
237
+ if (!checkResult.success) {
238
+ throw new CliError(`Health check failed: ${checkResult.error.message}`);
239
+ }
240
+
241
+ const summary = checkResult.data;
242
+
231
243
  let localeAudit: LocaleAuditSummary | undefined;
232
244
  if (auditEnabled) {
233
245
  const auditLocales = options.auditLocales?.filter(Boolean) ?? [];
@@ -282,4 +282,130 @@ export function registerConfig(program: Command) {
282
282
  }
283
283
  })
284
284
  );
285
+
286
+ // Subcommand: config migrate
287
+ configCmd
288
+ .command('migrate')
289
+ .description('Migrate configuration from v1 to v2 format and apply modern defaults')
290
+ .option('-c, --config <path>', 'Path to i18nsmith config file', DEFAULT_CONFIG_FILENAME)
291
+ .option('--dry-run', 'Show what would be changed without modifying files', false)
292
+ .option('--json', 'Output result as JSON', false)
293
+ .action(
294
+ withErrorHandling(async (options: { config?: string; dryRun?: boolean; json?: boolean }) => {
295
+ try {
296
+ const { configPath } = await loadConfigWithMeta(options.config);
297
+ const { parsed: rawConfig } = await readRawConfig(configPath);
298
+
299
+ // Check if migration is needed
300
+ const currentVersion = (rawConfig.configVersion ?? rawConfig.version ?? 1) as number;
301
+ if (currentVersion >= 2) {
302
+ if (options.json) {
303
+ console.log(JSON.stringify({ migrated: false, reason: 'Already at latest version' }, null, 2));
304
+ } else {
305
+ console.log(chalk.blue('Configuration is already up to date (v2 or later)'));
306
+ }
307
+ return;
308
+ }
309
+
310
+ // Apply migration transformations
311
+ const migratedConfig = { ...rawConfig };
312
+
313
+ // Set new version
314
+ migratedConfig.configVersion = 2;
315
+ delete migratedConfig.version;
316
+
317
+ // Migrate field names
318
+ if (rawConfig.sourceLocale) {
319
+ migratedConfig.sourceLanguage = rawConfig.sourceLocale;
320
+ delete migratedConfig.sourceLocale;
321
+ }
322
+ if (rawConfig.targetLocales) {
323
+ migratedConfig.targetLanguages = rawConfig.targetLocales;
324
+ delete migratedConfig.targetLocales;
325
+ }
326
+
327
+ // Add extraction preset with strict defaults for new configs
328
+ if (!migratedConfig.extraction) {
329
+ migratedConfig.extraction = {};
330
+ }
331
+ const extraction = migratedConfig.extraction as Record<string, unknown>;
332
+ if (!extraction.preset) {
333
+ extraction.preset = 'strict';
334
+ }
335
+
336
+ // Clean up deprecated fields
337
+ const deprecatedFields = ['projectName'];
338
+ for (const field of deprecatedFields) {
339
+ if (field in migratedConfig) {
340
+ delete migratedConfig[field];
341
+ }
342
+ }
343
+
344
+ if (options.dryRun) {
345
+ if (options.json) {
346
+ console.log(JSON.stringify({
347
+ migrated: true,
348
+ dryRun: true,
349
+ changes: {
350
+ added: { configVersion: 2, 'extraction.preset': 'strict' },
351
+ removed: deprecatedFields.filter(f => f in rawConfig),
352
+ renamed: Object.assign({},
353
+ rawConfig.sourceLocale ? { sourceLocale: 'sourceLanguage' } : {},
354
+ rawConfig.targetLocales ? { targetLocales: 'targetLanguages' } : {},
355
+ ),
356
+ },
357
+ result: migratedConfig,
358
+ }, null, 2));
359
+ } else {
360
+ console.log(chalk.blue('Migration preview (dry run):'));
361
+ console.log();
362
+ console.log(chalk.green('✓ Would set configVersion: 2'));
363
+ console.log(chalk.green('✓ Would add extraction.preset: strict'));
364
+ if (rawConfig.sourceLocale) {
365
+ console.log(chalk.green(`✓ Would rename sourceLocale → sourceLanguage`));
366
+ }
367
+ if (rawConfig.targetLocales) {
368
+ console.log(chalk.green(`✓ Would rename targetLocales → targetLanguages`));
369
+ }
370
+ const removed = deprecatedFields.filter(f => f in rawConfig);
371
+ if (removed.length > 0) {
372
+ console.log(chalk.green(`✓ Would remove deprecated fields: ${removed.join(', ')}`));
373
+ }
374
+ console.log();
375
+ console.log(chalk.dim('Run without --dry-run to apply changes'));
376
+ }
377
+ } else {
378
+ await writeConfig(configPath, migratedConfig);
379
+
380
+ if (options.json) {
381
+ console.log(JSON.stringify({
382
+ migrated: true,
383
+ configPath,
384
+ changes: {
385
+ added: { configVersion: 2, 'extraction.preset': 'strict' },
386
+ removed: deprecatedFields.filter(f => f in rawConfig),
387
+ renamed: Object.assign({},
388
+ rawConfig.sourceLocale ? { sourceLocale: 'sourceLanguage' } : {},
389
+ rawConfig.targetLocales ? { targetLocales: 'targetLanguages' } : {},
390
+ ),
391
+ },
392
+ }, null, 2));
393
+ } else {
394
+ console.log(chalk.green('✓ Configuration migrated to v2'));
395
+ console.log(chalk.dim(` Updated: ${configPath}`));
396
+ console.log();
397
+ console.log(chalk.blue('New features available:'));
398
+ console.log(chalk.dim(' • extraction.preset: strict (reduces false positives)'));
399
+ console.log(chalk.dim(' • Improved field names for clarity'));
400
+ }
401
+ }
402
+ } catch (error) {
403
+ if (error instanceof CliError) {
404
+ throw error;
405
+ }
406
+ const message = error instanceof Error ? error.message : String(error);
407
+ throw new CliError(`Failed to migrate config: ${message}`);
408
+ }
409
+ })
410
+ );
285
411
  }
@@ -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, Syncer } from '@i18nsmith/core';
5
+ import { loadConfigWithMeta } from '@i18nsmith/core';
6
6
  import type { DynamicKeyCoverage, I18nConfig } from '@i18nsmith/core';
7
7
  import { CliError, withErrorHandling } from '../utils/errors.js';
8
+ import { getServiceContainer } from '../utils/bootstrap.js';
8
9
 
9
10
  interface CoverageCommandOptions {
10
11
  config?: string;
@@ -69,14 +70,20 @@ export function registerCoverage(program: Command) {
69
70
 
70
71
  export async function runCoverage(options: CoverageCommandOptions): Promise<void> {
71
72
  const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
72
- const syncer = new Syncer(config, { workspaceRoot: projectRoot });
73
-
74
- const summary = await syncer.run({
73
+
74
+ // Use ServiceContainer for sync operations
75
+ const container = getServiceContainer({ workspaceRoot: projectRoot });
76
+ const syncResult = await container.sync.sync(config, {
75
77
  write: false,
76
78
  targets: options.target,
77
79
  invalidateCache: options.invalidateCache,
78
80
  });
79
81
 
82
+ if (!syncResult.success) {
83
+ throw new CliError(`Coverage analysis failed: ${syncResult.error.message}`);
84
+ }
85
+
86
+ const summary = syncResult.data;
80
87
  const coverage = summary.dynamicKeyCoverage ?? [];
81
88
  const report = buildCoverageReport(coverage, config, projectRoot, configPath);
82
89
 
@@ -3,11 +3,11 @@ import path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import type { Command } from 'commander';
5
5
  import {
6
- ProjectIntelligenceService,
7
6
  type ProjectIntelligence,
8
7
  type ConfidenceLevel,
9
8
  } from '@i18nsmith/core';
10
- import { withErrorHandling } from '../utils/errors.js';
9
+ import { withErrorHandling, CliError } from '../utils/errors.js';
10
+ import { getServiceContainer } from '../utils/bootstrap.js';
11
11
 
12
12
  interface DetectOptions {
13
13
  json?: boolean;
@@ -304,8 +304,15 @@ export function registerDetect(program: Command) {
304
304
  console.log(chalk.blue('🔍 Analyzing project...'));
305
305
  console.log(chalk.gray(` Working directory: ${workspaceRoot}\n`));
306
306
 
307
- const service = new ProjectIntelligenceService();
308
- const result = await service.analyze({ workspaceRoot });
307
+ // Use ServiceContainer for project intelligence
308
+ const container = getServiceContainer({ workspaceRoot });
309
+ const analyzeResult = await container.projectIntelligence.analyze({ workspaceRoot });
310
+
311
+ if (!analyzeResult.success) {
312
+ throw new CliError(`Project analysis failed: ${analyzeResult.error.message}`);
313
+ }
314
+
315
+ const result = analyzeResult.data;
309
316
 
310
317
  // JSON output
311
318
  if (options.json) {
@@ -2,10 +2,11 @@ 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 { loadConfig, diagnoseWorkspace } from '@i18nsmith/core';
5
+ import { loadConfig } from '@i18nsmith/core';
6
6
  import type { DiagnosisReport } from '@i18nsmith/core';
7
7
  import { getDiagnosisExitSignal } from '../utils/diagnostics-exit.js';
8
8
  import { CliError, withErrorHandling } from '../utils/errors.js';
9
+ import { getServiceContainer } from '../utils/bootstrap.js';
9
10
 
10
11
  interface DiagnoseCommandOptions {
11
12
  config?: string;
@@ -109,7 +110,16 @@ export function registerDiagnose(program: Command) {
109
110
  console.log(chalk.blue('Running repository diagnostics...'));
110
111
  try {
111
112
  const config = await loadConfig(options.config);
112
- const report = await diagnoseWorkspace(config);
113
+
114
+ // Use ServiceContainer for diagnostics
115
+ const container = getServiceContainer();
116
+ const diagnoseResult = await container.diagnostics.diagnose(config);
117
+
118
+ if (!diagnoseResult.success) {
119
+ throw new CliError(`Diagnose failed: ${diagnoseResult.error.message}`);
120
+ }
121
+
122
+ const report = diagnoseResult.data;
113
123
 
114
124
  if (options.report) {
115
125
  const outputPath = path.resolve(process.cwd(), options.report);
@@ -160,6 +160,7 @@ vi.mock('inquirer', () => ({
160
160
  sourceLanguage: 'en',
161
161
  adapter: 'custom',
162
162
  localesDir: 'locales',
163
+ seedTargetLocales: false,
163
164
  }),
164
165
  },
165
166
  }));
@@ -256,6 +257,85 @@ describe('init command', () => {
256
257
  );
257
258
  });
258
259
  });
260
+
261
+ describe('interactive mode', () => {
262
+ it('writes seedTargetLocales into config when user enables it', async () => {
263
+ const program = new Command();
264
+ registerInit(program);
265
+ const command = program.commands.find((cmd) => cmd.name() === 'init')!;
266
+
267
+ // Mock process.cwd to return a test directory
268
+ const originalCwd = process.cwd;
269
+ process.cwd = vi.fn().mockReturnValue('/test/project');
270
+
271
+ // Make fs.access throw so init thinks config doesn't exist
272
+ vi.mocked(fs.access).mockRejectedValueOnce(new Error('File not found'));
273
+
274
+ // Override inquirer for this run to enable seedTargetLocales
275
+ const inquirer = await import('inquirer');
276
+ vi.mocked(inquirer.default.prompt).mockResolvedValueOnce({
277
+ setupMode: 'auto',
278
+ sourceLanguage: 'en',
279
+ localesDir: 'locales',
280
+ seedTargetLocales: true,
281
+ } as any);
282
+
283
+ try {
284
+ await (command as any).parseAsync([], { from: 'user' });
285
+ } finally {
286
+ process.cwd = originalCwd;
287
+ }
288
+
289
+ // Find the writeFile call that wrote i18n.config.json
290
+ const wroteConfig = vi.mocked(fs.writeFile).mock.calls.find((c) => String(c[0]).endsWith('i18n.config.json'));
291
+ expect(wroteConfig).toBeDefined();
292
+ expect(String(wroteConfig![1])).toContain('"seedTargetLocales": true');
293
+ });
294
+
295
+ it('continues when user chooses Overwrite for existing assets and records mergeStrategy', async () => {
296
+ const program = new Command();
297
+ registerInit(program);
298
+ const command = program.commands.find((cmd) => cmd.name() === 'init')!;
299
+
300
+ const originalCwd = process.cwd;
301
+ process.cwd = vi.fn().mockReturnValue('/test/project');
302
+
303
+ // Make fs.access throw so init thinks config doesn't exist
304
+ vi.mocked(fs.access).mockRejectedValueOnce(new Error('File not found'));
305
+
306
+ // Make diagnoseWorkspace report existing locale files so merge prompt appears
307
+ const core = await import('@i18nsmith/core');
308
+ vi.mocked(core.diagnoseWorkspace).mockResolvedValueOnce({
309
+ localesDir: 'locales',
310
+ localeFiles: [{ locale: 'en', missing: false, parseError: false }],
311
+ detectedLocales: [],
312
+ runtimePackages: [],
313
+ providerFiles: [],
314
+ adapterFiles: [],
315
+ translationUsage: { hookName: 'useTranslation', translationIdentifier: 't', filesExamined: 0, hookOccurrences: 0, identifierOccurrences: 0, hookExampleFiles: [], identifierExampleFiles: [] },
316
+ actionableItems: [],
317
+ conflicts: [],
318
+ recommendations: [],
319
+ } as any);
320
+
321
+ // Sequence of prompts: first the main answers, then the merge strategy choice
322
+ const inquirer = await import('inquirer');
323
+ vi.mocked(inquirer.default.prompt)
324
+ .mockResolvedValueOnce({ setupMode: 'auto', sourceLanguage: 'en', localesDir: 'locales', seedTargetLocales: false } as any)
325
+ .mockResolvedValueOnce({ strategy: 'overwrite' } as any);
326
+
327
+ try {
328
+ await (command as any).parseAsync([], { from: 'user' });
329
+ } finally {
330
+ process.cwd = originalCwd;
331
+ }
332
+
333
+ // Ensure config was written with mergeStrategy set to overwrite
334
+ const wroteConfig = vi.mocked(fs.writeFile).mock.calls.find((c) => String(c[0]).endsWith('i18n.config.json'));
335
+ expect(wroteConfig).toBeDefined();
336
+ expect(String(wroteConfig![1])).toContain('"mergeStrategy": "overwrite"');
337
+ });
338
+ });
259
339
  });
260
340
 
261
341
  describe('parseGlobList', () => {