@ownlate/cli 1.0.0 → 1.0.2

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