@meterapp/glossa 0.7.0 → 0.7.1

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
@@ -91,3 +91,8 @@ cancellation is safe. An in-flight model request may finish; translation workers
91
91
  check cancellation before storing its results or beginning the next batch.
92
92
  Use `glossa get_job --job <job-id>` to inspect progress and final status, or
93
93
  `glossa list_jobs --status cancelled` to list cancelled jobs.
94
+
95
+ Command help (`glossa sync --help`, `glossa check -h`, or any built-in command
96
+ with `--help`) prints usage without running the command, calling the API, or
97
+ writing files. Help for an API op reads only the public op catalog. Unknown
98
+ commands and failed API calls exit with status 1.
package/bin/glossa.mjs CHANGED
@@ -10,7 +10,7 @@ import { dirname, join, relative, resolve } from "node:path";
10
10
  import { createInterface } from "node:readline";
11
11
  import { brotliCompressSync, brotliDecompressSync, constants as zlib } from "node:zlib";
12
12
 
13
- const VERSION = "0.7.0";
13
+ const VERSION = "0.7.1";
14
14
  const DEFAULT_HOST = "https://getglossa.dev";
15
15
  const CONFIG_PATH = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "glossa", "config.json");
16
16
  /** The last failed call, so `glossa feedback report --last` can attach it. */
@@ -110,7 +110,9 @@ export function parseArgs(argv) {
110
110
  positional.push(...argv.slice(i + 1));
111
111
  break;
112
112
  }
