@ownlate/cli 1.0.2 → 1.0.4

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 +225 -131
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -5,17 +5,37 @@ let node_fs_promises = require("node:fs/promises");
5
5
  let node_os = require("node:os");
6
6
  let node_path = require("node:path");
7
7
 
8
+ //#region src/lib/global-config.ts
9
+ const CONFIG_PATH = (0, node_path.join)((0, node_os.homedir)(), ".ownlate");
10
+ const API_URL = process.env.DEPLOY_ENV === "dev" ? "http://127.0.0.1:3000" : "https://api.ownlate.com";
11
+ async function readGlobalConfig() {
12
+ try {
13
+ const raw = await (0, node_fs_promises.readFile)(CONFIG_PATH, "utf-8");
14
+ return JSON.parse(raw);
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ async function writeGlobalConfig(config) {
20
+ await (0, node_fs_promises.writeFile)(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
21
+ }
22
+ async function requireGlobalConfig() {
23
+ const config = await readGlobalConfig();
24
+ if (!config?.apiKey) throw new Error("Not logged in. Run \"ownlate login\" first.");
25
+ return config;
26
+ }
27
+
28
+ //#endregion
8
29
  //#region src/lib/api-client.ts
9
30
  var ApiClient = class {
10
- constructor(baseUrl, apiKey) {
11
- this.baseUrl = baseUrl.replace(/\/$/, "");
31
+ constructor(apiKey) {
12
32
  this.apiKey = apiKey;
13
33
  }
14
34
  authHeaders() {
15
35
  return { Authorization: `Bearer ${this.apiKey}` };
16
36
  }
17
37
  async request(path, init) {
18
- const res = await fetch(`${this.baseUrl}${path}`, {
38
+ const res = await fetch(`${API_URL}${path}`, {
19
39
  ...init,
20
40
  headers: {
21
41
  ...this.authHeaders(),
@@ -34,6 +54,27 @@ var ApiClient = class {
34
54
  async listProjects(workspaceId) {
35
55
  return this.request(`/v1/projects?workspaceId=${encodeURIComponent(workspaceId)}`);
36
56
  }
57
+ async createProject(dto) {
58
+ return this.request("/v1/projects", {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify(dto)
62
+ });
63
+ }
64
+ async getProgress(projectId, targetLanguages) {
65
+ const query = new URLSearchParams({
66
+ projectId,
67
+ targetLanguages: targetLanguages.join(",")
68
+ });
69
+ return this.request(`/v1/segments/progress?${query}`);
70
+ }
71
+ async preTranslate(params) {
72
+ await this.request("/v1/segments/pre-translate", {
73
+ method: "POST",
74
+ headers: { "Content-Type": "application/json" },
75
+ body: JSON.stringify(params)
76
+ });
77
+ }
37
78
  async upload(dto) {
38
79
  return this.request("/v1/translation-files/upload", {
39
80
  method: "POST",
@@ -49,7 +90,7 @@ var ApiClient = class {
49
90
  });
50
91
  if (params.fileId) query.set("fileId", params.fileId);
51
92
  if (params.sourceLanguage) query.set("sourceLanguage", params.sourceLanguage);
52
- const res = await fetch(`${this.baseUrl}/v1/segments/export?${query}`, { headers: this.authHeaders() });
93
+ const res = await fetch(`${API_URL}/v1/segments/export?${query}`, { headers: this.authHeaders() });
53
94
  if (!res.ok) {
54
95
  const text = await res.text();
55
96
  throw new Error(`HTTP ${res.status}: ${text}`);
@@ -58,46 +99,21 @@ var ApiClient = class {
58
99
  }
59
100
  };
60
101
 
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
102
  //#endregion
83
103
  //#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) {
104
+ async function loginCommand() {
88
105
  const rl = (0, node_readline_promises.createInterface)({
89
106
  input: process.stdin,
90
107
  output: process.stdout
91
108
  });
92
109
  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: ");
110
+ const apiKey = (await rl.question("API Key: ")).trim();
95
111
  if (!apiKey) {
96
112
  console.error("API Key is required.");
97
113
  process.exit(1);
98
114
  }
99
- process.stdout.write("\nConnecting...");
100
- const client = new ApiClient(apiUrl, apiKey);
115
+ process.stdout.write("Connecting...");
116
+ const client = new ApiClient(apiKey);
101
117
  let workspaces;
102
118
  try {
103
119
  workspaces = await client.listWorkspaces();
@@ -107,26 +123,17 @@ async function loginCommand(options) {
107
123
  console.error(`\n${err.message}`);
108
124
  process.exit(1);
109
125
  }
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}`);
126
+ if (workspaces.length === 0) {
127
+ console.error("\nNo workspace found for this API key.");
128
+ process.exit(1);
123
129
  }
130
+ const workspace = workspaces[0];
124
131
  await writeGlobalConfig({
125
- apiUrl,
126
132
  apiKey,
127
- currentWorkspaceId
133
+ workspaceId: workspace.id
128
134
  });
129
- console.log("\nLogged in. Run \"ownlate init\" to set up a project.");
135
+ console.log(`\nLogged in to workspace: ${workspace.name}`);
136
+ console.log("Run \"ownlate project list\" to see your projects.");
130
137
  } finally {
131
138
  rl.close();
132
139
  }
@@ -144,48 +151,45 @@ async function logoutCommand() {
144
151
  }
145
152
 
146
153
  //#endregion
147
- //#region src/commands/workspace.ts
148
- async function workspaceListCommand() {
154
+ //#region src/commands/project.ts
155
+ async function projectListCommand() {
149
156
  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.");
157
+ const projects = await new ApiClient(auth.apiKey).listProjects(auth.workspaceId);
158
+ if (projects.length === 0) {
159
+ console.log("No projects found.");
153
160
  return;
154
161
  }
155
- for (const w of workspaces) {
156
- const marker = w.id === auth.currentWorkspaceId ? "* " : " ";
157
- console.log(`${marker}${w.name} ${w.id}`);
158
- }
162
+ for (const p of projects) console.log(` ${p.name.padEnd(32)} ${p.id}`);
159
163
  }
160
- async function workspaceUseCommand(idOrName) {
164
+ async function projectCreateCommand(name, options) {
161
165
  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
166
+ const rl = (0, node_readline_promises.createInterface)({
167
+ input: process.stdin,
168
+ output: process.stdout
171
169
  });
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;
170
+ try {
171
+ const ask = async (question, fallback = "") => {
172
+ return (await rl.question(question)).trim() || fallback;
173
+ };
174
+ const projectName = name ?? await ask("Project name: ");
175
+ if (!projectName) {
176
+ console.error("Project name is required.");
177
+ process.exit(1);
178
+ }
179
+ const sourceLanguage = options.source ?? await ask("Source language [en]: ", "en");
180
+ const targetLanguages = (options.langs ?? await ask("Target languages, comma-separated [fr,de]: ", "fr,de")).split(",").map((l) => l.trim()).filter(Boolean);
181
+ const result = await new ApiClient(auth.apiKey).createProject({
182
+ workspaceId: auth.workspaceId,
183
+ name: projectName,
184
+ sourceLanguage,
185
+ targetLanguages
186
+ });
187
+ console.log(`\nCreated project: ${projectName}`);
188
+ console.log(`ID: ${result.id}`);
189
+ console.log(`\nRun "ownlate init" in your project directory to set it up.`);
190
+ } finally {
191
+ rl.close();
187
192
  }
188
- for (const p of projects) console.log(` ${p.name} ${p.id}`);
189
193
  }
190
194
 
191
195
  //#endregion
@@ -201,37 +205,29 @@ async function initCommand(options) {
201
205
  process.exit(1);
202
206
  } catch {}
203
207
  const auth = await requireGlobalConfig();
204
- const client = new ApiClient(auth.apiUrl, auth.apiKey);
208
+ const client = new ApiClient(auth.apiKey);
205
209
  const rl = (0, node_readline_promises.createInterface)({
206
210
  input: process.stdin,
207
211
  output: process.stdout
208
212
  });
209
213
  try {
210
- console.log("Initializing ownlate project config...\n");
214
+ let projects = [];
215
+ try {
216
+ projects = await client.listProjects(auth.workspaceId);
217
+ } catch {}
211
218
  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
- }
219
+ if (projects.length > 0) {
220
+ console.log("Projects:\n");
221
+ projects.forEach((p, i) => console.log(` ${i + 1}. ${p.name} (${p.id})`));
222
+ console.log(` ${projects.length + 1}. Enter ID manually\n`);
223
+ const choice = await ask(rl, "Select project: ");
224
+ const idx = parseInt(choice, 10) - 1;
225
+ if (idx >= 0 && idx < projects.length) projectId = projects[idx].id;
226
+ else projectId = await ask(rl, "Project ID: ");
227
+ } else projectId = await ask(rl, "Project ID: ");
232
228
  const sourceLanguage = await ask(rl, "Source language [en]: ", "en");
233
229
  const targetLangsRaw = await ask(rl, "Target languages, comma-separated [fr,de]: ", "fr,de");
234
- const sourceFile = await ask(rl, "Source file path [locales/en.json]: ", "locales/en.json");
230
+ const sourceFile = await ask(rl, "Source file [locales/en.json]: ", "locales/en.json");
235
231
  const outputPattern = await ask(rl, "Output pattern [locales/{{language}}.json]: ", "locales/{{language}}.json");
236
232
  const config = {
237
233
  projectId,
@@ -244,9 +240,8 @@ async function initCommand(options) {
244
240
  };
245
241
  await (0, node_fs_promises.writeFile)(configPath, JSON.stringify(config, null, 2) + "\n");
246
242
  console.log(`\nCreated ${configPath}`);
247
- console.log("\nNext steps:");
248
- console.log(" ownlate pushupload source strings to ownlate");
249
- console.log(" ownlate pull — download translations to local files");
243
+ console.log("\n ownlate push — upload source strings");
244
+ console.log(" ownlate pulldownload translations");
250
245
  } finally {
251
246
  rl.close();
252
247
  }
@@ -284,24 +279,21 @@ function detectFormat$1(filePath) {
284
279
  if (ext === ".po") return "po";
285
280
  throw new Error(`Unsupported extension "${ext}". Use .json, .yaml, .yml or .po`);
286
281
  }
287
- async function pushCommand(options) {
288
- const [auth, config] = await Promise.all([requireGlobalConfig(), readProjectConfig(options.config)]);
289
- const client = new ApiClient(auth.apiUrl, auth.apiKey);
282
+ async function pushAll(client, config, configPath, prefix = "") {
290
283
  let configUpdated = false;
291
- console.log(`Pushing to ${auth.apiUrl} (project: ${config.projectId})\n`);
292
284
  for (const file of config.files) {
293
285
  const absPath = (0, node_path.resolve)(file.source);
294
286
  let content;
295
287
  try {
296
288
  content = await (0, node_fs_promises.readFile)(absPath, "utf-8");
297
289
  } catch {
298
- console.error(` [skip] ${file.source} — file not found`);
290
+ console.error(`${prefix} [skip] ${file.source} — file not found (${absPath})`);
299
291
  continue;
300
292
  }
301
293
  const format = detectFormat$1(file.source);
302
294
  const name = (0, node_path.basename)(file.source);
303
295
  const dir = (0, node_path.dirname)(file.source);
304
- process.stdout.write(` ${file.source} ...`);
296
+ process.stdout.write(`${prefix} pushing ${file.source} ...`);
305
297
  try {
306
298
  const result = await client.upload({
307
299
  projectId: config.projectId,
@@ -318,12 +310,41 @@ async function pushCommand(options) {
318
310
  console.log(` ${result.segmentsCreated} segments`);
319
311
  } catch (err) {
320
312
  console.log(" failed");
321
- console.error(` [!] ${err.message}`);
313
+ console.error(`${prefix} [!] ${err.message}`);
322
314
  }
323
315
  }
324
- if (configUpdated) {
325
- await writeProjectConfig(options.config, config);
326
- console.log(`\nSaved file IDs to ${options.config}`);
316
+ if (configUpdated) await writeProjectConfig(configPath, config);
317
+ }
318
+ async function pushCommand(options) {
319
+ const configPath = (0, node_path.resolve)(options.config);
320
+ const [auth, config] = await Promise.all([requireGlobalConfig(), readProjectConfig(configPath)]);
321
+ const client = new ApiClient(auth.apiKey);
322
+ console.log(`project: ${config.projectId}`);
323
+ console.log(`config: ${configPath}\n`);
324
+ await pushAll(client, config, configPath);
325
+ if (!options.watch) return;
326
+ const sources = config.files.map((f) => (0, node_path.resolve)(f.source));
327
+ console.log(`\nWatching ${sources.map((s) => s.split("/").pop()).join(", ")}...\n`);
328
+ const timers = /* @__PURE__ */ new Map();
329
+ for (const absPath of sources) watchFile(absPath, client, config, configPath, timers);
330
+ }
331
+ async function watchFile(absPath, client, config, configPath, timers) {
332
+ try {
333
+ const watcher = (0, node_fs_promises.watch)(absPath);
334
+ for await (const event of watcher) {
335
+ if (event.eventType !== "change") continue;
336
+ const existing = timers.get(absPath);
337
+ if (existing) clearTimeout(existing);
338
+ timers.set(absPath, setTimeout(async () => {
339
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
340
+ process.stdout.write(`[${time}] ${absPath.split("/").pop()} changed → `);
341
+ await pushAll(client, config, configPath, "");
342
+ }, 300));
343
+ }
344
+ } catch {
345
+ setTimeout(() => {
346
+ watchFile(absPath, client, config, configPath, timers);
347
+ }, 1e3);
327
348
  }
328
349
  }
329
350
 
@@ -337,10 +358,12 @@ function detectFormat(pattern) {
337
358
  throw new Error(`Cannot detect format from output pattern "${pattern}"`);
338
359
  }
339
360
  async function pullCommand(options) {
340
- const [auth, config] = await Promise.all([requireGlobalConfig(), readProjectConfig(options.config)]);
341
- const client = new ApiClient(auth.apiUrl, auth.apiKey);
361
+ const configPath = (0, node_path.resolve)(options.config);
362
+ const [auth, config] = await Promise.all([requireGlobalConfig(), readProjectConfig(configPath)]);
363
+ const client = new ApiClient(auth.apiKey);
342
364
  const languages = options.lang ? [options.lang] : config.targetLanguages;
343
- console.log(`Pulling from ${auth.apiUrl} (project: ${config.projectId})\n`);
365
+ console.log(`project: ${config.projectId}`);
366
+ console.log(`config: ${configPath}\n`);
344
367
  for (const file of config.files) {
345
368
  const format = detectFormat(file.output);
346
369
  for (const language of languages) {
@@ -366,24 +389,95 @@ async function pullCommand(options) {
366
389
  }
367
390
  }
368
391
 
392
+ //#endregion
393
+ //#region src/commands/status.ts
394
+ const BAR_WIDTH = 24;
395
+ function bar(progress) {
396
+ const filled = Math.round(progress / 100 * BAR_WIDTH);
397
+ return "█".repeat(filled) + "░".repeat(BAR_WIDTH - filled);
398
+ }
399
+ function pad(s, n) {
400
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
401
+ }
402
+ async function statusCommand(options) {
403
+ const configPath = (0, node_path.resolve)(options.config);
404
+ const [auth, config] = await Promise.all([requireGlobalConfig(), readProjectConfig(configPath)]);
405
+ const client = new ApiClient(auth.apiKey);
406
+ let items;
407
+ try {
408
+ items = await client.getProgress(config.projectId, config.targetLanguages);
409
+ } catch (err) {
410
+ console.error(`Failed: ${err.message}`);
411
+ process.exit(1);
412
+ }
413
+ if (items.length === 0) {
414
+ console.log("No segments found for this project.");
415
+ return;
416
+ }
417
+ console.log(`project: ${config.projectId}\n`);
418
+ for (const item of items) {
419
+ const pct = `${item.progress}%`.padStart(4);
420
+ const detail = `(${item.approved}/${item.total} approved)`;
421
+ console.log(` ${pad(item.language, 6)} ${bar(item.progress)} ${pct} ${detail}`);
422
+ }
423
+ const minArg = options.min ? parseInt(options.min, 10) : void 0;
424
+ if (minArg !== void 0) {
425
+ const below = items.filter((i) => i.progress < minArg);
426
+ if (below.length > 0) {
427
+ console.error(`\nFailed: ${below.map((i) => i.language).join(", ")} below ${minArg}%`);
428
+ process.exit(1);
429
+ }
430
+ }
431
+ }
432
+
433
+ //#endregion
434
+ //#region src/commands/translate.ts
435
+ async function translateCommand(options) {
436
+ const configPath = (0, node_path.resolve)(options.config);
437
+ const [auth, config] = await Promise.all([requireGlobalConfig(), readProjectConfig(configPath)]);
438
+ const client = new ApiClient(auth.apiKey);
439
+ const languages = options.lang ? [options.lang] : config.targetLanguages;
440
+ const minTmScore = options.score ? parseInt(options.score, 10) : 100;
441
+ console.log(`project: ${config.projectId}`);
442
+ console.log(`score: ≥${minTmScore}%\n`);
443
+ for (const lang of languages) {
444
+ process.stdout.write(` pre-translating ${lang} ...`);
445
+ try {
446
+ await client.preTranslate({
447
+ projectId: config.projectId,
448
+ workspaceId: auth.workspaceId,
449
+ targetLanguage: lang,
450
+ sourceLanguage: config.sourceLanguage,
451
+ minTmScore
452
+ });
453
+ console.log(" queued");
454
+ } catch (err) {
455
+ console.log(" failed");
456
+ console.error(` [!] ${err.message}`);
457
+ }
458
+ }
459
+ console.log("\nTranslation runs in background. Use \"ownlate status\" to check progress.");
460
+ }
461
+
369
462
  //#endregion
370
463
  //#region src/index.ts
371
464
  const program = new commander.Command();
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);
465
+ program.name("ownlate").description("Sync translation files with your ownlate project").version("1.0.4");
466
+ program.command("login").description("Log in with your API key").action(loginCommand);
374
467
  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);
468
+ const project = program.command("project").description("Manage projects");
469
+ project.command("list").description("List projects in your workspace").action(projectListCommand);
470
+ project.command("create [name]").description("Create a new project").option("-s, --source <lang>", "source language (e.g. en)").option("-l, --langs <codes>", "target languages, comma-separated (e.g. fr,de)").action(projectCreateCommand);
379
471
  const configOpt = [
380
472
  "-c, --config <path>",
381
473
  "path to project config file",
382
474
  ".ownlate.json"
383
475
  ];
384
476
  program.command("init").description("Create .ownlate.json for this project").option(...configOpt).action(initCommand);
385
- program.command("push").description("Upload source files creates/updates segments in ownlate").option(...configOpt).action(pushCommand);
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);
477
+ program.command("push").description("Upload source files to ownlate").option(...configOpt).option("-w, --watch", "watch source files and push on change").action(pushCommand);
478
+ program.command("pull").description("Download translations to local files").option(...configOpt).option("-l, --lang <code>", "pull a single language only").action(pullCommand);
479
+ program.command("status").description("Show translation progress for each language").option(...configOpt).option("--min <percent>", "exit with code 1 if any language is below this threshold (for CI)").action(statusCommand);
480
+ program.command("translate").description("Pre-translate untranslated segments using Translation Memory").option(...configOpt).option("-l, --lang <code>", "translate a single language only").option("-s, --score <n>", "minimum TM match score 0–100 (default: 100)").action(translateCommand);
387
481
  program.parseAsync(process.argv).catch((err) => {
388
482
  console.error(err.message);
389
483
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ownlate/cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Ownlate CLI — push source files and pull translations",
5
5
  "type": "module",
6
6
  "files": [