@meterapp/glossa 0.7.0 → 0.7.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.
- package/README.md +5 -0
- package/bin/glossa.mjs +19 -5
- package/bin/glossa.test.mjs +4 -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.2";
|
|
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];
|
|
@@ -609,13 +611,14 @@ export function checkInput(flags, config, sources) {
|
|
|
609
611
|
|
|
610
612
|
/**
|
|
611
613
|
* Compares the locale files in the tree with what pull writes now (the hashes check_sync returns)
|
|
612
|
-
* and folds the result into the verdict. A file whose source differs from Glossa's
|
|
614
|
+
* and folds the result into the verdict. A file whose source differs from Glossa's is compared
|
|
615
|
+
* only when the server rendered it from the commit (`outputs_from: "commit"`); older servers didn't.
|
|
613
616
|
*/
|
|
614
617
|
export function compareLocal(result, stale, hashOf) {
|
|
615
618
|
const differ = [];
|
|
616
619
|
for (const file of result.files) {
|
|
617
620
|
for (const out of file.outputs) {
|
|
618
|
-
if (file.source === "changed") out.status = "not_comparable";
|
|
621
|
+
if (file.source === "changed" && file.outputs_from !== "commit") out.status = "not_comparable";
|
|
619
622
|
else {
|
|
620
623
|
const local = hashOf(out.path);
|
|
621
624
|
out.status = local === null ? "missing" : local === out.sha256 ? "in_sync" : "stale";
|
|
@@ -1022,9 +1025,21 @@ ${bold("Config")} .glossa.json: {"project": "my-app", "files": ["locales/en.jso
|
|
|
1022
1025
|
${bold("Env")} GLOSSA_API_KEY, GLOSSA_HOST (default ${DEFAULT_HOST})`);
|
|
1023
1026
|
}
|
|
1024
1027
|
|
|
1028
|
+
// Help must return before any command can read project files, write credentials,
|
|
1029
|
+
// start a browser or MCP bridge, or make a mutating request. Dynamic op help
|
|
1030
|
+
// still reads the public catalog below so it uses the current server schema.
|
|
1031
|
+
const BUILTIN_COMMANDS = new Set([
|
|
1032
|
+
"login", "logout", "whoami", "push", "pull", "sync", "missing", "check",
|
|
1033
|
+
"fill", "cancel", "tasks", "mcp", "pricing", "billing", "account",
|
|
1034
|
+
"subscribe", "upgrade", "feedback", "review", "reviews", "ops",
|
|
1035
|
+
]);
|
|
1036
|
+
|
|
1025
1037
|
async function main() {
|
|
1026
1038
|
const [command, ...rest] = process.argv.slice(2);
|
|
1027
1039
|
const { positional, flags } = parseArgs(rest);
|
|
1040
|
+
if (flags.help && BUILTIN_COMMANDS.has(command)) {
|
|
1041
|
+
return command === "feedback" ? console.log(FEEDBACK_USAGE) : help();
|
|
1042
|
+
}
|
|
1028
1043
|
if (flags.host && command !== "login") process.env.GLOSSA_HOST = String(flags.host);
|
|
1029
1044
|
switch (command) {
|
|
1030
1045
|
case undefined:
|
|
@@ -1069,7 +1084,6 @@ async function main() {
|
|
|
1069
1084
|
case "upgrade":
|
|
1070
1085
|
return subscribe(positional, flags);
|
|
1071
1086
|
case "feedback":
|
|
1072
|
-
if (flags.help) return console.log(FEEDBACK_USAGE);
|
|
1073
1087
|
return feedback(positional, flags);
|
|
1074
1088
|
case "review":
|
|
1075
1089
|
return review(flags);
|
package/bin/glossa.test.mjs
CHANGED
|
@@ -196,7 +196,7 @@ describe("check", () => {
|
|
|
196
196
|
expect(() => checkInput({ stale: "maybe" }, config, [])).toThrow(/--stale/);
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
-
it("compares the tree with pull's output and skips files whose source changed", () => {
|
|
199
|
+
it("compares the tree with pull's output and skips files whose source changed unless rendered from the commit", () => {
|
|
200
200
|
const result = {
|
|
201
201
|
ok: true,
|
|
202
202
|
failures: [],
|
|
@@ -205,11 +205,12 @@ describe("check", () => {
|
|
|
205
205
|
files: [
|
|
206
206
|
{ source: "current", outputs: [{ path: "es.json", sha256: syncHash("es") }, { path: "fr.json", sha256: syncHash("fr") }, { path: "de.json", sha256: syncHash("de") }] },
|
|
207
207
|
{ source: "changed", outputs: [{ path: "other/es.json", sha256: syncHash("x") }] },
|
|
208
|
+
{ source: "changed", outputs_from: "commit", outputs: [{ path: "web/es.json", sha256: syncHash("web") }] },
|
|
208
209
|
],
|
|
209
210
|
};
|
|
210
|
-
const tree = { "es.json": syncHash("es"), "fr.json": syncHash("old") };
|
|
211
|
+
const tree = { "es.json": syncHash("es"), "fr.json": syncHash("old"), "web/es.json": syncHash("web") };
|
|
211
212
|
const out = compareLocal(result, "fail", (path) => tree[path] ?? null);
|
|
212
|
-
expect(out.files.flatMap((f) => f.outputs.map((o) => o.status))).toEqual(["in_sync", "stale", "missing", "not_comparable"]);
|
|
213
|
+
expect(out.files.flatMap((f) => f.outputs.map((o) => o.status))).toEqual(["in_sync", "stale", "missing", "not_comparable", "in_sync"]);
|
|
213
214
|
expect(out).toMatchObject({ ok: false, totals: { stale: 2 }, failures: [{ code: "stale_files" }] });
|
|
214
215
|
expect(compareLocal(result, "warn", (path) => tree[path] ?? null)).toMatchObject({ ok: true, warnings: [expect.stringContaining("2 locale files")] });
|
|
215
216
|
});
|
|
@@ -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.2",
|
|
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" },
|