@fire-spark/cli 0.0.0-stage → 0.13.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.
@@ -0,0 +1,179 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import type { Copy, Locale } from "../../../core/i18n";
6
+ import { translate } from "../../../core/i18n";
7
+ import { UsageError, readString } from "../../../core/options";
8
+ import type { OptionValues } from "../../../core/options";
9
+
10
+ export type Verification = "deterministic" | "mixed";
11
+
12
+ export interface CatalogCheck {
13
+ id: string;
14
+ description: string;
15
+ acceptance: string;
16
+ channels: string[];
17
+ platforms: string[];
18
+ fixable: boolean;
19
+ required: boolean;
20
+ area: string;
21
+ notes: string;
22
+ verification: Verification;
23
+ howTo: string;
24
+ }
25
+
26
+ export interface CheckFilter {
27
+ channel?: string;
28
+ platform?: string;
29
+ requiredOnly: boolean;
30
+ area?: string;
31
+ verification?: Verification;
32
+ }
33
+
34
+ const file = join(dirname(fileURLToPath(import.meta.url)), "catalog.json");
35
+ export const CATALOG = JSON.parse(readFileSync(file, "utf8")) as CatalogCheck[];
36
+
37
+ const byId = new Map(CATALOG.map(check => [check.id, check]));
38
+
39
+ export const checkById = (id: string) => {
40
+ return byId.get(id.toUpperCase());
41
+ };
42
+
43
+ const fold = (value: string) => {
44
+ return value.trim().toLowerCase().normalize("NFD").replace(/\p{M}/gu, "");
45
+ };
46
+
47
+ const label = (es: string, en: string, pt: string): Copy => {
48
+ return { "es-EC": es, "en-US": en, "pt-BR": pt };
49
+ };
50
+
51
+ const cacheEs = "Caché y admin remoto";
52
+ const cacheEn = "Cache and remote admin";
53
+ const cachePt = "Cache e admin remoto";
54
+ const CACHE_LABEL = [cacheEs, cacheEn, cachePt] as const;
55
+ const posEs = "POS caja y offline";
56
+ const posEn = "POS register and offline";
57
+ const posPt = "POS caixa e offline";
58
+ const POS_LABEL = [posEs, posEn, posPt] as const;
59
+ const tablesEs = "Mesas y reservas";
60
+ const tablesEn = "Tables and reservations";
61
+ const tablesPt = "Mesas e reservas";
62
+ const TABLES_LABEL = [tablesEs, tablesEn, tablesPt] as const;
63
+
64
+ export const AREA_LABELS: Record<string, Copy> = {
65
+ payments: label("Pagos", "Payments", "Pagamentos"),
66
+ orders: label("Órdenes", "Orders", "Pedidos"),
67
+ menu: label("Menú", "Menu", "Cardápio"),
68
+ overrides: label("Overrides", "Overrides", "Overrides"),
69
+ availability: label("Disponibilidad", "Availability", "Disponibilidade"),
70
+ cache: label(...CACHE_LABEL),
71
+ login: label("Login", "Login", "Login"),
72
+ cms: label("CMS", "CMS", "CMS"),
73
+ images: label("Imágenes", "Images", "Imagens"),
74
+ hardware: label("Hardware", "Hardware", "Hardware"),
75
+ printing: label("Impresión", "Printing", "Impressão"),
76
+ pos: label(...POS_LABEL),
77
+ chat: label("Chat", "Chat", "Chat"),
78
+ marketing: label("Marketing", "Marketing", "Marketing"),
79
+ admin: label("Admin", "Admin", "Admin"),
80
+ sdk: label("SDK y config", "SDK and config", "SDK e config"),
81
+ checkout: label("Checkout", "Checkout", "Checkout"),
82
+ tables: label(...TABLES_LABEL)
83
+ };
84
+
85
+ const AREA_ALIAS = new Map<string, string>();
86
+ for (const [slug, copy] of Object.entries(AREA_LABELS)) {
87
+ AREA_ALIAS.set(fold(slug), slug);
88
+ for (const text of Object.values(copy)) AREA_ALIAS.set(fold(text), slug);
89
+ }
90
+
91
+ const channelA = ["kiosk", "pos", "web", "app", "chat"] as const;
92
+ const channelB = ["call-center", "server", "dashboard"] as const;
93
+ const CHANNELS = new Set([...channelA, ...channelB]);
94
+
95
+ const PLATFORMS = new Set(["web", "android", "ios", "dashboard"]);
96
+
97
+ const ver1 = ["deterministic", "deterministic"] as const;
98
+ const ver2 = ["deterministica", "deterministic"] as const;
99
+ const ver3 = ["mixed", "mixed"] as const;
100
+ const ver4 = ["mezcla", "mixed"] as const;
101
+ const ver5 = ["mista", "mixed"] as const;
102
+ const verPairs = [ver1, ver2, ver3, ver4, ver5] as const;
103
+ const VERIFICATION = new Map<string, Verification>(verPairs);
104
+
105
+ const BAD_FILTER = {
106
+ "es-EC": "Valor desconocido para --{flag}: {value}",
107
+ "en-US": "Unknown value for --{flag}: {value}",
108
+ "pt-BR": "Valor desconhecido para --{flag}: {value}"
109
+ } satisfies Copy;
110
+
111
+ const badFilter = (locale: Locale, flag: string, value: string) => {
112
+ return new UsageError(translate(locale, BAD_FILTER, { flag, value }));
113
+ };
114
+
115
+ const token = (
116
+ locale: Locale,
117
+ value: string | undefined,
118
+ allowed: Map<string, string> | Set<string>,
119
+ flag: string
120
+ ) => {
121
+ if (!value) return undefined;
122
+ const key = fold(value);
123
+ if (allowed instanceof Map) {
124
+ const found = allowed.get(key);
125
+ if (!found) throw badFilter(locale, flag, value);
126
+ return found;
127
+ }
128
+ if (!allowed.has(key)) throw badFilter(locale, flag, value);
129
+ return key;
130
+ };
131
+
132
+ export const parseCheckFilter = (
133
+ values: OptionValues,
134
+ locale: Locale
135
+ ): CheckFilter => {
136
+ const channelRaw = readString(values, "channel");
137
+ const channel = token(locale, channelRaw, CHANNELS, "channel");
138
+ const platformRaw = readString(values, "platform");
139
+ const platform = token(locale, platformRaw, PLATFORMS, "platform");
140
+ const area = token(locale, readString(values, "area"), AREA_ALIAS, "area");
141
+ const raw = readString(values, "verification");
142
+ const verification = token(locale, raw, VERIFICATION, "verification");
143
+ const kind = verification as Verification | undefined;
144
+ const requiredOnly = values.required === true;
145
+ const b1 = { channel, platform, requiredOnly, area };
146
+ return { ...b1, verification: kind };
147
+ };
148
+
149
+ export const filterChecks = (checks: CatalogCheck[], filter: CheckFilter) => {
150
+ return checks.filter(check => {
151
+ if (filter.channel && !check.channels.includes(filter.channel)) {
152
+ return false;
153
+ }
154
+ if (filter.platform && !check.platforms.includes(filter.platform)) {
155
+ return false;
156
+ }
157
+ if (filter.requiredOnly && !check.required) return false;
158
+ if (filter.area && check.area !== filter.area) return false;
159
+ if (filter.verification && check.verification !== filter.verification) {
160
+ return false;
161
+ }
162
+ return true;
163
+ });
164
+ };
165
+
166
+ const detEs = "Determinística";
167
+ const detEn = "Deterministic";
168
+ const DETERMINISTIC = label(detEs, detEn, detEs);
169
+ const MIXED = label("Mezcla", "Mixed", "Mista");
170
+
171
+ export const areaLabel = (area: string, locale: Locale) => {
172
+ const copy = AREA_LABELS[area];
173
+ return copy ? translate(locale, copy) : area;
174
+ };
175
+
176
+ export const verificationLabel = (kind: Verification, locale: Locale) => {
177
+ const copy = kind === "mixed" ? MIXED : DETERMINISTIC;
178
+ return translate(locale, copy);
179
+ };
@@ -0,0 +1,246 @@
1
+ import { spawn } from "node:child_process";
2
+ import { stdin, stdout } from "node:process";
3
+ import { createInterface } from "node:readline/promises";
4
+
5
+ import type { Command, CommandContext } from "../../../core/command.types";
6
+ import { renderCommandHelp } from "../../../core/help";
7
+ import { translate } from "../../../core/i18n";
8
+ import { UsageError, readString } from "../../../core/options";
9
+ import { areaLabel, checkById, filterChecks } from "./catalog";
10
+ import { CATALOG, parseCheckFilter, verificationLabel } from "./catalog";
11
+ import type { CatalogCheck } from "./catalog";
12
+ import { applyTargets, createConfig, fileExists, preflightGap } from "./config";
13
+ import { promptAll, readConfig, readTargets } from "./config";
14
+ import { resolveConfigPath, writeConfig } from "./config";
15
+ import { COPY, DETAILS, DOCS_PAGE, SUMMARY, USAGE } from "./copy";
16
+ import { CLIENT_OPTIONS } from "./options";
17
+ import { BINDINGS, printRuns, runOne } from "./runner";
18
+ import type { CheckRun } from "./runner";
19
+ import type { YamlChannel } from "./yaml";
20
+
21
+ const NAMES = new Set(["run", "init", "list", "show", "docs"]);
22
+
23
+ const formatOf = (context: CommandContext) => {
24
+ const value = readString(context.options, "format") ?? "text";
25
+ if (value === "text" || value === "json") return value;
26
+ throw new UsageError(translate(context.locale, COPY.badFormat));
27
+ };
28
+
29
+ const wrote = (context: CommandContext, path: string) => {
30
+ const line = translate(context.locale, COPY.wrote, { path });
31
+ const json = readString(context.options, "format") === "json";
32
+ if (json) context.output.error(line);
33
+ else context.output.line(line);
34
+ };
35
+
36
+ const askerOf = (context: CommandContext) => {
37
+ const interactive = context.isTty === true;
38
+ const ask = async (question: string) => {
39
+ if (!interactive) return "";
40
+ if (context.ask) return context.ask(question);
41
+ const rl = createInterface({ input: stdin, output: stdout });
42
+ try {
43
+ return await rl.question(question);
44
+ } finally {
45
+ rl.close();
46
+ }
47
+ };
48
+ return { ask, interactive };
49
+ };
50
+
51
+ const openPage = (url: string) => {
52
+ const command = process.platform === "darwin" ? "open" : "xdg-open";
53
+ try {
54
+ const child = spawn(command, [url], { stdio: "ignore", detached: true });
55
+ child.on("error", () => undefined);
56
+ child.unref();
57
+ } catch {
58
+ return;
59
+ }
60
+ };
61
+
62
+ const docs = (context: CommandContext) => {
63
+ const url = translate(context.locale, DOCS_PAGE);
64
+ context.output.line(translate(context.locale, COPY.docsLine, { url }));
65
+ if (context.isTty === true) openPage(url);
66
+ return 0;
67
+ };
68
+
69
+ const loadNew = async (context: CommandContext, path: string) => {
70
+ const flags = readTargets(context.options, context.locale);
71
+ const asker = askerOf(context);
72
+ const args = [context.cwd, flags, context.locale, asker] as const;
73
+ const file = await createConfig(...args);
74
+ await writeConfig(path, file);
75
+ wrote(context, path);
76
+ return file;
77
+ };
78
+
79
+ const loadExisting = async (context: CommandContext, path: string) => {
80
+ const flags = readTargets(context.options, context.locale);
81
+ const file = await readConfig(path, context.locale);
82
+ const channels = applyTargets(file.channels, flags, context.locale);
83
+ return { channels };
84
+ };
85
+
86
+ const selectedChecks = (context: CommandContext, ids: string[]) => {
87
+ const filter = parseCheckFilter(context.options, context.locale);
88
+ if (ids.length === 0) return filterChecks(CATALOG, filter);
89
+ const checks: CatalogCheck[] = [];
90
+ for (const id of ids) {
91
+ const check = checkById(id);
92
+ if (!check) {
93
+ const vars = { id };
94
+ const message = translate(context.locale, COPY.unknownCheck, vars);
95
+ throw new UsageError(message);
96
+ }
97
+ checks.push(check);
98
+ }
99
+ return filterChecks(checks, filter);
100
+ };
101
+
102
+ const showCheck = (context: CommandContext, check: CatalogCheck) => {
103
+ const { locale, output } = context;
104
+ const binding = BINDINGS[check.id];
105
+ const kind = binding ? binding.kind : "unbound";
106
+ const yes = translate(locale, COPY.yes);
107
+ const no = translate(locale, COPY.no);
108
+ const lines = [
109
+ check.id,
110
+ check.description,
111
+ check.acceptance,
112
+ `${check.channels.join(", ")}`,
113
+ `${check.platforms.join(", ")}`,
114
+ areaLabel(check.area, locale),
115
+ verificationLabel(check.verification, locale),
116
+ `${check.required ? yes : no}`,
117
+ `${check.fixable ? yes : no}`,
118
+ check.notes,
119
+ check.howTo,
120
+ kind
121
+ ];
122
+ for (const line of lines) output.line(line);
123
+ };
124
+
125
+ const listChecks = (context: CommandContext, checks: CatalogCheck[]) => {
126
+ const format = formatOf(context);
127
+ if (format === "json") {
128
+ const rows = checks.map(check => {
129
+ const b1 = { id: check.id, channels: check.channels };
130
+ const b2 = { platforms: check.platforms, required: check.required };
131
+ const b3 = { fixable: check.fixable, area: check.area };
132
+ const b4 = { verification: check.verification };
133
+ return { ...b1, ...b2, ...b3, ...b4 };
134
+ });
135
+ context.output.line(JSON.stringify(rows, null, 2));
136
+ return 0;
137
+ }
138
+ for (const check of checks) {
139
+ const area = areaLabel(check.area, context.locale);
140
+ const kind = verificationLabel(check.verification, context.locale);
141
+ context.output.line(`${check.id} ${area} ${kind}`);
142
+ }
143
+ return 0;
144
+ };
145
+
146
+ const runChecks = async (
147
+ context: CommandContext,
148
+ checks: CatalogCheck[],
149
+ channels: YamlChannel[],
150
+ ids: string[]
151
+ ) => {
152
+ const format = formatOf(context);
153
+ const fix = context.options.fix === true;
154
+ const strict = context.options.strict === true;
155
+ const named = new Set(ids.map(id => id.toUpperCase()));
156
+ const channelId = readString(context.options, "channel-id");
157
+ const b1 = { locale: context.locale, cwd: context.cwd, env: context.env };
158
+ const b2 = { channels, channelId, named, redactor: context.redactor };
159
+ const ctx = { ...b1, ...b2 };
160
+ const runs: CheckRun[] = [];
161
+ for (const check of checks) {
162
+ const outcome = await runOne(check, ctx, fix);
163
+ runs.push({ id: check.id, outcome });
164
+ }
165
+ return printRuns(context.output, context.locale, runs, format, strict);
166
+ };
167
+
168
+ const guard = (
169
+ context: CommandContext,
170
+ channels: Awaited<ReturnType<typeof loadExisting>>
171
+ ) => {
172
+ const gap = preflightGap(channels.channels, context.env);
173
+ if (!gap) return false;
174
+ const vars = { channel: gap.channel, field: gap.field };
175
+ const message = translate(context.locale, COPY.preflight, vars);
176
+ context.output.error(message);
177
+ return true;
178
+ };
179
+
180
+ export const runClientDoctor = async (
181
+ context: CommandContext,
182
+ command: Command
183
+ ) => {
184
+ const [name, ...rest] = context.positionals;
185
+ if (!name) {
186
+ context.output.line(renderCommandHelp(command, context.locale));
187
+ return 0;
188
+ }
189
+ if (!NAMES.has(name)) {
190
+ const vars = { name };
191
+ throw new UsageError(translate(context.locale, COPY.unknownCommand, vars));
192
+ }
193
+ if (rest.length > 0 && name !== "run" && name !== "show") {
194
+ const unknown = rest[0] ?? name;
195
+ const vars = { name: unknown };
196
+ const message = translate(context.locale, COPY.unknownCommand, vars);
197
+ throw new UsageError(message);
198
+ }
199
+ if (name === "docs") return docs(context);
200
+ const flag = readString(context.options, "config");
201
+ const path = await resolveConfigPath(context.cwd, flag, context.locale);
202
+ const exists = await fileExists(path);
203
+ if (name === "init") {
204
+ if (!exists) {
205
+ await loadNew(context, path);
206
+ return 0;
207
+ }
208
+ const flags = readTargets(context.options, context.locale);
209
+ const current = await readConfig(path, context.locale);
210
+ const merged = applyTargets(current.channels, flags, context.locale);
211
+ const asker = askerOf(context);
212
+ const args = [merged, flags, context.locale, asker, false] as const;
213
+ const channels = await promptAll(...args);
214
+ await writeConfig(path, { channels });
215
+ wrote(context, path);
216
+ return 0;
217
+ }
218
+ const read = exists ? loadExisting : loadNew;
219
+ const loaded = await read(context, path);
220
+ if (name === "list") return listChecks(context, selectedChecks(context, []));
221
+ if (name === "show") {
222
+ if (rest.length !== 1) {
223
+ throw new UsageError(translate(context.locale, COPY.showOne));
224
+ }
225
+ const checks = selectedChecks(context, rest);
226
+ const check = checks[0];
227
+ if (!check) throw new UsageError(translate(context.locale, COPY.none));
228
+ showCheck(context, check);
229
+ return 0;
230
+ }
231
+ if (guard(context, loaded)) return 1;
232
+ const checks = selectedChecks(context, rest);
233
+ if (checks.length === 0) {
234
+ throw new UsageError(translate(context.locale, COPY.none));
235
+ }
236
+ return runChecks(context, checks, loaded.channels, rest);
237
+ };
238
+
239
+ export const clientDoctorCommand: Command = {
240
+ name: "doctor",
241
+ usage: USAGE,
242
+ summary: SUMMARY,
243
+ details: DETAILS,
244
+ options: CLIENT_OPTIONS,
245
+ run: context => runClientDoctor(context, clientDoctorCommand)
246
+ };