langctl 0.1.2 → 0.2.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/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- package/CONTRIBUTING.md +33 -0
- package/LICENSE +1 -1
- package/README.md +52 -11
- package/dist/api.d.ts +20 -0
- package/dist/api.js +75 -0
- package/dist/auth.d.ts +3 -2
- package/dist/auth.js +21 -27
- package/dist/commands/auth.js +2 -10
- package/dist/commands/config.js +1 -0
- package/dist/commands/debug.js +45 -41
- package/dist/commands/export.d.ts +1 -2
- package/dist/commands/export.js +70 -60
- package/dist/commands/import.d.ts +1 -0
- package/dist/commands/import.js +51 -72
- package/dist/commands/init.js +0 -10
- package/dist/commands/keys.js +122 -224
- package/dist/commands/org.js +43 -72
- package/dist/commands/projects.d.ts +3 -0
- package/dist/commands/projects.js +146 -223
- package/dist/commands/pull.js +40 -95
- package/dist/commands/team.js +115 -130
- package/dist/config.d.ts +1 -0
- package/dist/index.js +3 -3
- package/package.json +2 -3
- package/NEXT_PHASE.md +0 -618
- package/WORKING_INSTRUCTIONS.md +0 -1389
- package/dist/supabase.d.ts +0 -14
- package/dist/supabase.js +0 -30
package/dist/commands/pull.js
CHANGED
|
@@ -2,8 +2,9 @@ import chalk from 'chalk';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { writeFileSync, mkdirSync } from 'fs';
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { isAuthenticated } from '../auth.js';
|
|
6
|
+
import { getApiClient } from '../api.js';
|
|
7
|
+
import { config } from '../config.js';
|
|
7
8
|
import { exportTranslations } from '../exporters/index.js';
|
|
8
9
|
/**
|
|
9
10
|
* Get platform-specific output directory
|
|
@@ -18,64 +19,24 @@ function getPlatformDirectory(format) {
|
|
|
18
19
|
};
|
|
19
20
|
return platformDirs[format] || 'i18n';
|
|
20
21
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Resolve project identifier (name, slug, or UUID) to project UUID
|
|
23
|
-
*/
|
|
24
|
-
async function resolveProjectId(identifier, organizationId) {
|
|
25
|
-
const supabase = getSupabase();
|
|
26
|
-
// Check if it's already a UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
|
|
27
|
-
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
28
|
-
if (uuidRegex.test(identifier)) {
|
|
29
|
-
// Verify it exists
|
|
30
|
-
const { data } = await supabase
|
|
31
|
-
.from('projects')
|
|
32
|
-
.select('id')
|
|
33
|
-
.eq('id', identifier)
|
|
34
|
-
.eq('organization_id', organizationId)
|
|
35
|
-
.is('deleted_at', null)
|
|
36
|
-
.single();
|
|
37
|
-
return data ? identifier : null;
|
|
38
|
-
}
|
|
39
|
-
// Try to match by slug (case-insensitive)
|
|
40
|
-
const { data: bySlug } = await supabase
|
|
41
|
-
.from('projects')
|
|
42
|
-
.select('id')
|
|
43
|
-
.eq('organization_id', organizationId)
|
|
44
|
-
.ilike('slug', identifier)
|
|
45
|
-
.is('deleted_at', null)
|
|
46
|
-
.single();
|
|
47
|
-
if (bySlug) {
|
|
48
|
-
return bySlug.id;
|
|
49
|
-
}
|
|
50
|
-
// Try to match by name (case-insensitive)
|
|
51
|
-
const { data: byName } = await supabase
|
|
52
|
-
.from('projects')
|
|
53
|
-
.select('id')
|
|
54
|
-
.eq('organization_id', organizationId)
|
|
55
|
-
.ilike('name', identifier)
|
|
56
|
-
.is('deleted_at', null)
|
|
57
|
-
.single();
|
|
58
|
-
if (byName) {
|
|
59
|
-
return byName.id;
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
22
|
export async function pullCommand(projectIdentifier, options) {
|
|
64
23
|
// Check authentication
|
|
65
24
|
if (!isAuthenticated()) {
|
|
66
25
|
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
26
|
+
process.exitCode = 1;
|
|
67
27
|
return;
|
|
68
28
|
}
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
71
|
-
console.log(chalk.red('✗ Organization ID not found. Please run "langctl
|
|
29
|
+
const orgId = config.get('organizationId');
|
|
30
|
+
if (!orgId) {
|
|
31
|
+
console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
|
|
32
|
+
process.exitCode = 1;
|
|
72
33
|
return;
|
|
73
34
|
}
|
|
74
35
|
// Set defaults
|
|
75
36
|
const format = (options.format || 'json');
|
|
76
37
|
const publishedOnly = options.publishedOnly !== false; // Default to true
|
|
77
|
-
const specificLanguage = options.language;
|
|
78
|
-
const baseDir = options.dir || './translations';
|
|
38
|
+
const specificLanguage = options.language;
|
|
39
|
+
const baseDir = options.dir || './translations';
|
|
79
40
|
// Validate format
|
|
80
41
|
const validFormats = ['json', 'json-nested', 'ios', 'android', 'flutter'];
|
|
81
42
|
if (!validFormats.includes(format)) {
|
|
@@ -85,31 +46,17 @@ export async function pullCommand(projectIdentifier, options) {
|
|
|
85
46
|
}
|
|
86
47
|
const spinner = ora('Resolving project...').start();
|
|
87
48
|
try {
|
|
88
|
-
const
|
|
89
|
-
// Resolve project
|
|
90
|
-
const
|
|
91
|
-
if (!
|
|
49
|
+
const api = getApiClient();
|
|
50
|
+
// Resolve project by slug
|
|
51
|
+
const project = await api.get(`/orgs/${orgId}/projects/by-slug/${projectIdentifier}`);
|
|
52
|
+
if (!project) {
|
|
92
53
|
spinner.fail(chalk.red(`Project "${projectIdentifier}" not found`));
|
|
93
54
|
console.log(chalk.yellow('\nRun "langctl projects list" to see available projects.\n'));
|
|
94
55
|
return;
|
|
95
56
|
}
|
|
96
|
-
// Fetch project details
|
|
97
|
-
spinner.text = 'Fetching project...';
|
|
98
|
-
const { data: project, error: projectError } = await supabase
|
|
99
|
-
.from('projects')
|
|
100
|
-
.select('id, name, slug, languages, default_language')
|
|
101
|
-
.eq('id', projectId)
|
|
102
|
-
.eq('organization_id', organizationId)
|
|
103
|
-
.is('deleted_at', null)
|
|
104
|
-
.single();
|
|
105
|
-
if (projectError || !project) {
|
|
106
|
-
spinner.fail(chalk.red('Project not found or access denied'));
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
57
|
// Determine which languages to pull
|
|
110
58
|
let languagesToPull;
|
|
111
59
|
if (specificLanguage) {
|
|
112
|
-
// Check if specified language exists in project
|
|
113
60
|
if (!project.languages.includes(specificLanguage)) {
|
|
114
61
|
spinner.fail(chalk.red(`Language "${specificLanguage}" not found in project`));
|
|
115
62
|
console.log(chalk.yellow(`Available languages: ${project.languages.join(', ')}\n`));
|
|
@@ -118,44 +65,37 @@ export async function pullCommand(projectIdentifier, options) {
|
|
|
118
65
|
languagesToPull = [specificLanguage];
|
|
119
66
|
}
|
|
120
67
|
else {
|
|
121
|
-
// Pull all languages
|
|
122
68
|
languagesToPull = project.languages;
|
|
123
69
|
}
|
|
124
70
|
spinner.text = `Fetching translations for "${project.name}"...`;
|
|
125
|
-
// Fetch translation keys
|
|
126
|
-
let query = supabase
|
|
127
|
-
.from('translation_keys')
|
|
128
|
-
.select('key, translations, description, module')
|
|
129
|
-
.eq('project_id', projectId)
|
|
130
|
-
.is('deleted_at', null);
|
|
131
|
-
if (publishedOnly) {
|
|
132
|
-
query = query.eq('published', true);
|
|
133
|
-
}
|
|
134
|
-
const { data: keys, error: keysError } = await query;
|
|
135
|
-
if (keysError) {
|
|
136
|
-
spinner.fail(chalk.red('Failed to fetch translations'));
|
|
137
|
-
console.error(chalk.red(`Error: ${keysError.message}\n`));
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (!keys || keys.length === 0) {
|
|
141
|
-
spinner.fail(chalk.yellow('No translations found'));
|
|
142
|
-
console.log(chalk.yellow(`\nNo ${publishedOnly ? 'published ' : ''}translations found for this project.\n`));
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
spinner.succeed(chalk.green(`Found ${keys.length} translation keys`));
|
|
146
71
|
// Get platform directory
|
|
147
72
|
const platformDir = getPlatformDirectory(format);
|
|
148
73
|
// Export translations for each language
|
|
149
74
|
const exportedFiles = [];
|
|
75
|
+
let totalKeyCount = 0;
|
|
150
76
|
for (const language of languagesToPull) {
|
|
151
|
-
spinner.
|
|
152
|
-
|
|
77
|
+
spinner.text = `Exporting ${language}...`;
|
|
78
|
+
// Fetch flat translations from the API
|
|
79
|
+
const result = await api.get(`/orgs/${orgId}/projects/${project.id}/export`, {
|
|
80
|
+
language,
|
|
81
|
+
publishedOnly: publishedOnly ? 'true' : 'false'
|
|
82
|
+
});
|
|
83
|
+
// Convert flat translations to TranslationKey array for the exporter
|
|
84
|
+
const flatTranslations = result.translations || {};
|
|
85
|
+
const translationKeys = Object.entries(flatTranslations).map(([key, value]) => ({
|
|
86
|
+
key,
|
|
87
|
+
translations: { [language]: value }
|
|
88
|
+
}));
|
|
89
|
+
if (totalKeyCount === 0) {
|
|
90
|
+
totalKeyCount = translationKeys.length;
|
|
91
|
+
}
|
|
92
|
+
const exportResult = exportTranslations(translationKeys, language, format);
|
|
153
93
|
// Determine output path
|
|
154
94
|
let outputPath;
|
|
155
95
|
if (options.output) {
|
|
156
96
|
// If custom output is specified and multiple languages, append language code
|
|
157
97
|
if (languagesToPull.length > 1) {
|
|
158
|
-
const ext =
|
|
98
|
+
const ext = exportResult.filename.split('.').pop();
|
|
159
99
|
const base = options.output.replace(/\.[^.]+$/, '');
|
|
160
100
|
outputPath = `${base}-${language}.${ext}`;
|
|
161
101
|
}
|
|
@@ -165,15 +105,19 @@ export async function pullCommand(projectIdentifier, options) {
|
|
|
165
105
|
}
|
|
166
106
|
else {
|
|
167
107
|
// Default output path: <baseDir>/<platform>/<language>.<ext>
|
|
168
|
-
const ext =
|
|
108
|
+
const ext = exportResult.filename.split('.').pop();
|
|
169
109
|
outputPath = join(baseDir, platformDir, `${language}.${ext}`);
|
|
170
110
|
}
|
|
171
111
|
// Create directory if it doesn't exist
|
|
172
112
|
const dir = dirname(outputPath);
|
|
173
113
|
mkdirSync(dir, { recursive: true });
|
|
174
|
-
writeFileSync(outputPath,
|
|
114
|
+
writeFileSync(outputPath, exportResult.content, 'utf-8');
|
|
175
115
|
exportedFiles.push(outputPath);
|
|
176
116
|
spinner.succeed(chalk.green(`Exported ${language} → ${outputPath}`));
|
|
117
|
+
// Restart spinner if more languages remain
|
|
118
|
+
if (languagesToPull.indexOf(language) < languagesToPull.length - 1) {
|
|
119
|
+
spinner.start();
|
|
120
|
+
}
|
|
177
121
|
}
|
|
178
122
|
// Summary
|
|
179
123
|
console.log(chalk.hex('#10b981').bold('\n📦 Export Summary:'));
|
|
@@ -181,7 +125,7 @@ export async function pullCommand(projectIdentifier, options) {
|
|
|
181
125
|
console.log(chalk.white(` Format: ${format}`));
|
|
182
126
|
console.log(chalk.white(` Platform: ${platformDir}`));
|
|
183
127
|
console.log(chalk.white(` Output: ${baseDir}`));
|
|
184
|
-
console.log(chalk.white(` Keys: ${
|
|
128
|
+
console.log(chalk.white(` Keys: ${totalKeyCount}`));
|
|
185
129
|
console.log(chalk.white(` Languages: ${languagesToPull.join(', ')}`));
|
|
186
130
|
console.log(chalk.white(` Files: ${exportedFiles.length}`));
|
|
187
131
|
console.log('');
|
|
@@ -189,6 +133,7 @@ export async function pullCommand(projectIdentifier, options) {
|
|
|
189
133
|
catch (error) {
|
|
190
134
|
spinner.fail(chalk.red('Failed to pull translations'));
|
|
191
135
|
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
136
|
+
process.exitCode = 1;
|
|
192
137
|
}
|
|
193
138
|
}
|
|
194
139
|
//# sourceMappingURL=pull.js.map
|
package/dist/commands/team.js
CHANGED
|
@@ -1,43 +1,38 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
-
import { isAuthenticated
|
|
4
|
-
|
|
3
|
+
import { isAuthenticated } from '../auth.js';
|
|
4
|
+
import { getApiClient } from '../api.js';
|
|
5
|
+
import { config } from '../config.js';
|
|
5
6
|
/**
|
|
6
7
|
* List all team members
|
|
7
8
|
*/
|
|
8
9
|
export async function listTeamCommand() {
|
|
9
10
|
if (!isAuthenticated()) {
|
|
10
11
|
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const orgId = config.get('organizationId');
|
|
16
|
+
if (!orgId) {
|
|
17
|
+
console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
|
|
18
|
+
process.exitCode = 1;
|
|
11
19
|
return;
|
|
12
20
|
}
|
|
13
|
-
const apiKey = getApiKey();
|
|
14
21
|
const spinner = ora('Fetching team members...').start();
|
|
15
22
|
try {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
headers: {
|
|
19
|
-
'Content-Type': 'application/json',
|
|
20
|
-
'X-API-Key': apiKey
|
|
21
|
-
},
|
|
22
|
-
body: JSON.stringify({
|
|
23
|
-
action: 'list'
|
|
24
|
-
})
|
|
25
|
-
});
|
|
26
|
-
const data = await response.json();
|
|
27
|
-
if (!data.success) {
|
|
28
|
-
throw new Error(data.error);
|
|
29
|
-
}
|
|
23
|
+
const api = getApiClient();
|
|
24
|
+
const members = await api.get(`/orgs/${orgId}/members`);
|
|
30
25
|
spinner.stop();
|
|
31
|
-
if (!
|
|
26
|
+
if (!members || members.length === 0) {
|
|
32
27
|
console.log(chalk.yellow('\nNo team members found\n'));
|
|
33
28
|
return;
|
|
34
29
|
}
|
|
35
|
-
console.log(chalk.blue.bold(`\n👥 Team Members (${
|
|
36
|
-
|
|
37
|
-
console.log(chalk.white.bold(member.email
|
|
30
|
+
console.log(chalk.blue.bold(`\n👥 Team Members (${members.length})\n`));
|
|
31
|
+
members.forEach((member) => {
|
|
32
|
+
console.log(chalk.white.bold(member.email));
|
|
38
33
|
console.log(chalk.gray(` Role: ${member.role}`));
|
|
39
|
-
if (member.
|
|
40
|
-
console.log(chalk.gray(` Joined: ${new Date(member.
|
|
34
|
+
if (member.joinedAt) {
|
|
35
|
+
console.log(chalk.gray(` Joined: ${new Date(member.joinedAt).toLocaleDateString()}`));
|
|
41
36
|
}
|
|
42
37
|
console.log('');
|
|
43
38
|
});
|
|
@@ -45,6 +40,7 @@ export async function listTeamCommand() {
|
|
|
45
40
|
catch (error) {
|
|
46
41
|
spinner.fail(chalk.red('Failed to list team members'));
|
|
47
42
|
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
43
|
+
process.exitCode = 1;
|
|
48
44
|
}
|
|
49
45
|
}
|
|
50
46
|
/**
|
|
@@ -53,40 +49,37 @@ export async function listTeamCommand() {
|
|
|
53
49
|
export async function getTeamMemberCommand(email) {
|
|
54
50
|
if (!isAuthenticated()) {
|
|
55
51
|
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const orgId = config.get('organizationId');
|
|
56
|
+
if (!orgId) {
|
|
57
|
+
console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
|
|
58
|
+
process.exitCode = 1;
|
|
56
59
|
return;
|
|
57
60
|
}
|
|
58
|
-
const apiKey = getApiKey();
|
|
59
61
|
const spinner = ora('Fetching member details...').start();
|
|
60
62
|
try {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
},
|
|
67
|
-
body: JSON.stringify({
|
|
68
|
-
action: 'get',
|
|
69
|
-
email
|
|
70
|
-
})
|
|
71
|
-
});
|
|
72
|
-
const data = await response.json();
|
|
73
|
-
if (!data.success) {
|
|
74
|
-
throw new Error(data.error);
|
|
63
|
+
const api = getApiClient();
|
|
64
|
+
const members = await api.get(`/orgs/${orgId}/members`);
|
|
65
|
+
const member = members.find((m) => m.email === email);
|
|
66
|
+
if (!member) {
|
|
67
|
+
throw new Error(`Member "${email}" not found`);
|
|
75
68
|
}
|
|
76
69
|
spinner.stop();
|
|
77
|
-
const member = data.member;
|
|
78
70
|
console.log(chalk.blue.bold('\n👤 Team Member Details\n'));
|
|
79
71
|
console.log(chalk.white.bold(`Email: ${member.email}`));
|
|
80
72
|
console.log(chalk.gray(`Role: ${member.role}`));
|
|
81
|
-
console.log(chalk.gray(`User ID: ${member.
|
|
82
|
-
if (member.
|
|
83
|
-
console.log(chalk.gray(`Joined: ${new Date(member.
|
|
73
|
+
console.log(chalk.gray(`User ID: ${member.userId}`));
|
|
74
|
+
if (member.joinedAt) {
|
|
75
|
+
console.log(chalk.gray(`Joined: ${new Date(member.joinedAt).toLocaleString()}`));
|
|
84
76
|
}
|
|
85
77
|
console.log('');
|
|
86
78
|
}
|
|
87
79
|
catch (error) {
|
|
88
80
|
spinner.fail(chalk.red('Failed to get member details'));
|
|
89
81
|
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
82
|
+
process.exitCode = 1;
|
|
90
83
|
}
|
|
91
84
|
}
|
|
92
85
|
/**
|
|
@@ -95,33 +88,29 @@ export async function getTeamMemberCommand(email) {
|
|
|
95
88
|
export async function inviteTeamMemberCommand(email, options) {
|
|
96
89
|
if (!isAuthenticated()) {
|
|
97
90
|
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const orgId = config.get('organizationId');
|
|
95
|
+
if (!orgId) {
|
|
96
|
+
console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
|
|
97
|
+
process.exitCode = 1;
|
|
98
98
|
return;
|
|
99
99
|
}
|
|
100
|
-
const apiKey = getApiKey();
|
|
101
100
|
const spinner = ora(`Inviting ${email}...`).start();
|
|
102
101
|
try {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
'X-API-Key': apiKey
|
|
108
|
-
},
|
|
109
|
-
body: JSON.stringify({
|
|
110
|
-
action: 'invite',
|
|
111
|
-
email,
|
|
112
|
-
role: options.role || 'member'
|
|
113
|
-
})
|
|
102
|
+
const api = getApiClient();
|
|
103
|
+
await api.post(`/orgs/${orgId}/invitations`, {
|
|
104
|
+
email,
|
|
105
|
+
role: options.role || 'member'
|
|
114
106
|
});
|
|
115
|
-
|
|
116
|
-
if (!data.success) {
|
|
117
|
-
throw new Error(data.error);
|
|
118
|
-
}
|
|
119
|
-
spinner.succeed(chalk.green(data.message));
|
|
107
|
+
spinner.succeed(chalk.green(`Invitation sent to ${email}`));
|
|
120
108
|
console.log('');
|
|
121
109
|
}
|
|
122
110
|
catch (error) {
|
|
123
111
|
spinner.fail(chalk.red('Failed to invite member'));
|
|
124
112
|
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
113
|
+
process.exitCode = 1;
|
|
125
114
|
}
|
|
126
115
|
}
|
|
127
116
|
/**
|
|
@@ -130,32 +119,31 @@ export async function inviteTeamMemberCommand(email, options) {
|
|
|
130
119
|
export async function removeTeamMemberCommand(email) {
|
|
131
120
|
if (!isAuthenticated()) {
|
|
132
121
|
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const orgId = config.get('organizationId');
|
|
126
|
+
if (!orgId) {
|
|
127
|
+
console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
|
|
128
|
+
process.exitCode = 1;
|
|
133
129
|
return;
|
|
134
130
|
}
|
|
135
|
-
const apiKey = getApiKey();
|
|
136
131
|
const spinner = ora(`Removing ${email}...`).start();
|
|
137
132
|
try {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
},
|
|
144
|
-
body: JSON.stringify({
|
|
145
|
-
action: 'remove',
|
|
146
|
-
email
|
|
147
|
-
})
|
|
148
|
-
});
|
|
149
|
-
const data = await response.json();
|
|
150
|
-
if (!data.success) {
|
|
151
|
-
throw new Error(data.error);
|
|
133
|
+
const api = getApiClient();
|
|
134
|
+
const members = await api.get(`/orgs/${orgId}/members`);
|
|
135
|
+
const member = members.find((m) => m.email === email);
|
|
136
|
+
if (!member) {
|
|
137
|
+
throw new Error(`Member "${email}" not found`);
|
|
152
138
|
}
|
|
153
|
-
|
|
139
|
+
await api.delete(`/orgs/${orgId}/members/${member.id}`);
|
|
140
|
+
spinner.succeed(chalk.green(`Removed ${email} from organization`));
|
|
154
141
|
console.log('');
|
|
155
142
|
}
|
|
156
143
|
catch (error) {
|
|
157
144
|
spinner.fail(chalk.red('Failed to remove member'));
|
|
158
145
|
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
146
|
+
process.exitCode = 1;
|
|
159
147
|
}
|
|
160
148
|
}
|
|
161
149
|
/**
|
|
@@ -164,38 +152,37 @@ export async function removeTeamMemberCommand(email) {
|
|
|
164
152
|
export async function updateTeamRoleCommand(email, role) {
|
|
165
153
|
if (!isAuthenticated()) {
|
|
166
154
|
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
155
|
+
process.exitCode = 1;
|
|
167
156
|
return;
|
|
168
157
|
}
|
|
169
158
|
const validRoles = ['viewer', 'member', 'admin', 'owner'];
|
|
170
159
|
if (!validRoles.includes(role)) {
|
|
171
160
|
console.log(chalk.red(`✗ Invalid role. Must be one of: ${validRoles.join(', ')}\n`));
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const orgId = config.get('organizationId');
|
|
165
|
+
if (!orgId) {
|
|
166
|
+
console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
|
|
167
|
+
process.exitCode = 1;
|
|
172
168
|
return;
|
|
173
169
|
}
|
|
174
|
-
const apiKey = getApiKey();
|
|
175
170
|
const spinner = ora(`Updating role for ${email}...`).start();
|
|
176
171
|
try {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
},
|
|
183
|
-
body: JSON.stringify({
|
|
184
|
-
action: 'update-role',
|
|
185
|
-
email,
|
|
186
|
-
role
|
|
187
|
-
})
|
|
188
|
-
});
|
|
189
|
-
const data = await response.json();
|
|
190
|
-
if (!data.success) {
|
|
191
|
-
throw new Error(data.error);
|
|
172
|
+
const api = getApiClient();
|
|
173
|
+
const members = await api.get(`/orgs/${orgId}/members`);
|
|
174
|
+
const member = members.find((m) => m.email === email);
|
|
175
|
+
if (!member) {
|
|
176
|
+
throw new Error(`Member "${email}" not found`);
|
|
192
177
|
}
|
|
193
|
-
|
|
178
|
+
await api.patch(`/orgs/${orgId}/members/${member.id}`, { role });
|
|
179
|
+
spinner.succeed(chalk.green(`Updated ${email} role to ${role}`));
|
|
194
180
|
console.log('');
|
|
195
181
|
}
|
|
196
182
|
catch (error) {
|
|
197
183
|
spinner.fail(chalk.red('Failed to update role'));
|
|
198
184
|
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
185
|
+
process.exitCode = 1;
|
|
199
186
|
}
|
|
200
187
|
}
|
|
201
188
|
/**
|
|
@@ -204,41 +191,39 @@ export async function updateTeamRoleCommand(email, role) {
|
|
|
204
191
|
export async function listInvitationsCommand(options) {
|
|
205
192
|
if (!isAuthenticated()) {
|
|
206
193
|
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const orgId = config.get('organizationId');
|
|
198
|
+
if (!orgId) {
|
|
199
|
+
console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
|
|
200
|
+
process.exitCode = 1;
|
|
207
201
|
return;
|
|
208
202
|
}
|
|
209
|
-
const apiKey = getApiKey();
|
|
210
203
|
const spinner = ora('Fetching invitations...').start();
|
|
211
204
|
try {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
'X-API-Key': apiKey
|
|
217
|
-
},
|
|
218
|
-
body: JSON.stringify({
|
|
219
|
-
action: 'list-invitations',
|
|
220
|
-
pending: options.pending || false
|
|
221
|
-
})
|
|
222
|
-
});
|
|
223
|
-
const data = await response.json();
|
|
224
|
-
if (!data.success) {
|
|
225
|
-
throw new Error(data.error);
|
|
205
|
+
const api = getApiClient();
|
|
206
|
+
const params = {};
|
|
207
|
+
if (options?.pending) {
|
|
208
|
+
params.status = 'pending';
|
|
226
209
|
}
|
|
210
|
+
const result = await api.get(`/orgs/${orgId}/invitations`, params);
|
|
211
|
+
const invitations = result.invitations || [];
|
|
227
212
|
spinner.stop();
|
|
228
|
-
if (
|
|
213
|
+
if (invitations.length === 0) {
|
|
229
214
|
console.log(chalk.yellow('\nNo invitations found\n'));
|
|
230
215
|
return;
|
|
231
216
|
}
|
|
232
|
-
console.log(chalk.blue.bold(`\n✉️ Invitations (${
|
|
233
|
-
|
|
217
|
+
console.log(chalk.blue.bold(`\n✉️ Invitations (${invitations.length})\n`));
|
|
218
|
+
invitations.forEach((invitation) => {
|
|
234
219
|
console.log(chalk.white.bold(invitation.email));
|
|
235
220
|
console.log(chalk.gray(` Role: ${invitation.role}`));
|
|
236
221
|
console.log(chalk.gray(` Status: ${invitation.status}`));
|
|
237
|
-
if (invitation.
|
|
238
|
-
console.log(chalk.gray(` Invited: ${new Date(invitation.
|
|
222
|
+
if (invitation.createdAt) {
|
|
223
|
+
console.log(chalk.gray(` Invited: ${new Date(invitation.createdAt).toLocaleDateString()}`));
|
|
239
224
|
}
|
|
240
|
-
if (invitation.
|
|
241
|
-
console.log(chalk.gray(` Expires: ${new Date(invitation.
|
|
225
|
+
if (invitation.expiresAt) {
|
|
226
|
+
console.log(chalk.gray(` Expires: ${new Date(invitation.expiresAt).toLocaleDateString()}`));
|
|
242
227
|
}
|
|
243
228
|
console.log('');
|
|
244
229
|
});
|
|
@@ -246,6 +231,7 @@ export async function listInvitationsCommand(options) {
|
|
|
246
231
|
catch (error) {
|
|
247
232
|
spinner.fail(chalk.red('Failed to list invitations'));
|
|
248
233
|
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
234
|
+
process.exitCode = 1;
|
|
249
235
|
}
|
|
250
236
|
}
|
|
251
237
|
/**
|
|
@@ -254,32 +240,31 @@ export async function listInvitationsCommand(options) {
|
|
|
254
240
|
export async function revokeInvitationCommand(email) {
|
|
255
241
|
if (!isAuthenticated()) {
|
|
256
242
|
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const orgId = config.get('organizationId');
|
|
247
|
+
if (!orgId) {
|
|
248
|
+
console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
|
|
249
|
+
process.exitCode = 1;
|
|
257
250
|
return;
|
|
258
251
|
}
|
|
259
|
-
const apiKey = getApiKey();
|
|
260
252
|
const spinner = ora(`Revoking invitation for ${email}...`).start();
|
|
261
253
|
try {
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
},
|
|
268
|
-
body: JSON.stringify({
|
|
269
|
-
action: 'revoke-invitation',
|
|
270
|
-
email
|
|
271
|
-
})
|
|
272
|
-
});
|
|
273
|
-
const data = await response.json();
|
|
274
|
-
if (!data.success) {
|
|
275
|
-
throw new Error(data.error);
|
|
254
|
+
const api = getApiClient();
|
|
255
|
+
const result = await api.get(`/orgs/${orgId}/invitations`);
|
|
256
|
+
const invitation = (result.invitations || []).find((inv) => inv.email === email && inv.status === 'pending');
|
|
257
|
+
if (!invitation) {
|
|
258
|
+
throw new Error(`No pending invitation found for "${email}"`);
|
|
276
259
|
}
|
|
277
|
-
|
|
260
|
+
await api.post(`/orgs/${orgId}/invitations/${invitation.id}/revoke`);
|
|
261
|
+
spinner.succeed(chalk.green(`Invitation for ${email} revoked`));
|
|
278
262
|
console.log('');
|
|
279
263
|
}
|
|
280
264
|
catch (error) {
|
|
281
265
|
spinner.fail(chalk.red('Failed to revoke invitation'));
|
|
282
266
|
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
267
|
+
process.exitCode = 1;
|
|
283
268
|
}
|
|
284
269
|
}
|
|
285
270
|
//# sourceMappingURL=team.js.map
|
package/dist/config.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ const program = new Command();
|
|
|
17
17
|
// Package info
|
|
18
18
|
const packageJson = {
|
|
19
19
|
name: 'langctl',
|
|
20
|
-
version: '0.
|
|
20
|
+
version: '0.2.0',
|
|
21
21
|
description: 'CLI-first translation management for developers'
|
|
22
22
|
};
|
|
23
23
|
program
|
|
@@ -462,7 +462,7 @@ const debug = program
|
|
|
462
462
|
.description('Debug helpers');
|
|
463
463
|
debug
|
|
464
464
|
.command('list-api-keys')
|
|
465
|
-
.description('
|
|
465
|
+
.description('Test API connection and show debug info')
|
|
466
466
|
.action(async () => {
|
|
467
467
|
try {
|
|
468
468
|
await debugListApiKeys();
|
|
@@ -474,7 +474,7 @@ debug
|
|
|
474
474
|
});
|
|
475
475
|
debug
|
|
476
476
|
.command('list-organizations')
|
|
477
|
-
.description('
|
|
477
|
+
.description('Show current organization details')
|
|
478
478
|
.action(async () => {
|
|
479
479
|
try {
|
|
480
480
|
await debugListOrganizations();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "langctl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Translation management CLI for developers. Manage i18n translations, export to 6 formats (iOS, Android, Flutter, i18next), import translations, and collaborate with teams. Affordable alternative to Lokalise and Crowdin.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"langctl": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsc",
|
|
11
|
+
"build": "rm -rf dist && tsc",
|
|
12
12
|
"dev": "tsx src/index.ts",
|
|
13
13
|
"start": "node dist/index.js",
|
|
14
14
|
"prepublishOnly": "npm run build",
|
|
@@ -87,7 +87,6 @@
|
|
|
87
87
|
"node": ">=16.0.0"
|
|
88
88
|
},
|
|
89
89
|
"dependencies": {
|
|
90
|
-
"@supabase/supabase-js": "^2.89.0",
|
|
91
90
|
"chalk": "^5.6.2",
|
|
92
91
|
"commander": "^14.0.2",
|
|
93
92
|
"conf": "^15.0.2",
|