@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 +5 -0
- package/bin/glossa.mjs +16 -3
- package/bin/help.test.mjs +101 -0
- package/package.json +1 -1
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.
|
|
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
|
|
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.
|
|
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" },
|