113
- if (arg.startsWith("--")) {
113
+ if (arg === "--help" || arg === "-h") {
114
+ flags.help = true;
115
+ } else if (arg.startsWith("--")) {
114
116
  const [name, inline] = arg.slice(2).split(/=(.*)/s, 2);
115
117
  if (inline !== undefined) flags[name] = inline;
116
118
  else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith("--")) flags[name] = argv[++i];
@@ -1022,9 +1024,21 @@ ${bold("Config")} .glossa.json: {"project": "my-app", "files": ["locales/en.jso
1022
1024
  ${bold("Env")} GLOSSA_API_KEY, GLOSSA_HOST (default ${DEFAULT_HOST})`);
1023
1025
  }
1024
1026
 
1027
+ // Help must return before any command can read project files, write credentials,
1028
+ // start a browser or MCP bridge, or make a mutating request. Dynamic op help
1029
+ // still reads the public catalog below so it uses the current server schema.
1030
+ const BUILTIN_COMMANDS = new Set([
1031
+ "login", "logout", "whoami", "push", "pull", "sync", "missing", "check",
1032
+ "fill", "cancel", "tasks", "mcp", "pricing", "billing", "account",
1033
+ "subscribe", "upgrade", "feedback", "review", "reviews", "ops",
1034
+ ]);
1035
+
1025
1036
  async function main() {
1026
1037
  const [command, ...rest] = process.argv.slice(2);
1027
1038
  const { positional, flags } = parseArgs(rest);
1039
+ if (flags.help && BUILTIN_COMMANDS.has(command)) {
1040
+ return command === "feedback" ? console.log(FEEDBACK_USAGE) : help();
1041
+ }
1028
1042
  if (flags.host && command !== "login") process.env.GLOSSA_HOST = String(flags.host);
1029
1043
  switch (command) {
1030
1044
  case undefined:
@@ -1069,7 +1083,6 @@ async function main() {
1069
1083
  case "upgrade":
1070
1084
  return subscribe(positional, flags);
1071
1085
  case "feedback":
1072
- if (flags.help) return console.log(FEEDBACK_USAGE);
1073
1086
  return feedback(positional, flags);
1074
1087
  case "review":
1075
1088
  return review(flags);
@@ -0,0 +1,101 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createServer } from "node:http";
3
+ import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
8
+
9
+ const cli = fileURLToPath(new URL("./glossa.mjs", import.meta.url));
10
+ const builtins = ["login", "logout", "whoami", "push", "pull", "sync", "missing", "check", "fill", "cancel", "tasks", "mcp", "pricing", "billing", "account", "subscribe", "upgrade", "feedback", "review", "reviews", "ops"];
11
+
12
+ describe("CLI help and exit status", () => {
13
+ let root, server, base, requests;
14
+ const snapshot = () => Object.fromEntries(readdirSync(root, { recursive: true, withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => {
15
+ const path = join(entry.parentPath, entry.name);
16
+ return [path, readFileSync(path, "utf8")];
17
+ }));
18
+ beforeEach(async () => {
19
+ root = mkdtempSync(join(tmpdir(), "glossa-cli-help-"));
20
+ mkdirSync(join(root, "glossa"));
21
+ writeFileSync(join(root, "glossa", "config.json"), JSON.stringify({ api_key: "gls_test_local_only" }));
22
+ writeFileSync(join(root, ".glossa.json"), JSON.stringify({ project: "test", files: ["en.json"], locales: ["es"] }));
23
+ writeFileSync(join(root, "en.json"), '{"hello":"Hello"}');
24
+ requests = [];
25
+ server = createServer((request, response) => {
26
+ requests.push(`${request.method} ${request.url}`);
27
+ response.setHeader("content-type", "application/json");
28
+ if (request.url === "/api/v1") response.end(JSON.stringify({ ops: [{ name: "test_op", description: "Test operation", input_schema: { type: "object" } }] }));
29
+ else {
30
+ response.statusCode = 400;
31
+ response.end(JSON.stringify({ detail: "Test API rejection", request_id: "req_test" }));
32
+ }
33
+ });
34
+ await new Promise((done) => server.listen(0, "127.0.0.1", done));
35
+ base = `http://127.0.0.1:${server.address().port}`;
36
+ });
37
+ afterEach(async () => {
38
+ await new Promise((done) => server.close(done));
39
+ rmSync(root, { recursive: true, force: true });
40
+ });
41
+ const run = (args) => new Promise((done) => {
42
+ const child = spawn(process.execPath, [cli, ...args], { cwd: root, env: { ...process.env, XDG_CONFIG_HOME: root, GLOSSA_HOST: base, GLOSSA_API_KEY: "gls_test_local_only" }, stdio: ["ignore", "pipe", "pipe"] });
43
+ let stdout = "", stderr = "";
44
+ child.stdout.on("data", (data) => { stdout += data; });
45
+ child.stderr.on("data", (data) => { stderr += data; });
46
+ const timeout = setTimeout(() => child.kill(), 2000);
47
+ child.on("close", (code) => { clearTimeout(timeout); done({ code, stdout, stderr }); });
48
+ });
49
+
50
+ it.each(builtins)("%s --help has no API calls or file writes", async (command) => {
51
+ const before = snapshot();
52
+ const result = await run([command, "--help"]);
53
+ expect(result.code, result.stderr).toBe(0);
54
+ expect(result.stdout).toContain("glossa");
55
+ expect(requests).toEqual([]);
56
+ expect(snapshot()).toEqual(before);
57
+ });
58
+
59
+ it("accepts -h without executing sync", async () => {
60
+ const result = await run(["sync", "-h"]);
61
+ expect(result.code, result.stderr).toBe(0);
62
+ expect(result.stdout).toContain("Usage");
63
+ expect(requests).toEqual([]);
64
+ });
65
+
66
+ it("ignores credential and purchase flags when help is requested", async () => {
67
+ const before = snapshot();
68
+ for (const args of [
69
+ ["login", "--help", "--host", base, "--key", "gls_replacement"],
70
+ ["upgrade", "--plan", "pro", "--confirm", "--help"],
71
+ ["feedback", "report", "A test report", "-h"],
72
+ ]) {
73
+ const result = await run(args);
74
+ expect(result.code, result.stderr).toBe(0);
75
+ }
76
+ expect(requests).toEqual([]);
77
+ expect(snapshot()).toEqual(before);
78
+ });
79
+
80
+ it("only reads the catalog for dynamic op help", async () => {
81
+ const before = snapshot();
82
+ const result = await run(["test-op", "--help", "--json", "@missing.json"]);
83
+ expect(result.code, result.stderr).toBe(0);
84
+ expect(result.stdout).toContain("Test operation");
85
+ expect(requests).toEqual(["GET /api/v1"]);
86
+ expect(snapshot()).toEqual(before);
87
+ });
88
+
89
+ it("exits nonzero for unknown commands", async () => {
90
+ const result = await run(["not-a-command"]);
91
+ expect(result.code).toBe(1);
92
+ expect(result.stderr).toContain("unknown command");
93
+ expect(requests).toEqual(["GET /api/v1"]);
94
+ });
95
+
96
+ it("exits nonzero for API errors", async () => {
97
+ const result = await run(["whoami"]);
98
+ expect(result.code).toBe(1);
99
+ expect(result.stderr).toContain("Test API rejection");
100
+ });
101
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meterapp/glossa",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Glossa CLI: push and pull translation files, run the AI review queue, and every other Glossa op from your terminal or CI. Includes a stdio MCP bridge.",
5
5
  "type": "module",
6
6
  "bin": { "glossa": "bin/glossa.mjs" },