@meterapp/glossa 0.4.0 → 0.6.0
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 +9 -0
- package/bin/glossa.mjs +207 -1
- package/bin/glossa.test.mjs +50 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,3 +39,12 @@ open the printed URL on any device (`--no-browser` skips opening one). To save
|
|
|
39
39
|
an existing key instead, run `login --key gls_…`.
|
|
40
40
|
|
|
41
41
|
In CI set `GLOSSA_API_KEY` (a workspace key, `gls_…`).
|
|
42
|
+
|
|
43
|
+
### Cancel a background job
|
|
44
|
+
|
|
45
|
+
`glossa cancel <job-id>` cancels queued or running work. `glossa cancel_job --job <job-id>`
|
|
46
|
+
is the equivalent registry op. Completed translations remain in place, and repeated
|
|
47
|
+
cancellation is safe. An in-flight model request may finish; translation workers
|
|
48
|
+
check cancellation before storing its results or beginning the next batch.
|
|
49
|
+
Use `glossa get_job --job <job-id>` to inspect progress and final status, or
|
|
50
|
+
`glossa list_jobs --status cancelled` to list cancelled jobs.
|
package/bin/glossa.mjs
CHANGED
|
@@ -9,9 +9,11 @@ import { dirname, join, relative, resolve } from "node:path";
|
|
|
9
9
|
import { createInterface } from "node:readline";
|
|
10
10
|
import { brotliCompressSync, brotliDecompressSync, constants as zlib } from "node:zlib";
|
|
11
11
|
|
|
12
|
-
const VERSION = "0.
|
|
12
|
+
const VERSION = "0.6.0";
|
|
13
13
|
const DEFAULT_HOST = "https://getglossa.dev";
|
|
14
14
|
const CONFIG_PATH = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "glossa", "config.json");
|
|
15
|
+
/** The last failed call, so `glossa feedback report --last` can attach it. */
|
|
16
|
+
const LAST_ERROR_PATH = join(dirname(CONFIG_PATH), "last-error.json");
|
|
15
17
|
const PROJECT_CONFIG = ".glossa.json";
|
|
16
18
|
|
|
17
19
|
// ------------------------------------------------------------------ config
|
|
@@ -73,11 +75,24 @@ async function call(op, input = {}, { method = "POST", anonymous = false } = {})
|
|
|
73
75
|
const error = new Error(body.detail || `${op} failed with HTTP ${response.status}`);
|
|
74
76
|
error.status = response.status;
|
|
75
77
|
error.body = body;
|
|
78
|
+
if (op !== "report_issue") rememberFailure(op, response.status, body, response.headers.get("x-request-id"));
|
|
76
79
|
throw error;
|
|
77
80
|
}
|
|
78
81
|
return body;
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
/** Saves what `report --last` attaches: never the input, only the request id and the error. */
|
|
85
|
+
function rememberFailure(op, status, body, headerId) {
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(dirname(LAST_ERROR_PATH), { recursive: true });
|
|
88
|
+
const record = { request_id: body.request_id ?? headerId ?? null, op, status, error_code: body.code ?? body.title ?? null, error: String(body.detail ?? "").slice(0, 2000), at: new Date().toISOString() };
|
|
89
|
+
writeFileSync(LAST_ERROR_PATH, JSON.stringify(record, null, 2) + "\n", { mode: 0o600 });
|
|
90
|
+
} catch {
|
|
91
|
+
// Best effort: a read-only home directory must not hide the real error.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const lastFailure = () => readJson(LAST_ERROR_PATH);
|
|
95
|
+
|
|
81
96
|
async function manifest() {
|
|
82
97
|
const response = await fetch(`${host()}/api/v1`, { headers: { "User-Agent": `glossa-cli/${VERSION}` } });
|
|
83
98
|
if (!response.ok) fail(`could not read the op catalog from ${host()}/api/v1 (HTTP ${response.status}).`);
|
|
@@ -480,6 +495,14 @@ async function fill(positional, flags) {
|
|
|
480
495
|
if (totals.rejected.length) process.exitCode = 1;
|
|
481
496
|
}
|
|
482
497
|
|
|
498
|
+
export function cancelJobInput(positional, flags) {
|
|
499
|
+
const job = positional[0] ?? flags.job;
|
|
500
|
+
if (positional.length > 1 || typeof job !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(job)) {
|
|
501
|
+
throw new Error("Usage: glossa cancel <job-id> (or --job <job-id>)");
|
|
502
|
+
}
|
|
503
|
+
return { job };
|
|
504
|
+
}
|
|
505
|
+
|
|
483
506
|
async function tasks(flags) {
|
|
484
507
|
const { tasks: list, total } = await call("list_tasks", coerceFlags(flags, { properties: { limit: { type: "integer" }, kind: { type: "string" }, status: { type: "string" } } }));
|
|
485
508
|
if (flags.json || !isTTY) return print({ total, tasks: list }, flags);
|
|
@@ -521,6 +544,173 @@ async function mcp() {
|
|
|
521
544
|
});
|
|
522
545
|
}
|
|
523
546
|
|
|
547
|
+
// ---------------------------------------------------------------- feedback
|
|
548
|
+
const FEEDBACK_USAGE = `glossa feedback list [--kind feature|issue] [--status active|all|done…] [--sort top|new|recent_activity] [--q text]
|
|
549
|
+
glossa feedback show <id>
|
|
550
|
+
glossa feedback mine
|
|
551
|
+
glossa feedback request "<title>" [--body text] [--force]
|
|
552
|
+
glossa feedback report "<title>" [--body text] [--request-id id] [--op op] [--project p --key k --locale l --file f] [--last] [--force]
|
|
553
|
+
glossa feedback upvote <id> [--remove]
|
|
554
|
+
glossa feedback comment <id> "<text>"
|
|
555
|
+
glossa review --rating 1-5 [--title t] [--body b] [--likes l] [--dislikes d] [--use-case u] [--name n] [--company c] [--consent]
|
|
556
|
+
glossa reviews [--sort newest|helpful|highest|lowest]`;
|
|
557
|
+
|
|
558
|
+
const str = (value) => (value === undefined || value === true ? undefined : String(value));
|
|
559
|
+
const compact = (object) => Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined && value !== ""));
|
|
560
|
+
|
|
561
|
+
/** `report` input: the title, body, flags as context, and with --last the last failed call. */
|
|
562
|
+
export function reportInput(title, flags, last = null) {
|
|
563
|
+
if (!title) throw new Error('give the problem a title: glossa feedback report "push_file drops plural forms" --last');
|
|
564
|
+
if (flags.last && !last) throw new Error("no failed call recorded yet; pass --request-id from the error instead.");
|
|
565
|
+
const fromLast = flags.last && last ? compact({ request_id: last.request_id ?? undefined, op: last.op, status: last.status, error_code: last.error_code ?? undefined, error: last.error }) : {};
|
|
566
|
+
// Flags win over the recorded failure.
|
|
567
|
+
const context = {
|
|
568
|
+
...fromLast,
|
|
569
|
+
...compact({
|
|
570
|
+
request_id: str(flags["request-id"] ?? flags.request_id),
|
|
571
|
+
op: str(flags.op),
|
|
572
|
+
project: str(flags.project),
|
|
573
|
+
key: str(flags.key),
|
|
574
|
+
locale: str(flags.locale),
|
|
575
|
+
file: str(flags.file),
|
|
576
|
+
client: `glossa-cli/${VERSION}`,
|
|
577
|
+
}),
|
|
578
|
+
};
|
|
579
|
+
return compact({ title, details: str(flags.body ?? flags.details), context, force: flags.force ? true : undefined });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** `review` flags as submit_review input. */
|
|
583
|
+
export function reviewInput(flags) {
|
|
584
|
+
const rating = Number(flags.rating);
|
|
585
|
+
if (!Number.isInteger(rating) || rating < 1 || rating > 5) throw new Error("rate 1 to 5: glossa review --rating 5 --body \"…\"");
|
|
586
|
+
return compact({
|
|
587
|
+
rating,
|
|
588
|
+
title: str(flags.title),
|
|
589
|
+
body: str(flags.body),
|
|
590
|
+
likes: str(flags.likes),
|
|
591
|
+
dislikes: str(flags.dislikes),
|
|
592
|
+
use_case: str(flags["use-case"] ?? flags.use_case),
|
|
593
|
+
display_name: str(flags.name),
|
|
594
|
+
display_company: str(flags.company),
|
|
595
|
+
marketing_consent: flags.consent ? true : undefined,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** `feedback <sub> …` as the op to call and its input. */
|
|
600
|
+
export function feedbackCall(positional, flags, last = null) {
|
|
601
|
+
const [sub, ...rest] = positional;
|
|
602
|
+
const id = rest[0];
|
|
603
|
+
const needId = () => {
|
|
604
|
+
if (!/^\d+$/.test(String(id ?? ""))) throw new Error(`which item? glossa feedback ${sub} <id>`);
|
|
605
|
+
return Number(id);
|
|
606
|
+
};
|
|
607
|
+
switch (sub) {
|
|
608
|
+
case undefined:
|
|
609
|
+
case "list":
|
|
610
|
+
return { op: "list_feedback", anonymous: true, input: compact({ kind: str(flags.kind), status: str(flags.status), sort: str(flags.sort), q: str(flags.q), limit: flags.limit ? Number(flags.limit) : undefined, offset: flags.offset ? Number(flags.offset) : undefined }) };
|
|
611
|
+
case "show":
|
|
612
|
+
return { op: "get_feedback", anonymous: true, input: { id: needId() } };
|
|
613
|
+
case "mine":
|
|
614
|
+
return { op: "list_my_feedback", input: {} };
|
|
615
|
+
case "request":
|
|
616
|
+
if (!rest.join(" ").trim()) throw new Error('say what you want: glossa feedback request "Open a GitHub PR on pull"');
|
|
617
|
+
return { op: "request_feature", input: compact({ title: rest.join(" ").trim(), details: str(flags.body ?? flags.details), force: flags.force ? true : undefined }) };
|
|
618
|
+
case "report": {
|
|
619
|
+
// `report --last "title"` parses the title as --last's value.
|
|
620
|
+
const title = rest.join(" ").trim() || (typeof flags.last === "string" ? flags.last : "");
|
|
621
|
+
return { op: "report_issue", input: reportInput(title, flags, last) };
|
|
622
|
+
}
|
|
623
|
+
case "upvote":
|
|
624
|
+
case "vote":
|
|
625
|
+
return { op: flags.remove ? "remove_feedback_vote" : "upvote_feedback", input: { id: needId() } };
|
|
626
|
+
case "unvote":
|
|
627
|
+
return { op: "remove_feedback_vote", input: { id: needId() } };
|
|
628
|
+
case "comment": {
|
|
629
|
+
const body = rest.slice(1).join(" ").trim() || str(flags.body);
|
|
630
|
+
if (!body) throw new Error('what do you want to add? glossa feedback comment <id> "Same for us…"');
|
|
631
|
+
return { op: "comment_on_feedback", input: { id: needId(), body } };
|
|
632
|
+
}
|
|
633
|
+
default:
|
|
634
|
+
throw new Error(`unknown feedback command "${sub}".\n${FEEDBACK_USAGE}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function itemLine(item) {
|
|
639
|
+
const flags = [item.kind, item.status.replace(/_/g, " "), item.mine ? "yours" : null, item.voted ? "voted" : null].filter(Boolean).join(" · ");
|
|
640
|
+
return `${bold(String(item.votes).padStart(4))} ${dim(`#${item.id}`.padEnd(6))} ${item.title} ${dim(flags)}`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function feedback(positional, flags) {
|
|
644
|
+
let request;
|
|
645
|
+
try {
|
|
646
|
+
request = feedbackCall(positional, flags, lastFailure());
|
|
647
|
+
} catch (error) {
|
|
648
|
+
fail(error.message);
|
|
649
|
+
}
|
|
650
|
+
const result = await call(request.op, request.input, { anonymous: request.anonymous });
|
|
651
|
+
if (flags.json || !isTTY) return print(result, flags);
|
|
652
|
+
switch (request.op) {
|
|
653
|
+
case "list_feedback":
|
|
654
|
+
console.log(bold(`${result.total} items`));
|
|
655
|
+
for (const item of result.items) console.log(itemLine(item));
|
|
656
|
+
if (result.next_cursor) console.log(dim(`… more: --offset ${result.next_cursor}`));
|
|
657
|
+
return console.log(dim("\nUpvote: glossa feedback upvote <id> · new: glossa feedback request \"…\""));
|
|
658
|
+
case "get_feedback":
|
|
659
|
+
console.log(itemLine(result));
|
|
660
|
+
if (result.body) console.log(`\n${result.body}`);
|
|
661
|
+
for (const event of result.timeline.filter((e) => e.kind === "status")) console.log(dim(` ${event.occurred_at.slice(0, 10)} ${event.from ?? ""} → ${event.to}${event.note ? `: ${event.note}` : ""}`));
|
|
662
|
+
for (const c of result.comment_list) console.log(`\n${c.official ? green(c.author) : bold(c.author)} ${dim(c.created_at.slice(0, 10))}\n${c.body}`);
|
|
663
|
+
return console.log(dim(`\n${result.url}`));
|
|
664
|
+
case "list_my_feedback":
|
|
665
|
+
console.log(bold(`Filed by your workspace (${result.items.length})`));
|
|
666
|
+
for (const item of result.items) console.log(itemLine(item));
|
|
667
|
+
console.log(bold(`\nFollowing (${result.subscriptions.length})`));
|
|
668
|
+
for (const item of result.subscriptions) console.log(`${itemLine(item)}${item.muted ? dim(" · muted") : ""}`);
|
|
669
|
+
if (result.review) console.log(bold(`\nYour review: ${"★".repeat(result.review.rating)}`) + dim(` ${result.review.status}`));
|
|
670
|
+
return console.log(dim(`\nFeedback email: ${result.email.subscribed ? "on" : "off"}${result.email.address ? ` (${result.email.address})` : ""}`));
|
|
671
|
+
case "request_feature":
|
|
672
|
+
case "report_issue":
|
|
673
|
+
if (!result.filed && !result.deduplicated) {
|
|
674
|
+
console.log(`${bold("Not filed:")} ${result.note}`);
|
|
675
|
+
for (const s of result.similar) console.log(` ${dim(`#${s.id}`)} ${s.title} ${dim(`${s.votes} votes · ${s.status}`)}`);
|
|
676
|
+
return console.log(dim("\nglossa feedback upvote <id>, or add --force to file anyway."));
|
|
677
|
+
}
|
|
678
|
+
return console.log(`${green("✓")} ${result.note}\n${dim(result.url)}`);
|
|
679
|
+
default:
|
|
680
|
+
return console.log(`${green("✓")} ${result.note ?? itemLine(result.item ?? result)}`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async function review(flags) {
|
|
685
|
+
if (flags.rating === undefined) {
|
|
686
|
+
const mine = await call("list_my_feedback");
|
|
687
|
+
if (flags.json || !isTTY) return print(mine.review, flags);
|
|
688
|
+
if (!mine.review) return console.log(`No review yet.\n${dim(FEEDBACK_USAGE.split("\n").slice(-2)[0])}`);
|
|
689
|
+
return console.log(`${"★".repeat(mine.review.rating)} ${bold(mine.review.title || "Your review")} ${dim(mine.review.status)}\n${mine.review.body}`);
|
|
690
|
+
}
|
|
691
|
+
let input;
|
|
692
|
+
try {
|
|
693
|
+
input = reviewInput(flags);
|
|
694
|
+
} catch (error) {
|
|
695
|
+
fail(error.message);
|
|
696
|
+
}
|
|
697
|
+
const result = await call("submit_review", input);
|
|
698
|
+
if (flags.json || !isTTY) return print(result, flags);
|
|
699
|
+
console.log(`${green("✓")} ${result.note}`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function reviews(flags) {
|
|
703
|
+
const result = await call("list_reviews", compact({ sort: str(flags.sort), rating: flags.rating ? Number(flags.rating) : undefined, limit: flags.limit ? Number(flags.limit) : undefined }), { anonymous: true });
|
|
704
|
+
if (flags.json || !isTTY) return print(result, flags);
|
|
705
|
+
const { average, count } = result.aggregate;
|
|
706
|
+
console.log(bold(`${average ?? "–"} / 5 from ${count} reviews`));
|
|
707
|
+
for (const r of result.reviews) {
|
|
708
|
+
console.log(`\n${"★".repeat(r.rating)}${"☆".repeat(5 - r.rating)} ${bold(r.title || "")} ${dim(`${r.author}${r.company ? `, ${r.company}` : ""} · ${r.created_at.slice(0, 10)}`)}`);
|
|
709
|
+
if (r.body) console.log(r.body);
|
|
710
|
+
if (r.response) console.log(dim(` ↳ ${r.response.author}: ${r.response.body}`));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
524
714
|
async function help() {
|
|
525
715
|
console.log(`${bold("glossa")} ${VERSION} — localization your AI agents can run
|
|
526
716
|
|
|
@@ -535,6 +725,7 @@ ${bold("Usage")}
|
|
|
535
725
|
their keys and file paths, for an agent
|
|
536
726
|
glossa fill <file.json|-> write translations: [{"unit","locale","text"}]
|
|
537
727
|
[--status reviewed|needs_review] [--overwrite missing|machine|all]
|
|
728
|
+
glossa cancel <job-id> cancel queued or running work; keeps completed translations
|
|
538
729
|
glossa tasks [--kind k] [--limit n] the review queue
|
|
539
730
|
glossa mcp stdio MCP bridge to the hosted server
|
|
540
731
|
glossa pricing [--json] plans, prices and limits (no key needed)
|
|
@@ -545,6 +736,12 @@ ${bold("Usage")}
|
|
|
545
736
|
Stripe Checkout link for a human, also before
|
|
546
737
|
signing in (\`glossa login\` attaches it after);
|
|
547
738
|
--confirm charges the saved card (once approved)
|
|
739
|
+
glossa feedback list|show|mine|request|report|upvote|comment
|
|
740
|
+
feature requests and issue reports, upvotes,
|
|
741
|
+
comments; report --last attaches the last
|
|
742
|
+
failed call (glossa feedback --help)
|
|
743
|
+
glossa review --rating 1-5 [--body …] post or edit your workspace's review
|
|
744
|
+
glossa reviews read reviews (no key needed)
|
|
548
745
|
glossa <op> [--field value…] [--json '{…}' | @file | -] run any op (glossa ops lists them)
|
|
549
746
|
glossa ops list every op
|
|
550
747
|
|
|
@@ -579,6 +776,8 @@ async function main() {
|
|
|
579
776
|
return missing(flags);
|
|
580
777
|
case "fill":
|
|
581
778
|
return fill(positional, flags);
|
|
779
|
+
case "cancel":
|
|
780
|
+
return print(await call("cancel_job", cancelJobInput(positional, flags)), flags);
|
|
582
781
|
case "tasks":
|
|
583
782
|
return tasks(flags);
|
|
584
783
|
case "mcp":
|
|
@@ -592,6 +791,13 @@ async function main() {
|
|
|
592
791
|
case "subscribe":
|
|
593
792
|
case "upgrade":
|
|
594
793
|
return subscribe(positional, flags);
|
|
794
|
+
case "feedback":
|
|
795
|
+
if (flags.help) return console.log(FEEDBACK_USAGE);
|
|
796
|
+
return feedback(positional, flags);
|
|
797
|
+
case "review":
|
|
798
|
+
return review(flags);
|
|
799
|
+
case "reviews":
|
|
800
|
+
return reviews(flags);
|
|
595
801
|
case "ops": {
|
|
596
802
|
const ops = await manifest();
|
|
597
803
|
if (flags.json || !isTTY) return print(ops, flags);
|
package/bin/glossa.test.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { coerceFlags, deviceLogin, fileBody, fileText, parseArgs, readJsonArg, fillBatches, pricingLines, subscribeInput } from "./glossa.mjs";
|
|
2
|
+
import { cancelJobInput, coerceFlags, deviceLogin, feedbackCall, fileBody, fileText, parseArgs, readJsonArg, fillBatches, pricingLines, reportInput, reviewInput, subscribeInput } from "./glossa.mjs";
|
|
3
3
|
import { pricingDocument } from "../../../lib/billing/pricing";
|
|
4
4
|
|
|
5
5
|
describe("cli args", () => {
|
|
@@ -110,3 +110,52 @@ describe("pricing", () => {
|
|
|
110
110
|
expect(lines[3].text).toMatch(/^custom/);
|
|
111
111
|
});
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
describe("feedback", () => {
|
|
115
|
+
it("maps every feedback subcommand to its op", () => {
|
|
116
|
+
expect(feedbackCall([], {})).toEqual({ op: "list_feedback", anonymous: true, input: {} });
|
|
117
|
+
expect(feedbackCall(["list"], { kind: "issue", sort: "new", limit: "5" })).toEqual({ op: "list_feedback", anonymous: true, input: { kind: "issue", sort: "new", limit: 5 } });
|
|
118
|
+
expect(feedbackCall(["show", "42"], {})).toEqual({ op: "get_feedback", anonymous: true, input: { id: 42 } });
|
|
119
|
+
expect(feedbackCall(["mine"], {})).toEqual({ op: "list_my_feedback", input: {} });
|
|
120
|
+
expect(feedbackCall(["request", "Open", "a", "PR"], { body: "for CI", force: true })).toEqual({ op: "request_feature", input: { title: "Open a PR", details: "for CI", force: true } });
|
|
121
|
+
expect(feedbackCall(["upvote", "7"], {})).toEqual({ op: "upvote_feedback", input: { id: 7 } });
|
|
122
|
+
expect(feedbackCall(["upvote", "7"], { remove: true })).toEqual({ op: "remove_feedback_vote", input: { id: 7 } });
|
|
123
|
+
expect(feedbackCall(["comment", "7", "Same", "here"], {})).toEqual({ op: "comment_on_feedback", input: { id: 7, body: "Same here" } });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("rejects missing ids, titles and unknown subcommands", () => {
|
|
127
|
+
expect(() => feedbackCall(["show"], {})).toThrow(/which item/);
|
|
128
|
+
expect(() => feedbackCall(["request"], {})).toThrow(/say what you want/);
|
|
129
|
+
expect(() => feedbackCall(["comment", "7"], {})).toThrow(/what do you want to add/);
|
|
130
|
+
expect(() => feedbackCall(["nope"], {})).toThrow(/unknown feedback command/);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("report --last attaches the last failed call; flags add context", () => {
|
|
134
|
+
const last = { request_id: "req_9", op: "push_file", status: 500, error_code: "internal_error", error: "Something went wrong", at: "2026-10-17T00:00:00Z" };
|
|
135
|
+
const input = reportInput("push_file 500s", { last: true, project: "ios", locale: "de" }, last);
|
|
136
|
+
expect(input).toMatchObject({ title: "push_file 500s", context: { request_id: "req_9", op: "push_file", status: 500, error_code: "internal_error", error: "Something went wrong", project: "ios", locale: "de" } });
|
|
137
|
+
expect(input.context.client).toMatch(/^glossa-cli\//);
|
|
138
|
+
expect(reportInput("Docs typo", { "request-id": "req_1" }).context).toMatchObject({ request_id: "req_1" });
|
|
139
|
+
expect(() => reportInput("x y z", { last: true }, null)).toThrow(/no failed call/);
|
|
140
|
+
// `report --last "title"` parses the title as --last's value.
|
|
141
|
+
expect(feedbackCall(["report"], { last: "push_file 500s" }, last).input).toMatchObject({ title: "push_file 500s", context: { request_id: "req_9" } });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("builds a review from flags", () => {
|
|
145
|
+
expect(reviewInput({ rating: "5", body: "Great", "use-case": "iOS app", consent: true, name: "Ann" })).toEqual({ rating: 5, body: "Great", use_case: "iOS app", display_name: "Ann", marketing_consent: true });
|
|
146
|
+
expect(() => reviewInput({ rating: "6" })).toThrow(/rate 1 to 5/);
|
|
147
|
+
expect(() => reviewInput({})).toThrow(/rate 1 to 5/);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
describe("cancel", () => {
|
|
153
|
+
const job = "bcae2e7c-cbc6-466b-8d97-c21ac165d099";
|
|
154
|
+
it("accepts one job id as a positional or flag", () => {
|
|
155
|
+
expect(cancelJobInput([job], {})).toEqual({ job });
|
|
156
|
+
expect(cancelJobInput([], { job })).toEqual({ job });
|
|
157
|
+
});
|
|
158
|
+
it("rejects missing, malformed or extra ids", () => {
|
|
159
|
+
for (const ids of [[], ["invalid"], [job, job]]) expect(() => cancelJobInput(ids, {})).toThrow(/Usage/);
|
|
160
|
+
});
|
|
161
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meterapp/glossa",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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" },
|