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
|
@@ -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
package/src/commands/backup.ts
CHANGED
|
@@ -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 (
|
|
16
|
+
withErrorHandling(async (_options: { backupDir?: string }) => {
|
|
17
17
|
try {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
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,
|
|
56
|
+
withErrorHandling(async (timestamp: string, _options: { backupDir?: string }) => {
|
|
50
57
|
try {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
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
|
|
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
|
}
|
package/src/commands/check.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
const
|
|
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) ?? [];
|
package/src/commands/config.ts
CHANGED
|
@@ -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
|
}
|
package/src/commands/coverage.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 { 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
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
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
|
|
package/src/commands/detect.ts
CHANGED
|
@@ -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
|
-
|
|
308
|
-
const
|
|
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) {
|
package/src/commands/diagnose.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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', () => {
|