@i18n-agent/cli 1.0.1
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/LICENSE +21 -0
- package/README.md +107 -0
- package/bin/i18nagent.js +6 -0
- package/docs/BINARY_DISTRIBUTION.md +108 -0
- package/package.json +44 -0
- package/src/api-client.js +127 -0
- package/src/commands/analyze.js +58 -0
- package/src/commands/credits.js +27 -0
- package/src/commands/download.js +53 -0
- package/src/commands/languages.js +33 -0
- package/src/commands/login.js +26 -0
- package/src/commands/logout.js +19 -0
- package/src/commands/resume.js +30 -0
- package/src/commands/status.js +44 -0
- package/src/commands/translate.js +301 -0
- package/src/commands/tui.js +91 -0
- package/src/commands/upload.js +115 -0
- package/src/config.js +45 -0
- package/src/index.js +156 -0
- package/src/namespace-detector.js +362 -0
- package/src/output.js +39 -0
- package/src/prompts.js +65 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
import { loginAction } from './commands/login.js';
|
|
7
|
+
import { logoutAction } from './commands/logout.js';
|
|
8
|
+
import { creditsAction } from './commands/credits.js';
|
|
9
|
+
import { languagesAction } from './commands/languages.js';
|
|
10
|
+
import { translateAction, translateTextAction, translateFileAction } from './commands/translate.js';
|
|
11
|
+
import { statusAction } from './commands/status.js';
|
|
12
|
+
import { downloadAction } from './commands/download.js';
|
|
13
|
+
import { resumeAction } from './commands/resume.js';
|
|
14
|
+
import { uploadAction, uploadsListAction } from './commands/upload.js';
|
|
15
|
+
import { analyzeAction } from './commands/analyze.js';
|
|
16
|
+
import { tuiAction } from './commands/tui.js';
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
function getVersion() {
|
|
21
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
22
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
23
|
+
return pkg.version;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createProgram() {
|
|
27
|
+
const program = new Command();
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.name('i18nagent')
|
|
31
|
+
.description('Terminal client for i18n-agent translation service')
|
|
32
|
+
.version(getVersion());
|
|
33
|
+
|
|
34
|
+
// --- Auth ---
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('login')
|
|
38
|
+
.description('Save API key to config file')
|
|
39
|
+
.option('--key <key>', 'API key (or enter interactively)')
|
|
40
|
+
.option('--force', 'Overwrite existing key')
|
|
41
|
+
.action(loginAction);
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.command('logout')
|
|
45
|
+
.description('Remove saved API key')
|
|
46
|
+
.action(logoutAction);
|
|
47
|
+
|
|
48
|
+
// --- Translation ---
|
|
49
|
+
|
|
50
|
+
const addTranslateOpts = (cmd) => cmd
|
|
51
|
+
.option('--lang <languages>', 'Target language(s), comma-separated (e.g. es,fr,ja)')
|
|
52
|
+
.option('--source <lang>', 'Source language (auto-detected if omitted)')
|
|
53
|
+
.option('--audience <audience>', 'Target audience (e.g. general, technical)')
|
|
54
|
+
.option('--industry <industry>', 'Industry context (e.g. technology, healthcare)')
|
|
55
|
+
.option('--context <context>', 'Additional instructions for translation')
|
|
56
|
+
.option('--namespace <ns>', 'Namespace for project tracking')
|
|
57
|
+
.option('--pseudo', 'Pseudo-translation mode (no AI cost)')
|
|
58
|
+
.option('--skip-warnings', 'Skip source quality warnings')
|
|
59
|
+
.option('--output <dir>', 'Output directory for file translations')
|
|
60
|
+
.option('--json', 'Output as JSON');
|
|
61
|
+
|
|
62
|
+
const translate = program
|
|
63
|
+
.command('translate [input]')
|
|
64
|
+
.description('Translate text or file (auto-detected). Run without args for interactive mode.');
|
|
65
|
+
addTranslateOpts(translate).action(translateAction);
|
|
66
|
+
|
|
67
|
+
const translateTextCmd = translate
|
|
68
|
+
.command('text [text]')
|
|
69
|
+
.description('Translate text content');
|
|
70
|
+
addTranslateOpts(translateTextCmd).action(translateTextAction);
|
|
71
|
+
|
|
72
|
+
const translateFileCmd = translate
|
|
73
|
+
.command('file [path]')
|
|
74
|
+
.description('Translate a file');
|
|
75
|
+
addTranslateOpts(translateFileCmd).action(translateFileAction);
|
|
76
|
+
|
|
77
|
+
// --- Job management ---
|
|
78
|
+
|
|
79
|
+
program
|
|
80
|
+
.command('status <jobId>')
|
|
81
|
+
.description('Check translation job status')
|
|
82
|
+
.option('--json', 'Output as JSON')
|
|
83
|
+
.option('--page-size <n>', 'Languages per page (default: 50)')
|
|
84
|
+
.action(statusAction);
|
|
85
|
+
|
|
86
|
+
program
|
|
87
|
+
.command('download <jobId>')
|
|
88
|
+
.description('Download completed translations')
|
|
89
|
+
.option('--output <dir>', 'Output directory')
|
|
90
|
+
.option('--json', 'Output as JSON')
|
|
91
|
+
.action(downloadAction);
|
|
92
|
+
|
|
93
|
+
program
|
|
94
|
+
.command('resume <jobId>')
|
|
95
|
+
.description('Resume a failed or interrupted translation job')
|
|
96
|
+
.option('--json', 'Output as JSON')
|
|
97
|
+
.action(resumeAction);
|
|
98
|
+
|
|
99
|
+
// --- Upload ---
|
|
100
|
+
|
|
101
|
+
program
|
|
102
|
+
.command('upload [filePath]')
|
|
103
|
+
.description('Upload existing translations for reuse')
|
|
104
|
+
.requiredOption('--source <lang>', 'Source language code')
|
|
105
|
+
.requiredOption('--target <lang>', 'Target language code')
|
|
106
|
+
.option('--namespace <ns>', 'Namespace for tracking')
|
|
107
|
+
.option('--json', 'Output as JSON')
|
|
108
|
+
.action(uploadAction);
|
|
109
|
+
|
|
110
|
+
const uploads = program
|
|
111
|
+
.command('uploads')
|
|
112
|
+
.description('Manage uploaded translations');
|
|
113
|
+
|
|
114
|
+
uploads
|
|
115
|
+
.command('list')
|
|
116
|
+
.description('List uploaded translations in a namespace')
|
|
117
|
+
.requiredOption('--namespace <ns>', 'Namespace to list')
|
|
118
|
+
.option('--source <lang>', 'Filter by source language')
|
|
119
|
+
.option('--target <lang>', 'Filter by target language')
|
|
120
|
+
.option('--json', 'Output as JSON')
|
|
121
|
+
.action(uploadsListAction);
|
|
122
|
+
|
|
123
|
+
// --- Info ---
|
|
124
|
+
|
|
125
|
+
program
|
|
126
|
+
.command('credits')
|
|
127
|
+
.description('Check translation credit balance')
|
|
128
|
+
.option('--json', 'Output as JSON')
|
|
129
|
+
.action(creditsAction);
|
|
130
|
+
|
|
131
|
+
program
|
|
132
|
+
.command('languages')
|
|
133
|
+
.description('List supported languages')
|
|
134
|
+
.option('--json', 'Output as JSON')
|
|
135
|
+
.option('--no-quality', 'Exclude quality ratings')
|
|
136
|
+
.action(languagesAction);
|
|
137
|
+
|
|
138
|
+
program
|
|
139
|
+
.command('analyze <input>')
|
|
140
|
+
.description('Analyze content for translation readiness')
|
|
141
|
+
.requiredOption('--lang <lang>', 'Target language')
|
|
142
|
+
.option('--source <lang>', 'Source language')
|
|
143
|
+
.option('--audience <audience>', 'Target audience')
|
|
144
|
+
.option('--industry <industry>', 'Industry context')
|
|
145
|
+
.option('--json', 'Output as JSON')
|
|
146
|
+
.action(analyzeAction);
|
|
147
|
+
|
|
148
|
+
// --- Interactive ---
|
|
149
|
+
|
|
150
|
+
program
|
|
151
|
+
.command('tui')
|
|
152
|
+
.description('Launch interactive TUI mode')
|
|
153
|
+
.action(tuiAction);
|
|
154
|
+
|
|
155
|
+
return program;
|
|
156
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespace Auto-Detection Utility for MCP Client
|
|
3
|
+
*
|
|
4
|
+
* Analyzes file paths and suggests namespace names based on project structure.
|
|
5
|
+
* Follows the same validation rules as the service-mcp namespace validator.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Auto-detect namespace from file path
|
|
10
|
+
*/
|
|
11
|
+
export function detectNamespaceFromPath(filePath, options = {}) {
|
|
12
|
+
const { includeAlternatives = true, maxAlternatives = 3 } = options;
|
|
13
|
+
|
|
14
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
15
|
+
return {
|
|
16
|
+
suggestion: null,
|
|
17
|
+
confidence: 0,
|
|
18
|
+
source: 'none',
|
|
19
|
+
reasoning: 'No file path provided',
|
|
20
|
+
alternatives: []
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Normalize path separators
|
|
25
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
26
|
+
const pathSegments = normalizedPath.split('/').filter(segment => segment.length > 0);
|
|
27
|
+
|
|
28
|
+
// Pattern detection strategies (ordered by confidence)
|
|
29
|
+
const detectionStrategies = [
|
|
30
|
+
detectServicePattern,
|
|
31
|
+
detectGitRepoPattern,
|
|
32
|
+
detectProjectFolderPattern,
|
|
33
|
+
detectDirectoryPattern
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
let bestResult = null;
|
|
37
|
+
const allSuggestions = [];
|
|
38
|
+
|
|
39
|
+
for (const strategy of detectionStrategies) {
|
|
40
|
+
const result = strategy(pathSegments, normalizedPath);
|
|
41
|
+
if (result.suggestion && isValidNamespace(result.suggestion)) {
|
|
42
|
+
if (!bestResult || result.confidence > bestResult.confidence) {
|
|
43
|
+
bestResult = result;
|
|
44
|
+
}
|
|
45
|
+
if (includeAlternatives && !allSuggestions.includes(result.suggestion)) {
|
|
46
|
+
allSuggestions.push(result.suggestion);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!bestResult) {
|
|
52
|
+
return {
|
|
53
|
+
suggestion: null,
|
|
54
|
+
confidence: 0,
|
|
55
|
+
source: 'none',
|
|
56
|
+
reasoning: 'Could not detect a valid namespace from the file path',
|
|
57
|
+
alternatives: []
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Generate alternatives
|
|
62
|
+
const alternatives = includeAlternatives
|
|
63
|
+
? allSuggestions
|
|
64
|
+
.filter(s => s !== bestResult.suggestion)
|
|
65
|
+
.slice(0, maxAlternatives)
|
|
66
|
+
: [];
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
...bestResult,
|
|
70
|
+
alternatives
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Detect service-* pattern (highest confidence)
|
|
76
|
+
* Examples:
|
|
77
|
+
* - /path/to/service-platform/file.json ā "service-platform"
|
|
78
|
+
* - /path/to/service-auth/config.yaml ā "service-auth"
|
|
79
|
+
*/
|
|
80
|
+
function detectServicePattern(pathSegments) {
|
|
81
|
+
for (let i = 0; i < pathSegments.length; i++) {
|
|
82
|
+
const segment = pathSegments[i];
|
|
83
|
+
if (segment.startsWith('service-') && segment.length > 8) {
|
|
84
|
+
const serviceName = segment.toLowerCase();
|
|
85
|
+
return {
|
|
86
|
+
suggestion: serviceName,
|
|
87
|
+
confidence: 0.9,
|
|
88
|
+
source: 'service-name',
|
|
89
|
+
reasoning: `Detected service name "${serviceName}" in path`,
|
|
90
|
+
alternatives: []
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { suggestion: null, confidence: 0, source: 'none', reasoning: '', alternatives: [] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Detect git repository pattern (high confidence)
|
|
100
|
+
* Examples:
|
|
101
|
+
* - /path/to/i18n-agent/service-platform/file.json ā "service-platform"
|
|
102
|
+
* - /path/to/my-project/src/file.ts ā "my-project"
|
|
103
|
+
*/
|
|
104
|
+
function detectGitRepoPattern(pathSegments) {
|
|
105
|
+
// Look for common project root indicators
|
|
106
|
+
const projectRootIndicators = [
|
|
107
|
+
'package.json', '.git', 'node_modules', 'src', 'lib', 'app',
|
|
108
|
+
'components', 'pages', 'public', 'assets', 'docs', 'tests'
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// Find potential project root by looking for these indicators
|
|
112
|
+
for (let i = pathSegments.length - 1; i >= 0; i--) {
|
|
113
|
+
const segment = pathSegments[i];
|
|
114
|
+
|
|
115
|
+
// If this looks like a project folder, use it
|
|
116
|
+
if (segment.includes('-') || segment.includes('_')) {
|
|
117
|
+
// Check if next segments contain project indicators
|
|
118
|
+
const remainingPath = pathSegments.slice(i + 1);
|
|
119
|
+
const hasProjectIndicators = remainingPath.some(seg =>
|
|
120
|
+
projectRootIndicators.includes(seg) ||
|
|
121
|
+
seg === 'src' ||
|
|
122
|
+
seg === 'lib' ||
|
|
123
|
+
seg.includes('component') ||
|
|
124
|
+
seg.includes('page')
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (hasProjectIndicators) {
|
|
128
|
+
const suggestion = normalizeNamespace(segment);
|
|
129
|
+
if (suggestion) {
|
|
130
|
+
return {
|
|
131
|
+
suggestion,
|
|
132
|
+
confidence: 0.7,
|
|
133
|
+
source: 'git-repo',
|
|
134
|
+
reasoning: `Detected project root "${segment}" based on directory structure`,
|
|
135
|
+
alternatives: []
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { suggestion: null, confidence: 0, source: 'none', reasoning: '', alternatives: [] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Detect project folder pattern (medium confidence)
|
|
147
|
+
* Examples:
|
|
148
|
+
* - /Users/user/Documents/my-awesome-project/file.js ā "my-awesome-project"
|
|
149
|
+
* - /workspace/client-docs/locales/en.json ā "client-docs"
|
|
150
|
+
*/
|
|
151
|
+
function detectProjectFolderPattern(pathSegments) {
|
|
152
|
+
// Look for segments that contain hyphens or underscores (common in project names)
|
|
153
|
+
const projectLikeSegments = pathSegments.filter(segment =>
|
|
154
|
+
(segment.includes('-') || segment.includes('_')) &&
|
|
155
|
+
segment.length >= 3 &&
|
|
156
|
+
!segment.startsWith('.') &&
|
|
157
|
+
segment !== 'node_modules'
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (projectLikeSegments.length > 0) {
|
|
161
|
+
// Use the last project-like segment (closest to the file)
|
|
162
|
+
const lastProjectSegment = projectLikeSegments[projectLikeSegments.length - 1];
|
|
163
|
+
const suggestion = normalizeNamespace(lastProjectSegment);
|
|
164
|
+
|
|
165
|
+
if (suggestion) {
|
|
166
|
+
return {
|
|
167
|
+
suggestion,
|
|
168
|
+
confidence: 0.5,
|
|
169
|
+
source: 'project-folder',
|
|
170
|
+
reasoning: `Detected project-like folder "${lastProjectSegment}" in path`,
|
|
171
|
+
alternatives: []
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { suggestion: null, confidence: 0, source: 'none', reasoning: '', alternatives: [] };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Detect directory pattern (low confidence fallback)
|
|
181
|
+
* Uses the immediate parent directory if it looks reasonable
|
|
182
|
+
*/
|
|
183
|
+
function detectDirectoryPattern(pathSegments) {
|
|
184
|
+
if (pathSegments.length < 2) {
|
|
185
|
+
return { suggestion: null, confidence: 0, source: 'none', reasoning: '', alternatives: [] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Use the parent directory of the file
|
|
189
|
+
const parentDir = pathSegments[pathSegments.length - 2];
|
|
190
|
+
const suggestion = normalizeNamespace(parentDir);
|
|
191
|
+
|
|
192
|
+
if (suggestion && suggestion.length >= 3) {
|
|
193
|
+
return {
|
|
194
|
+
suggestion,
|
|
195
|
+
confidence: 0.3,
|
|
196
|
+
source: 'directory-name',
|
|
197
|
+
reasoning: `Using parent directory "${parentDir}" as fallback`,
|
|
198
|
+
alternatives: []
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { suggestion: null, confidence: 0, source: 'none', reasoning: '', alternatives: [] };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Normalize a potential namespace to follow validation rules
|
|
207
|
+
*/
|
|
208
|
+
function normalizeNamespace(input) {
|
|
209
|
+
if (!input || typeof input !== 'string') return null;
|
|
210
|
+
|
|
211
|
+
// Convert to lowercase and trim
|
|
212
|
+
let normalized = input.toLowerCase().trim();
|
|
213
|
+
|
|
214
|
+
// Replace invalid characters with hyphens
|
|
215
|
+
normalized = normalized.replace(/[^a-z0-9_-]/g, '-');
|
|
216
|
+
|
|
217
|
+
// Remove multiple consecutive hyphens/underscores
|
|
218
|
+
normalized = normalized.replace(/[-_]{2,}/g, '-');
|
|
219
|
+
|
|
220
|
+
// Remove leading/trailing hyphens or underscores
|
|
221
|
+
normalized = normalized.replace(/^[-_]+|[-_]+$/g, '');
|
|
222
|
+
|
|
223
|
+
// Check length constraints
|
|
224
|
+
if (normalized.length < 3 || normalized.length > 50) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check if it's a reserved keyword
|
|
229
|
+
const reservedKeywords = [
|
|
230
|
+
'admin', 'api', 'www', 'system', 'root', 'default',
|
|
231
|
+
'translations', 'upload', 'download', 'temp', 'cache',
|
|
232
|
+
'public', 'private', 'test', 'staging', 'production'
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
if (reservedKeywords.includes(normalized)) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return normalized;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Quick validation using the same rules as service-mcp
|
|
244
|
+
*/
|
|
245
|
+
function isValidNamespace(namespace) {
|
|
246
|
+
if (!namespace || typeof namespace !== 'string') return false;
|
|
247
|
+
|
|
248
|
+
// Basic validation
|
|
249
|
+
const pattern = /^[a-z0-9_-]+$/;
|
|
250
|
+
return (
|
|
251
|
+
namespace.length >= 3 &&
|
|
252
|
+
namespace.length <= 50 &&
|
|
253
|
+
pattern.test(namespace) &&
|
|
254
|
+
!['admin', 'api', 'www', 'system', 'root', 'default'].includes(namespace)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Generate additional namespace suggestions based on file context
|
|
260
|
+
*/
|
|
261
|
+
export function generateNamespaceSuggestions(fileName, fileContent) {
|
|
262
|
+
const suggestions = [];
|
|
263
|
+
|
|
264
|
+
if (fileName) {
|
|
265
|
+
// Extract potential namespace from filename
|
|
266
|
+
const baseName = fileName.replace(/\.(json|yaml|yml|ts|js|md)$/i, '');
|
|
267
|
+
const normalized = normalizeNamespace(baseName);
|
|
268
|
+
if (normalized) {
|
|
269
|
+
suggestions.push(normalized);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Look for common patterns in filename
|
|
273
|
+
if (fileName.includes('i18n') || fileName.includes('locale') || fileName.includes('lang')) {
|
|
274
|
+
suggestions.push('localization', 'translations', 'i18n-project');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (fileName.includes('api') || fileName.includes('endpoint')) {
|
|
278
|
+
suggestions.push('api-docs', 'api-project');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (fileName.includes('component') || fileName.includes('ui')) {
|
|
282
|
+
suggestions.push('components', 'ui-library');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (fileContent) {
|
|
287
|
+
// Analyze content for clues (basic implementation)
|
|
288
|
+
const content = fileContent.toLowerCase();
|
|
289
|
+
|
|
290
|
+
if (content.includes('api') || content.includes('endpoint')) {
|
|
291
|
+
suggestions.push('api-docs');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (content.includes('component') || content.includes('react') || content.includes('vue')) {
|
|
295
|
+
suggestions.push('components');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (content.includes('translation') || content.includes('locale')) {
|
|
299
|
+
suggestions.push('translations');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Remove duplicates and filter valid ones
|
|
304
|
+
return [...new Set(suggestions)]
|
|
305
|
+
.filter(s => isValidNamespace(s))
|
|
306
|
+
.slice(0, 5);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get helpful namespace suggestions text for user guidance
|
|
311
|
+
*/
|
|
312
|
+
export function getNamespaceSuggestionText(filePath, fileName) {
|
|
313
|
+
if (!filePath && !fileName) {
|
|
314
|
+
return `š” Namespace Suggestions:
|
|
315
|
+
⢠Use descriptive project names: "my-website", "mobile-app"
|
|
316
|
+
⢠Include service names: "service-auth", "api-gateway"
|
|
317
|
+
⢠Use organization prefixes: "company-docs", "team-frontend"
|
|
318
|
+
⢠Avoid generic names: "app", "project", "files"`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const detection = detectNamespaceFromPath(filePath || fileName);
|
|
322
|
+
|
|
323
|
+
if (detection.suggestion) {
|
|
324
|
+
let text = `šÆ Auto-detected suggestion: "${detection.suggestion}"`;
|
|
325
|
+
text += `\nš Source: ${getSourceDescription(detection.source)}`;
|
|
326
|
+
text += `\nšŖ Confidence: ${Math.round(detection.confidence * 100)}%`;
|
|
327
|
+
|
|
328
|
+
if (detection.alternatives.length > 0) {
|
|
329
|
+
text += `\nš Alternatives: ${detection.alternatives.join(', ')}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return text;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return `š” Namespace Suggestions for "${fileName || filePath}":
|
|
336
|
+
⢠Extract from path: "${getPathBasedSuggestion(filePath || fileName)}"
|
|
337
|
+
⢠Use project context: "docs", "website", "api"
|
|
338
|
+
⢠Include team/org: "frontend-team", "backend-api"
|
|
339
|
+
⢠Keep it descriptive: "user-dashboard", "admin-panel"`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getSourceDescription(source) {
|
|
343
|
+
const descriptions = {
|
|
344
|
+
'service-name': 'Service name pattern detected',
|
|
345
|
+
'git-repo': 'Git repository structure analyzed',
|
|
346
|
+
'project-folder': 'Project folder pattern found',
|
|
347
|
+
'directory-name': 'Parent directory used as fallback'
|
|
348
|
+
};
|
|
349
|
+
return descriptions[source] || 'Unknown source';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function getPathBasedSuggestion(path) {
|
|
353
|
+
if (!path) return 'my-project';
|
|
354
|
+
|
|
355
|
+
const segments = path.replace(/\\/g, '/').split('/').filter(s => s.length > 0);
|
|
356
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
357
|
+
const normalized = normalizeNamespace(segments[i]);
|
|
358
|
+
if (normalized) return normalized;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return 'my-project';
|
|
362
|
+
}
|
package/src/output.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function formatOutput(data, opts = {}) {
|
|
2
|
+
if (opts.json) {
|
|
3
|
+
return JSON.stringify(data, null, 2);
|
|
4
|
+
}
|
|
5
|
+
if (opts.template) {
|
|
6
|
+
return opts.template(data);
|
|
7
|
+
}
|
|
8
|
+
return JSON.stringify(data, null, 2);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatError(message, opts = {}) {
|
|
12
|
+
if (opts.json) {
|
|
13
|
+
return JSON.stringify({ error: message });
|
|
14
|
+
}
|
|
15
|
+
return `Error: ${message}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatSuccess(message, opts = {}) {
|
|
19
|
+
if (opts.json) {
|
|
20
|
+
return JSON.stringify({ success: true, message });
|
|
21
|
+
}
|
|
22
|
+
return message;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatProgress(current, total, width = 20) {
|
|
26
|
+
const pct = total > 0 ? Math.round((current / total) * 100) : 0;
|
|
27
|
+
const filled = Math.round((pct / 100) * width);
|
|
28
|
+
const empty = width - filled;
|
|
29
|
+
const bar = '#'.repeat(filled) + '.'.repeat(empty);
|
|
30
|
+
return `[${bar}] ${pct}%`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function print(text) {
|
|
34
|
+
process.stdout.write(text + '\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function printErr(text) {
|
|
38
|
+
process.stderr.write(text + '\n');
|
|
39
|
+
}
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
|
|
3
|
+
function createInterface() {
|
|
4
|
+
return readline.createInterface({
|
|
5
|
+
input: process.stdin,
|
|
6
|
+
output: process.stderr,
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function ask(rl, question) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function promptText(question) {
|
|
17
|
+
const rl = createInterface();
|
|
18
|
+
try {
|
|
19
|
+
const answer = await ask(rl, question);
|
|
20
|
+
return answer || null;
|
|
21
|
+
} finally {
|
|
22
|
+
rl.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function promptApiKey() {
|
|
27
|
+
const rl = createInterface();
|
|
28
|
+
try {
|
|
29
|
+
const key = await ask(rl, 'Enter your API key (get one at https://app.i18nagent.ai): ');
|
|
30
|
+
if (!key) return null;
|
|
31
|
+
if (!key.startsWith('i18n_') || key.length <= 5) {
|
|
32
|
+
console.error('Invalid API key format. Keys should start with "i18n_"');
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return key;
|
|
36
|
+
} finally {
|
|
37
|
+
rl.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function promptChoice(question, choices) {
|
|
42
|
+
const rl = createInterface();
|
|
43
|
+
try {
|
|
44
|
+
console.error(question);
|
|
45
|
+
choices.forEach((choice, i) => {
|
|
46
|
+
console.error(` [${i + 1}] ${choice}`);
|
|
47
|
+
});
|
|
48
|
+
const answer = await ask(rl, '\n Choose: ');
|
|
49
|
+
const num = parseInt(answer, 10);
|
|
50
|
+
if (isNaN(num) || num < 1 || num > choices.length) return null;
|
|
51
|
+
return num - 1;
|
|
52
|
+
} finally {
|
|
53
|
+
rl.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function promptConfirm(question) {
|
|
58
|
+
const rl = createInterface();
|
|
59
|
+
try {
|
|
60
|
+
const answer = await ask(rl, `${question} (y/n): `);
|
|
61
|
+
return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
62
|
+
} finally {
|
|
63
|
+
rl.close();
|
|
64
|
+
}
|
|
65
|
+
}
|