@intlpullhq/cli 0.1.7 → 0.1.9

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 CHANGED
@@ -19,6 +19,31 @@
19
19
 
20
20
  ---
21
21
 
22
+ ## Table of Contents
23
+
24
+ | Section | Description |
25
+ |---------|-------------|
26
+ | [Features](#features) | Key capabilities of the CLI |
27
+ | [Installation](#installation) | How to install the CLI |
28
+ | [Quick Start](#quick-start) | Get started in 4 commands |
29
+ | [Authentication](#authentication) | Login, API keys, and auth methods |
30
+ | [Commands Reference](#commands-reference) | All available commands |
31
+ | ↳ [Initialize](#initialize-project) | Set up IntlPull in your project |
32
+ | ↳ [Upload / Push](#upload--push) | Upload translation keys |
33
+ | ↳ [Download / Pull](#download--pull) | Download translations |
34
+ | ↳ [Sync](#sync) | One-time sync for CI/CD |
35
+ | ↳ [Listen / Watch](#listen--watch) | Real-time sync during dev |
36
+ | ↳ [Import / Export](#import) | Bulk import/export translations |
37
+ | ↳ [OTA Releases](#ota-releases) | Over-the-air updates |
38
+ | ↳ [Migration](#migration) | Migrate from other platforms |
39
+ | [CI/CD Integration](#cicd-integration) | GitHub Actions, GitLab CI, Docker |
40
+ | [Configuration](#configuration) | Project config and output patterns |
41
+ | ↳ [Output Patterns](#output-patterns-namespace-support) | Namespace file structures |
42
+ | [Tips & Best Practices](#tips--best-practices) | Recommended usage patterns |
43
+ | [Support](#support) | Get help |
44
+
45
+ ---
46
+
22
47
  ## Features
23
48
 
24
49
  - **Parallel Downloads** - Fetch translations by namespace concurrently for blazing fast syncs
@@ -792,35 +817,54 @@ npx @intlpullhq/cli pull --output "locales/[namespace]/[locale].json"
792
817
 
793
818
  **Important:** Ensure your i18n library's configuration matches the output path:
794
819
 
795
- - **next-intl**: Update `src/i18n/request.ts` to load from the correct path
796
- - **i18next / react-i18next**: Update `backend.loadPath` in your i18n config
797
- - **Chrome Extension**: Must use `_locales/[locale]/messages.json` (Chrome requirement)
798
- ```
820
+ | Library | Config Update Required |
821
+ |---------|----------------------|
822
+ | **next-intl** | Update `src/i18n/request.ts` to load from the correct path |
823
+ | **i18next / react-i18next** | Update `backend.loadPath` in your i18n config |
824
+ | **Chrome Extension** | Must use `_locales/[locale]/messages.json` (Chrome requirement) |
799
825
 
800
826
  ### Environment Variables
801
827
 
802
828
  | Variable | Description |
803
829
  |----------|-------------|
804
- | `INTLPULL_API_KEY` | API key for authentication |
805
- | `INTLPULL_DEBUG` | Enable debug logging |
830
+ | `INTLPULL_API_KEY` | API key for authentication (required) |
831
+ | `INTLPULL_PROJECT_ID` | Default project ID |
832
+ | `INTLPULL_DEBUG` | Enable debug logging (`true`/`false`) |
806
833
 
807
834
  ---
808
835
 
809
836
  ## Tips & Best Practices
810
837
 
811
- 1. **Use project-scoped API keys** - Auto-detects project ID, no need to specify `--project`
812
- 2. **Enable parallel mode** (default) - Downloads namespaces concurrently for faster syncs
813
- 3. **Use `--quiet` in CI** - Minimal output, machine-friendly
814
- 4. **Use `--dry-run`** - Preview changes before applying
815
- 5. **Set up Git branch detection** - CLI auto-detects git branch for branch workflows
838
+ | Practice | Recommendation |
839
+ |----------|----------------|
840
+ | 🔑 **API Keys** | Use project-scoped keys for auto-detection (no `--project` flag needed) |
841
+ | **Parallel Mode** | Enabled by default — downloads namespaces concurrently |
842
+ | 🤫 **CI/CD Output** | Use `--quiet` for minimal, machine-friendly logs |
843
+ | 🔍 **Preview Changes** | Use `--dry-run` before applying destructive operations |
844
+ | 🌿 **Branch Workflows** | CLI auto-detects git branch for branch-based translations |
845
+ | 📦 **Namespace Files** | Use output patterns like `[locale]/[namespace].json` for organized files |
816
846
 
817
847
  ---
818
848
 
819
849
  ## Support
820
850
 
821
- - Documentation: [docs.intlpull.com](https://docs.intlpull.com)
822
- - Email: support@intlpull.com
851
+ | Resource | Link |
852
+ |:---------|:-----|
853
+ | 📚 Documentation | [docs.intlpull.com](https://docs.intlpull.com) |
854
+ | 📧 Email Support | [support@intlpull.com](mailto:support@intlpull.com) |
855
+ | 🐛 GitHub Issues | [github.com/intlpullhq/cli/issues](https://github.com/intlpullhq/cli/issues) |
856
+ | 💬 Discord | [discord.gg/intlpull](https://discord.gg/intlpull) |
857
+
858
+ ---
823
859
 
824
- ## License
860
+ <p align="center">
861
+ <a href="#features">Features</a> •
862
+ <a href="#installation">Installation</a> •
863
+ <a href="#commands-reference">Commands</a> •
864
+ <a href="#cicd-integration">CI/CD</a> •
865
+ <a href="#configuration">Config</a>
866
+ </p>
825
867
 
826
- MIT © [IntlPull](https://intlpull.com)
868
+ <p align="center">
869
+ MIT © <a href="https://intlpull.com">IntlPull</a>
870
+ </p>
@@ -0,0 +1,47 @@
1
+ import {
2
+ ApiClient,
3
+ ApiKeysApi,
4
+ BillingApi,
5
+ ContributorsApi,
6
+ DocumentsApi,
7
+ EmailsApi,
8
+ ImportExportApi,
9
+ PLAN_LIMITS,
10
+ SnapshotsApi,
11
+ TMApi,
12
+ WebhooksApi,
13
+ WorkflowsApi,
14
+ createApiClient
15
+ } from "./chunk-WVCVQFBI.js";
16
+ import {
17
+ ProjectsApi
18
+ } from "./chunk-KCZQUMQP.js";
19
+ import {
20
+ TranslationsApi
21
+ } from "./chunk-WSY27J6N.js";
22
+ import {
23
+ KeysApi
24
+ } from "./chunk-BULIQM4U.js";
25
+ import {
26
+ BaseApiClient
27
+ } from "./chunk-KIDP7N6D.js";
28
+ import "./chunk-IWYURZV2.js";
29
+ export {
30
+ ApiClient,
31
+ ApiKeysApi,
32
+ BaseApiClient,
33
+ BillingApi,
34
+ ContributorsApi,
35
+ DocumentsApi,
36
+ EmailsApi,
37
+ ImportExportApi,
38
+ KeysApi,
39
+ PLAN_LIMITS,
40
+ ProjectsApi,
41
+ SnapshotsApi,
42
+ TMApi,
43
+ TranslationsApi,
44
+ WebhooksApi,
45
+ WorkflowsApi,
46
+ createApiClient
47
+ };
@@ -0,0 +1,73 @@
1
+ import {
2
+ BaseApiClient
3
+ } from "./chunk-KIDP7N6D.js";
4
+
5
+ // src/lib/api/keys.ts
6
+ var KeysApi = class extends BaseApiClient {
7
+ /**
8
+ * List translation keys for a project
9
+ */
10
+ async listKeys(projectId, options = {}) {
11
+ const params = new URLSearchParams();
12
+ if (options.namespace) {
13
+ params.append("namespace", options.namespace);
14
+ }
15
+ if (options.search) {
16
+ params.append("search", options.search);
17
+ }
18
+ if (options.tags && options.tags.length > 0) {
19
+ params.append("tags", options.tags.join(","));
20
+ }
21
+ if (options.limit) {
22
+ params.append("limit", options.limit.toString());
23
+ }
24
+ const query = params.toString();
25
+ const endpoint = `/api/v1/projects/${projectId}/keys${query ? `?${query}` : ""}`;
26
+ return this.request(endpoint);
27
+ }
28
+ /**
29
+ * Get a specific translation key
30
+ */
31
+ async getKey(projectId, keyId) {
32
+ return this.request(`/api/v1/projects/${projectId}/keys/${keyId}`);
33
+ }
34
+ /**
35
+ * Create a new translation key
36
+ */
37
+ async createKey(projectId, data) {
38
+ return this.request(`/api/v1/projects/${projectId}/keys`, {
39
+ method: "POST",
40
+ body: JSON.stringify(data)
41
+ });
42
+ }
43
+ /**
44
+ * Update an existing translation key
45
+ */
46
+ async updateKey(projectId, keyId, data) {
47
+ return this.request(`/api/v1/projects/${projectId}/keys/${keyId}`, {
48
+ method: "PATCH",
49
+ body: JSON.stringify(data)
50
+ });
51
+ }
52
+ /**
53
+ * Delete a translation key
54
+ */
55
+ async deleteKey(projectId, keyId) {
56
+ return this.request(`/api/v1/projects/${projectId}/keys/${keyId}`, {
57
+ method: "DELETE"
58
+ });
59
+ }
60
+ /**
61
+ * Bulk delete translation keys
62
+ */
63
+ async bulkDeleteKeys(projectId, keyIds) {
64
+ return this.request(`/api/v1/projects/${projectId}/keys/bulk-delete`, {
65
+ method: "POST",
66
+ body: JSON.stringify({ key_ids: keyIds })
67
+ });
68
+ }
69
+ };
70
+
71
+ export {
72
+ KeysApi
73
+ };
@@ -1,7 +1,7 @@
1
1
  // src/lib/config.ts
2
2
  import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, chmodSync, renameSync } from "fs";
3
3
  import { homedir } from "os";
4
- import { join, dirname } from "path";
4
+ import { join, dirname, resolve, relative } from "path";
5
5
  function writeFileAtomic(filePath, content, options) {
6
6
  const dir = dirname(filePath);
7
7
  if (options?.createDir && !existsSync(dir)) {
@@ -331,7 +331,16 @@ function resolveOutputDir(outputDir, projectRoot) {
331
331
  if (outputDir.startsWith("/") || outputDir.match(/^[A-Z]:\\/i)) {
332
332
  return outputDir;
333
333
  }
334
- return join(projectRoot, outputDir);
334
+ const resolvedPath = resolve(projectRoot, outputDir);
335
+ const relativePath = relative(projectRoot, resolvedPath);
336
+ if (relativePath.startsWith("..") || relativePath.includes("/../")) {
337
+ throw new Error(
338
+ `Invalid output directory: path traversal detected. Output directory must be within the project directory.
339
+ Project root: ${projectRoot}
340
+ Attempted path: ${outputDir}`
341
+ );
342
+ }
343
+ return resolvedPath;
335
344
  }
336
345
  function detectGitBranch(dir = process.cwd()) {
337
346
  try {
@@ -0,0 +1,64 @@
1
+ import {
2
+ BaseApiClient
3
+ } from "./chunk-KIDP7N6D.js";
4
+
5
+ // src/lib/api/projects.ts
6
+ var ProjectsApi = class extends BaseApiClient {
7
+ async listProjects() {
8
+ return this.request("/api/v1/projects");
9
+ }
10
+ async getProject(projectId) {
11
+ return this.request(`/api/v1/projects/${projectId}`);
12
+ }
13
+ async createProject(data) {
14
+ return this.request("/api/v1/projects", {
15
+ method: "POST",
16
+ body: JSON.stringify(data)
17
+ });
18
+ }
19
+ /**
20
+ * Find a project by name (searches through all projects)
21
+ */
22
+ async findProjectByName(name) {
23
+ const { projects } = await this.listProjects();
24
+ const normalizedName = name.toLowerCase().trim();
25
+ return projects.find(
26
+ (p) => p.name.toLowerCase() === normalizedName || p.slug.toLowerCase() === normalizedName
27
+ ) || null;
28
+ }
29
+ /**
30
+ * Get project languages/settings
31
+ */
32
+ async getProjectLanguages(projectId) {
33
+ return this.request(`/api/v1/projects/${projectId}/language-settings`);
34
+ }
35
+ /**
36
+ * Add multiple languages to a project with optional skip_translation flag
37
+ */
38
+ async addLanguagesBulk(projectId, languages, skipTranslation = false) {
39
+ return this.request(`/api/v1/projects/${projectId}/language-settings/bulk`, {
40
+ method: "POST",
41
+ body: JSON.stringify({
42
+ languages,
43
+ skip_translation: skipTranslation
44
+ })
45
+ });
46
+ }
47
+ // Versions
48
+ async listVersions(projectId) {
49
+ return this.request(`/api/v1/projects/${projectId}/versions`);
50
+ }
51
+ async createVersion(projectId, data) {
52
+ return this.request(`/api/v1/projects/${projectId}/versions`, {
53
+ method: "POST",
54
+ body: JSON.stringify(data)
55
+ });
56
+ }
57
+ async downloadVersion(projectId, version) {
58
+ return this.requestBlob(`/api/v1/projects/${projectId}/versions/${version}/download`);
59
+ }
60
+ };
61
+
62
+ export {
63
+ ProjectsApi
64
+ };
@@ -0,0 +1,149 @@
1
+ import {
2
+ getAuthErrorMessage,
3
+ getGlobalConfig,
4
+ getResolvedApiKey
5
+ } from "./chunk-IWYURZV2.js";
6
+
7
+ // src/lib/api/base.ts
8
+ var DEFAULT_API_URL = process.env.INTLPULL_API_URL || "https://api.intlpull.com";
9
+ var DEFAULT_TIMEOUT_MS = 3e4;
10
+ var BaseApiClient = class {
11
+ baseUrl;
12
+ apiKey;
13
+ timeout;
14
+ constructor() {
15
+ const globalConfig = getGlobalConfig();
16
+ this.baseUrl = globalConfig.apiUrl || DEFAULT_API_URL;
17
+ const resolved = getResolvedApiKey();
18
+ this.apiKey = resolved?.key || null;
19
+ this.timeout = DEFAULT_TIMEOUT_MS;
20
+ }
21
+ async request(endpoint, options = {}) {
22
+ if (!this.apiKey) {
23
+ throw new Error(getAuthErrorMessage());
24
+ }
25
+ const url = `${this.baseUrl}${endpoint}`;
26
+ const headers = {
27
+ "Content-Type": "application/json",
28
+ "X-API-Key": this.apiKey,
29
+ ...options.headers || {}
30
+ };
31
+ const controller = new AbortController();
32
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
33
+ let response;
34
+ try {
35
+ response = await fetch(url, {
36
+ ...options,
37
+ headers,
38
+ signal: controller.signal
39
+ });
40
+ } catch (err) {
41
+ clearTimeout(timeoutId);
42
+ if (err instanceof Error && err.name === "AbortError") {
43
+ throw new Error(`Request timeout: Server did not respond within ${this.timeout / 1e3}s`);
44
+ }
45
+ if (err instanceof TypeError) {
46
+ throw new Error(`Network error: Unable to connect to ${this.baseUrl}. Check your internet connection.`);
47
+ }
48
+ throw new Error(`Request failed: ${err instanceof Error ? err.message : "Unknown network error"}`);
49
+ } finally {
50
+ clearTimeout(timeoutId);
51
+ }
52
+ if (!response.ok) {
53
+ const error = await response.json().catch(() => ({ error: `Request failed: ${response.status}` }));
54
+ throw new Error(error.error || `Request failed: ${response.status}`);
55
+ }
56
+ const text = await response.text();
57
+ if (!text) {
58
+ return {};
59
+ }
60
+ try {
61
+ return JSON.parse(text);
62
+ } catch {
63
+ throw new Error(`Invalid JSON response from API`);
64
+ }
65
+ }
66
+ async requestBlob(endpoint) {
67
+ if (!this.apiKey) {
68
+ throw new Error(getAuthErrorMessage());
69
+ }
70
+ const url = `${this.baseUrl}${endpoint}`;
71
+ const controller = new AbortController();
72
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
73
+ let response;
74
+ try {
75
+ response = await fetch(url, {
76
+ headers: {
77
+ "X-API-Key": this.apiKey
78
+ },
79
+ signal: controller.signal
80
+ });
81
+ } catch (err) {
82
+ clearTimeout(timeoutId);
83
+ if (err instanceof Error && err.name === "AbortError") {
84
+ throw new Error(`Request timeout: Server did not respond within ${this.timeout / 1e3}s`);
85
+ }
86
+ if (err instanceof TypeError) {
87
+ throw new Error(`Network error: Unable to connect to ${this.baseUrl}. Check your internet connection.`);
88
+ }
89
+ throw new Error(`Request failed: ${err instanceof Error ? err.message : "Unknown network error"}`);
90
+ } finally {
91
+ clearTimeout(timeoutId);
92
+ }
93
+ if (!response.ok) {
94
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
95
+ throw new Error(error.error || "Request failed");
96
+ }
97
+ return response.blob();
98
+ }
99
+ async requestBlobWithJsonFallback(endpoint) {
100
+ if (!this.apiKey) {
101
+ throw new Error(getAuthErrorMessage());
102
+ }
103
+ const url = `${this.baseUrl}${endpoint}`;
104
+ const controller = new AbortController();
105
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
106
+ let response;
107
+ try {
108
+ response = await fetch(url, {
109
+ headers: {
110
+ "X-API-Key": this.apiKey
111
+ },
112
+ signal: controller.signal
113
+ });
114
+ } catch (err) {
115
+ clearTimeout(timeoutId);
116
+ if (err instanceof Error && err.name === "AbortError") {
117
+ throw new Error(`Request timeout: Server did not respond within ${this.timeout / 1e3}s`);
118
+ }
119
+ if (err instanceof TypeError) {
120
+ throw new Error(`Network error: Unable to connect to ${this.baseUrl}. Check your internet connection.`);
121
+ }
122
+ throw new Error(`Request failed: ${err instanceof Error ? err.message : "Unknown network error"}`);
123
+ } finally {
124
+ clearTimeout(timeoutId);
125
+ }
126
+ if (!response.ok) {
127
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
128
+ throw new Error(error.error || "Request failed");
129
+ }
130
+ const contentType = response.headers.get("content-type") || "";
131
+ if (contentType.includes("application/json")) {
132
+ const jsonData = await response.json();
133
+ if (jsonData.url && typeof jsonData.url === "string") {
134
+ const fileResponse = await fetch(jsonData.url);
135
+ if (!fileResponse.ok) {
136
+ throw new Error(`Failed to download file: ${fileResponse.status}`);
137
+ }
138
+ return fileResponse.blob();
139
+ }
140
+ const jsonString = JSON.stringify(jsonData, null, 2);
141
+ return new Blob([jsonString], { type: "application/json" });
142
+ }
143
+ return response.blob();
144
+ }
145
+ };
146
+
147
+ export {
148
+ BaseApiClient
149
+ };
@@ -0,0 +1,116 @@
1
+ import {
2
+ BaseApiClient
3
+ } from "./chunk-KIDP7N6D.js";
4
+ import {
5
+ getProjectConfig
6
+ } from "./chunk-IWYURZV2.js";
7
+
8
+ // src/lib/api/translations.ts
9
+ var TranslationsApi = class extends BaseApiClient {
10
+ // Keys - uses import endpoint to create/update keys
11
+ async pushKeys(projectId, keys, language = "en", namespace, options) {
12
+ const content = {};
13
+ for (const k of keys) {
14
+ content[k.key] = k.value;
15
+ }
16
+ const fileName = namespace ? `${namespace}.json` : "push.json";
17
+ const result = await this.request(`/api/v1/projects/${projectId}/import`, {
18
+ method: "POST",
19
+ body: JSON.stringify({
20
+ content: JSON.stringify(content),
21
+ file_name: fileName,
22
+ language,
23
+ options: {
24
+ namespace_name: namespace || "common",
25
+ update_existing: true,
26
+ branch_id: options?.branchId,
27
+ platforms: options?.platforms
28
+ }
29
+ })
30
+ });
31
+ return {
32
+ keys_inserted: result.keys_inserted,
33
+ keys_updated: result.keys_updated,
34
+ keys_skipped: result.keys_skipped,
35
+ translations_inserted: result.translations_inserted,
36
+ translations_updated: result.translations_updated
37
+ };
38
+ }
39
+ async getKeys(projectId, namespace) {
40
+ const params = namespace ? `?namespace=${namespace}` : "";
41
+ return this.request(`/api/v1/projects/${projectId}/keys${params}`);
42
+ }
43
+ // Translations - uses export/download endpoint
44
+ async pullTranslations(projectId, options) {
45
+ const params = new URLSearchParams();
46
+ params.set("format", "json");
47
+ if (options.languages?.length) {
48
+ options.languages.forEach((lang) => params.append("languages", lang));
49
+ }
50
+ if (options.branch) {
51
+ params.set("branch", options.branch);
52
+ }
53
+ if (options.platform) {
54
+ params.set("platform", options.platform);
55
+ }
56
+ return this.request(`/api/v1/projects/${projectId}/export/download?${params.toString()}`);
57
+ }
58
+ // Glossary
59
+ async searchGlossary(query, options) {
60
+ const params = new URLSearchParams();
61
+ params.set("q", query);
62
+ if (options?.limit) params.set("limit", String(options.limit));
63
+ if (options?.language) params.set("language", options.language);
64
+ return this.request(`/api/v1/glossary/search?${params.toString()}`);
65
+ }
66
+ async addGlossaryTerm(data) {
67
+ const config = getProjectConfig();
68
+ if (!config?.projectId) {
69
+ throw new Error("No project configured. Run `npx @intlpullhq/cli init` first.");
70
+ }
71
+ return this.request(`/api/v1/projects/${config.projectId}/glossary`, {
72
+ method: "POST",
73
+ body: JSON.stringify(data)
74
+ });
75
+ }
76
+ async exportGlossary(glossaryId) {
77
+ const config = getProjectConfig();
78
+ if (!config?.projectId) {
79
+ throw new Error("No project configured. Run `npx @intlpullhq/cli init` first.");
80
+ }
81
+ const params = glossaryId ? `?glossary_id=${glossaryId}` : "";
82
+ return this.request(`/api/v1/projects/${config.projectId}/glossary/export${params}`);
83
+ }
84
+ // Translation Memory
85
+ async searchTM(data) {
86
+ const config = getProjectConfig();
87
+ if (!config?.projectId) {
88
+ throw new Error("No project configured. Run `npx @intlpullhq/cli init` first.");
89
+ }
90
+ return this.request(`/api/v1/projects/${config.projectId}/memory/search`, {
91
+ method: "POST",
92
+ body: JSON.stringify(data)
93
+ });
94
+ }
95
+ async addTMEntry(data) {
96
+ const config = getProjectConfig();
97
+ if (!config?.projectId) {
98
+ throw new Error("No project configured. Run `npx @intlpullhq/cli init` first.");
99
+ }
100
+ return this.request(`/api/v1/projects/${config.projectId}/memory`, {
101
+ method: "POST",
102
+ body: JSON.stringify(data)
103
+ });
104
+ }
105
+ async getTMStats() {
106
+ const config = getProjectConfig();
107
+ if (!config?.projectId) {
108
+ throw new Error("No project configured. Run `npx @intlpullhq/cli init` first.");
109
+ }
110
+ return this.request(`/api/v1/projects/${config.projectId}/memory/stats`);
111
+ }
112
+ };
113
+
114
+ export {
115
+ TranslationsApi
116
+ };