@ownlate/cli 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +235 -74
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -2,8 +2,196 @@
2
2
  let commander = require("commander");
3
3
  let node_readline_promises = require("node:readline/promises");
4
4
  let node_fs_promises = require("node:fs/promises");
5
+ let node_os = require("node:os");
5
6
  let node_path = require("node:path");
6
7
 
8
+ //#region src/lib/api-client.ts
9
+ var ApiClient = class {
10
+ constructor(baseUrl, apiKey, workspaceId) {
11
+ this.baseUrl = baseUrl.replace(/\/$/, "");
12
+ this.apiKey = apiKey;
13
+ this.workspaceId = workspaceId;
14
+ }
15
+ authHeaders() {
16
+ const headers = { Authorization: `Bearer ${this.apiKey}` };
17
+ if (this.workspaceId) headers["X-Tenant-Id"] = this.workspaceId;
18
+ return headers;
19
+ }
20
+ async request(path, init) {
21
+ const res = await fetch(`${this.baseUrl}${path}`, {
22
+ ...init,
23
+ headers: {
24
+ ...this.authHeaders(),
25
+ ...init?.headers ?? {}
26
+ }
27
+ });
28
+ if (!res.ok) {
29
+ const text = await res.text();
30
+ throw new Error(`HTTP ${res.status}: ${text}`);
31
+ }
32
+ return res.json();
33
+ }
34
+ async listWorkspaces() {
35
+ return this.request("/v1/workspaces");
36
+ }
37
+ async listProjects(workspaceId) {
38
+ return this.request(`/v1/projects?workspaceId=${encodeURIComponent(workspaceId)}`);
39
+ }
40
+ async upload(dto) {
41
+ return this.request("/v1/translation-files/upload", {
42
+ method: "POST",
43
+ headers: { "Content-Type": "application/json" },
44
+ body: JSON.stringify(dto)
45
+ });
46
+ }
47
+ async exportTranslations(params) {
48
+ const query = new URLSearchParams({
49
+ projectId: params.projectId,
50
+ language: params.language,
51
+ format: params.format
52
+ });
53
+ if (params.fileId) query.set("fileId", params.fileId);
54
+ if (params.sourceLanguage) query.set("sourceLanguage", params.sourceLanguage);
55
+ const res = await fetch(`${this.baseUrl}/v1/segments/export?${query}`, { headers: this.authHeaders() });
56
+ if (!res.ok) {
57
+ const text = await res.text();
58
+ throw new Error(`HTTP ${res.status}: ${text}`);
59
+ }
60
+ return res.text();
61
+ }
62
+ };
63
+
64
+ //#endregion
65
+ //#region src/lib/global-config.ts
66
+ const CONFIG_PATH = (0, node_path.join)((0, node_os.homedir)(), ".ownlate");
67
+ const DEFAULT_API_URL = "https://api.ownlate.com";
68
+ async function readGlobalConfig() {
69
+ try {
70
+ const raw = await (0, node_fs_promises.readFile)(CONFIG_PATH, "utf-8");
71
+ return JSON.parse(raw);
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+ async function writeGlobalConfig(config) {
77
+ await (0, node_fs_promises.writeFile)(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
78
+ }
79
+ async function requireGlobalConfig() {
80
+ const config = await readGlobalConfig();
81
+ if (!config?.apiKey) throw new Error("Not logged in. Run \"ownlate login\" first.");
82
+ return config;
83
+ }
84
+
85
+ //#endregion
86
+ //#region src/commands/login.ts
87
+ async function ask$1(rl, question, fallback = "") {
88
+ return (await rl.question(question)).trim() || fallback;
89
+ }
90
+ async function loginCommand(options) {
91
+ const rl = (0, node_readline_promises.createInterface)({
92
+ input: process.stdin,
93
+ output: process.stdout
94
+ });
95
+ try {
96
+ const apiUrl = options.url ?? await ask$1(rl, `API URL [${DEFAULT_API_URL}]: `, DEFAULT_API_URL);
97
+ const apiKey = await ask$1(rl, "API Key: ");
98
+ if (!apiKey) {
99
+ console.error("API Key is required.");
100
+ process.exit(1);
101
+ }
102
+ process.stdout.write("\nConnecting...");
103
+ const client = new ApiClient(apiUrl, apiKey);
104
+ let workspaces;
105
+ try {
106
+ workspaces = await client.listWorkspaces();
107
+ console.log(" ok");
108
+ } catch (err) {
109
+ console.log(" failed");
110
+ console.error(`\n${err.message}`);
111
+ process.exit(1);
112
+ }
113
+ let currentWorkspaceId;
114
+ if (workspaces.length === 0) console.log("\nNo workspaces found. Create one in the web app first.");
115
+ else if (workspaces.length === 1) {
116
+ currentWorkspaceId = workspaces[0].id;
117
+ console.log(`\nUsing workspace: ${workspaces[0].name}`);
118
+ } else {
119
+ console.log("\nAvailable workspaces:");
120
+ workspaces.forEach((w, i) => console.log(` ${i + 1}. ${w.name}`));
121
+ const choice = await ask$1(rl, "\nSelect workspace [1]: ", "1");
122
+ const idx = parseInt(choice, 10) - 1;
123
+ const picked = workspaces[Math.max(0, Math.min(idx, workspaces.length - 1))];
124
+ currentWorkspaceId = picked.id;
125
+ console.log(`Using: ${picked.name}`);
126
+ }
127
+ await writeGlobalConfig({
128
+ apiUrl,
129
+ apiKey,
130
+ currentWorkspaceId
131
+ });
132
+ console.log("\nLogged in. Run \"ownlate init\" to set up a project.");
133
+ } finally {
134
+ rl.close();
135
+ }
136
+ }
137
+
138
+ //#endregion
139
+ //#region src/commands/logout.ts
140
+ async function logoutCommand() {
141
+ try {
142
+ await (0, node_fs_promises.unlink)((0, node_path.join)((0, node_os.homedir)(), ".ownlate"));
143
+ console.log("Logged out.");
144
+ } catch {
145
+ console.log("Already logged out.");
146
+ }
147
+ }
148
+
149
+ //#endregion
150
+ //#region src/commands/workspace.ts
151
+ async function workspaceListCommand() {
152
+ const auth = await requireGlobalConfig();
153
+ const workspaces = await new ApiClient(auth.apiUrl, auth.apiKey, auth.currentWorkspaceId).listWorkspaces();
154
+ if (workspaces.length === 0) {
155
+ console.log("No workspaces found.");
156
+ return;
157
+ }
158
+ for (const w of workspaces) {
159
+ const marker = w.id === auth.currentWorkspaceId ? "* " : " ";
160
+ console.log(`${marker}${w.name} ${w.id}`);
161
+ }
162
+ }
163
+ async function workspaceUseCommand(idOrName) {
164
+ const auth = await requireGlobalConfig();
165
+ const found = (await new ApiClient(auth.apiUrl, auth.apiKey, auth.currentWorkspaceId).listWorkspaces()).find((w) => w.id === idOrName || w.name === idOrName);
166
+ if (!found) {
167
+ console.error(`Workspace not found: "${idOrName}"`);
168
+ console.log("Run \"ownlate workspace list\" to see available workspaces.");
169
+ process.exit(1);
170
+ }
171
+ await writeGlobalConfig({
172
+ ...auth,
173
+ currentWorkspaceId: found.id
174
+ });
175
+ console.log(`Now using workspace: ${found.name}`);
176
+ }
177
+
178
+ //#endregion
179
+ //#region src/commands/project.ts
180
+ async function projectListCommand() {
181
+ const auth = await requireGlobalConfig();
182
+ if (!auth.currentWorkspaceId) {
183
+ console.error("No workspace selected. Run \"ownlate workspace use <id>\" first.");
184
+ process.exit(1);
185
+ }
186
+ const projects = await new ApiClient(auth.apiUrl, auth.apiKey, auth.currentWorkspaceId).listProjects(auth.currentWorkspaceId);
187
+ if (projects.length === 0) {
188
+ console.log("No projects found in current workspace.");
189
+ return;
190
+ }
191
+ for (const p of projects) console.log(` ${p.name} ${p.id}`);
192
+ }
193
+
194
+ //#endregion
7
195
  //#region src/commands/init.ts
8
196
  async function ask(rl, question, fallback = "") {
9
197
  return (await rl.question(question)).trim() || fallback;
@@ -15,22 +203,40 @@ async function initCommand(options) {
15
203
  console.error(`Config already exists: ${configPath}`);
16
204
  process.exit(1);
17
205
  } catch {}
206
+ const auth = await requireGlobalConfig();
207
+ const client = new ApiClient(auth.apiUrl, auth.apiKey, auth.currentWorkspaceId);
18
208
  const rl = (0, node_readline_promises.createInterface)({
19
209
  input: process.stdin,
20
210
  output: process.stdout
21
211
  });
22
212
  try {
23
- console.log("Initializing ownlate config...\n");
24
- const apiUrl = await ask(rl, "API URL [http://localhost:3000]: ", "http://localhost:3000");
25
- const apiKey = await ask(rl, "API Key: ");
26
- const projectId = await ask(rl, "Project ID: ");
213
+ console.log("Initializing ownlate project config...\n");
214
+ let projectId;
215
+ if (auth.currentWorkspaceId) {
216
+ let projects = [];
217
+ try {
218
+ projects = await client.listProjects(auth.currentWorkspaceId);
219
+ } catch {}
220
+ if (projects.length > 0) {
221
+ console.log("Projects in current workspace:");
222
+ projects.forEach((p, i) => console.log(` ${i + 1}. ${p.name} (${p.id})`));
223
+ console.log(` ${projects.length + 1}. Enter ID manually\n`);
224
+ const choice = await ask(rl, "Select project: ");
225
+ const idx = parseInt(choice, 10) - 1;
226
+ if (idx >= 0 && idx < projects.length) {
227
+ projectId = projects[idx].id;
228
+ console.log(`Using: ${projects[idx].name}`);
229
+ } else projectId = await ask(rl, "Project ID: ");
230
+ } else projectId = await ask(rl, "Project ID: ");
231
+ } else {
232
+ console.log("No workspace selected. Run \"ownlate workspace use <id>\" to pick one, or enter the project ID manually.\n");
233
+ projectId = await ask(rl, "Project ID: ");
234
+ }
27
235
  const sourceLanguage = await ask(rl, "Source language [en]: ", "en");
28
236
  const targetLangsRaw = await ask(rl, "Target languages, comma-separated [fr,de]: ", "fr,de");
29
237
  const sourceFile = await ask(rl, "Source file path [locales/en.json]: ", "locales/en.json");
30
238
  const outputPattern = await ask(rl, "Output pattern [locales/{{language}}.json]: ", "locales/{{language}}.json");
31
239
  const config = {
32
- apiUrl,
33
- apiKey,
34
240
  projectId,
35
241
  sourceLanguage,
36
242
  targetLanguages: targetLangsRaw.split(",").map((l) => l.trim()).filter(Boolean),
@@ -51,25 +257,16 @@ async function initCommand(options) {
51
257
 
52
258
  //#endregion
53
259
  //#region src/lib/config.ts
54
- async function readConfig(configPath) {
260
+ async function readProjectConfig(configPath) {
55
261
  const absPath = (0, node_path.resolve)(configPath);
56
262
  let raw;
57
263
  try {
58
264
  raw = await (0, node_fs_promises.readFile)(absPath, "utf-8");
59
265
  } catch {
60
- throw new Error(`Config file not found: ${absPath}\nRun "ownlate init" to create one.`);
61
- }
62
- let config;
63
- try {
64
- config = JSON.parse(raw);
65
- } catch {
66
- throw new Error(`Config file is not valid JSON: ${absPath}`);
266
+ throw new Error(`Config not found: ${absPath}\nRun "ownlate init" to create one.`);
67
267
  }
68
- config.apiUrl = process.env.OWNLATE_API_URL ?? config.apiUrl;
69
- config.apiKey = process.env.OWNLATE_API_KEY ?? config.apiKey;
268
+ const config = JSON.parse(raw);
70
269
  for (const field of [
71
- "apiUrl",
72
- "apiKey",
73
270
  "projectId",
74
271
  "sourceLanguage",
75
272
  "targetLanguages",
@@ -77,52 +274,10 @@ async function readConfig(configPath) {
77
274
  ]) if (!config[field]) throw new Error(`Missing required config field: "${field}"`);
78
275
  return config;
79
276
  }
80
- async function writeConfig(configPath, config) {
277
+ async function writeProjectConfig(configPath, config) {
81
278
  await (0, node_fs_promises.writeFile)((0, node_path.resolve)(configPath), JSON.stringify(config, null, 2) + "\n");
82
279
  }
83
280
 
84
- //#endregion
85
- //#region src/lib/api-client.ts
86
- var ApiClient = class {
87
- constructor(baseUrl, apiKey) {
88
- this.baseUrl = baseUrl.replace(/\/$/, "");
89
- this.apiKey = apiKey;
90
- }
91
- authHeaders() {
92
- return { Authorization: `Bearer ${this.apiKey}` };
93
- }
94
- async upload(dto) {
95
- const res = await fetch(`${this.baseUrl}/v1/translation-files/upload`, {
96
- method: "POST",
97
- headers: {
98
- ...this.authHeaders(),
99
- "Content-Type": "application/json"
100
- },
101
- body: JSON.stringify(dto)
102
- });
103
- if (!res.ok) {
104
- const text = await res.text();
105
- throw new Error(`HTTP ${res.status}: ${text}`);
106
- }
107
- return res.json();
108
- }
109
- async exportTranslations(params) {
110
- const query = new URLSearchParams({
111
- projectId: params.projectId,
112
- language: params.language,
113
- format: params.format
114
- });
115
- if (params.fileId) query.set("fileId", params.fileId);
116
- if (params.sourceLanguage) query.set("sourceLanguage", params.sourceLanguage);
117
- const res = await fetch(`${this.baseUrl}/v1/segments/export?${query}`, { headers: this.authHeaders() });
118
- if (!res.ok) {
119
- const text = await res.text();
120
- throw new Error(`HTTP ${res.status}: ${text}`);
121
- }
122
- return res.text();
123
- }
124
- };
125
-
126
281
  //#endregion
127
282
  //#region src/commands/push.ts
128
283
  function detectFormat$1(filePath) {
@@ -133,10 +288,10 @@ function detectFormat$1(filePath) {
133
288
  throw new Error(`Unsupported extension "${ext}". Use .json, .yaml, .yml or .po`);
134
289
  }
135
290
  async function pushCommand(options) {
136
- const config = await readConfig(options.config);
137
- const client = new ApiClient(config.apiUrl, config.apiKey);
291
+ const [auth, config] = await Promise.all([requireGlobalConfig(), readProjectConfig(options.config)]);
292
+ const client = new ApiClient(auth.apiUrl, auth.apiKey, auth.currentWorkspaceId);
138
293
  let configUpdated = false;
139
- console.log(`Pushing to ${config.apiUrl} (project: ${config.projectId})\n`);
294
+ console.log(`Pushing to ${auth.apiUrl} (project: ${config.projectId})\n`);
140
295
  for (const file of config.files) {
141
296
  const absPath = (0, node_path.resolve)(file.source);
142
297
  let content;
@@ -149,13 +304,12 @@ async function pushCommand(options) {
149
304
  const format = detectFormat$1(file.source);
150
305
  const name = (0, node_path.basename)(file.source);
151
306
  const dir = (0, node_path.dirname)(file.source);
152
- const filePath = dir === "." ? "" : dir;
153
307
  process.stdout.write(` ${file.source} ...`);
154
308
  try {
155
309
  const result = await client.upload({
156
310
  projectId: config.projectId,
157
311
  name,
158
- path: filePath,
312
+ path: dir === "." ? "" : dir,
159
313
  format,
160
314
  content,
161
315
  fileId: file.fileId
@@ -171,7 +325,7 @@ async function pushCommand(options) {
171
325
  }
172
326
  }
173
327
  if (configUpdated) {
174
- await writeConfig(options.config, config);
328
+ await writeProjectConfig(options.config, config);
175
329
  console.log(`\nSaved file IDs to ${options.config}`);
176
330
  }
177
331
  }
@@ -186,10 +340,10 @@ function detectFormat(pattern) {
186
340
  throw new Error(`Cannot detect format from output pattern "${pattern}"`);
187
341
  }
188
342
  async function pullCommand(options) {
189
- const config = await readConfig(options.config);
190
- const client = new ApiClient(config.apiUrl, config.apiKey);
343
+ const [auth, config] = await Promise.all([requireGlobalConfig(), readProjectConfig(options.config)]);
344
+ const client = new ApiClient(auth.apiUrl, auth.apiKey, auth.currentWorkspaceId);
191
345
  const languages = options.lang ? [options.lang] : config.targetLanguages;
192
- console.log(`Pulling from ${config.apiUrl} (project: ${config.projectId})\n`);
346
+ console.log(`Pulling from ${auth.apiUrl} (project: ${config.projectId})\n`);
193
347
  for (const file of config.files) {
194
348
  const format = detectFormat(file.output);
195
349
  for (const language of languages) {
@@ -201,7 +355,8 @@ async function pullCommand(options) {
201
355
  projectId: config.projectId,
202
356
  language,
203
357
  format,
204
- fileId: file.fileId
358
+ fileId: file.fileId,
359
+ sourceLanguage: config.sourceLanguage
205
360
  });
206
361
  await (0, node_fs_promises.mkdir)((0, node_path.dirname)(absPath), { recursive: true });
207
362
  await (0, node_fs_promises.writeFile)(absPath, content);
@@ -217,13 +372,19 @@ async function pullCommand(options) {
217
372
  //#endregion
218
373
  //#region src/index.ts
219
374
  const program = new commander.Command();
220
- program.name("ownlate").description("Sync translation files with your ownlate project").version("1.0.0");
375
+ program.name("ownlate").description("Sync translation files with your ownlate project").version("1.0.1");
376
+ program.command("login").description("Log in with your API key").option("--url <url>", "API URL (default: https://api.ownlate.com)").action(loginCommand);
377
+ program.command("logout").description("Remove stored credentials").action(logoutCommand);
378
+ const workspace = program.command("workspace").description("Manage workspaces");
379
+ workspace.command("list").description("List available workspaces").action(workspaceListCommand);
380
+ workspace.command("use <id>").description("Switch current workspace by ID or name").action(workspaceUseCommand);
381
+ program.command("project").description("Manage projects").command("list").description("List projects in the current workspace").action(projectListCommand);
221
382
  const configOpt = [
222
383
  "-c, --config <path>",
223
- "path to config file",
384
+ "path to project config file",
224
385
  ".ownlate.json"
225
386
  ];
226
- program.command("init").description("Create .ownlate.json config interactively").option(...configOpt).action(initCommand);
387
+ program.command("init").description("Create .ownlate.json for this project").option(...configOpt).action(initCommand);
227
388
  program.command("push").description("Upload source files — creates/updates segments in ownlate").option(...configOpt).action(pushCommand);
228
389
  program.command("pull").description("Download translated files from ownlate to local filesystem").option(...configOpt).option("-l, --lang <code>", "pull a single language only (e.g. fr)").action(pullCommand);
229
390
  program.parseAsync(process.argv).catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ownlate/cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Ownlate CLI — push source files and pull translations",
5
5
  "type": "module",
6
6
  "files": [