@fire-spark/cli 0.0.0-stage → 0.13.3

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.
Files changed (39) hide show
  1. package/bin/firespark.mjs +14 -0
  2. package/package.json +39 -3
  3. package/src/commands/doctor/client/admin.flow.ts +553 -0
  4. package/src/commands/doctor/client/catalog.json +1586 -0
  5. package/src/commands/doctor/client/catalog.ts +179 -0
  6. package/src/commands/doctor/client/checkout.flow.ts +400 -0
  7. package/src/commands/doctor/client/command.ts +246 -0
  8. package/src/commands/doctor/client/config.ts +346 -0
  9. package/src/commands/doctor/client/content.flow.ts +450 -0
  10. package/src/commands/doctor/client/copy.ts +248 -0
  11. package/src/commands/doctor/client/flow.ts +182 -0
  12. package/src/commands/doctor/client/log.flow.ts +410 -0
  13. package/src/commands/doctor/client/manifests.ts +161 -0
  14. package/src/commands/doctor/client/men.flow.ts +412 -0
  15. package/src/commands/doctor/client/ops.flow.ts +312 -0
  16. package/src/commands/doctor/client/options.ts +201 -0
  17. package/src/commands/doctor/client/ord.flow.ts +445 -0
  18. package/src/commands/doctor/client/order.ts +261 -0
  19. package/src/commands/doctor/client/pay.flow.ts +772 -0
  20. package/src/commands/doctor/client/repo.scan.ts +142 -0
  21. package/src/commands/doctor/client/runner.ts +182 -0
  22. package/src/commands/doctor/client/sdk.flow.ts +630 -0
  23. package/src/commands/doctor/client/store.flow.ts +577 -0
  24. package/src/commands/doctor/client/yaml.ts +179 -0
  25. package/src/commands/doctor/live/http.ts +81 -0
  26. package/src/commands/doctor/live/live.session.ts +250 -0
  27. package/src/commands/doctor/live/order.ts +95 -0
  28. package/src/commands/doctor/live/realtime.ts +161 -0
  29. package/src/commands/doctor/live/result.ts +76 -0
  30. package/src/commands/index.ts +8 -0
  31. package/src/core/command.types.ts +25 -0
  32. package/src/core/help.ts +85 -0
  33. package/src/core/i18n.ts +49 -0
  34. package/src/core/options.ts +131 -0
  35. package/src/core/output.ts +47 -0
  36. package/src/core/redact.ts +71 -0
  37. package/src/index.ts +4 -0
  38. package/src/main.ts +113 -0
  39. package/README.md +0 -3
@@ -0,0 +1,248 @@
1
+ import type { Copy } from "../../../core/i18n";
2
+
3
+ const text = (es: string, en: string, pt: string): Copy => {
4
+ return { "es-EC": es, "en-US": en, "pt-BR": pt };
5
+ };
6
+
7
+ export const DOCS_PAGE: Copy = {
8
+ "es-EC": "https://firespark.cloud/docs/es/guides/doctor",
9
+ "en-US": "https://firespark.cloud/docs/guides/doctor",
10
+ "pt-BR": "https://firespark.cloud/docs/guides/doctor"
11
+ };
12
+
13
+ export const SUMMARY = text(
14
+ "Revisa el FireSpark ya desplegado de un merchant.",
15
+ "Checks a merchant's already deployed FireSpark.",
16
+ "Verifica o FireSpark já implantado de um merchant."
17
+ );
18
+
19
+ const usage = "firespark doctor <run|list|show|init|docs> [ID ...] [options]";
20
+ export const USAGE = usage;
21
+
22
+ const detailsEs = [
23
+ "Comandos: run [ID ...], list, show <ID>, init [--config <archivo>], docs.",
24
+ "run ejecuta los checks contra el FireSpark ya desplegado. Sin --fix solo informa.",
25
+ "Si falta el YAML, se crea aquí con init y se pregunta solo lo que falte.",
26
+ "Un canal que nunca se desplegó puede quedar solo con su identidad.",
27
+ "ORD-01 crea una orden de prueba solo si lo nombras y el pago es cash o sandbox. Antes de place se une al canal realtime y espera a que el checkout se libere.",
28
+ `La documentación está aquí: ${DOCS_PAGE["es-EC"]}`
29
+ ].join("\n");
30
+
31
+ const detailsEn = [
32
+ "Commands: run [ID ...], list, show <ID>, init [--config <file>], docs.",
33
+ "run checks an already deployed FireSpark. Without --fix it only reports.",
34
+ "If the YAML is missing, init creates it here and asks only for what is missing.",
35
+ "A channel that was never deployed can keep identity only.",
36
+ "ORD-01 places a test order only when you name it and payment is cash or sandbox. It joins the realtime channel before place and waits until checkout is released.",
37
+ `Documentation: ${DOCS_PAGE["en-US"]}`
38
+ ].join("\n");
39
+
40
+ const detailsPt = [
41
+ "Comandos: run [ID ...], list, show <ID>, init [--config <arquivo>], docs.",
42
+ "run verifica o FireSpark já implantado. Sem --fix, só informa.",
43
+ "Se o YAML não existe, o init cria o arquivo aqui e pergunta só o que falta.",
44
+ "Um canal que nunca foi implantado pode ficar só com a identidade.",
45
+ "ORD-01 cria um pedido de teste só se você o nomear e o pagamento for cash ou sandbox. Ele entra no canal realtime antes do place e espera o checkout ser liberado.",
46
+ `A documentação está aqui: ${DOCS_PAGE["pt-BR"]}`
47
+ ].join("\n");
48
+
49
+ export const DETAILS: Copy = {
50
+ "es-EC": detailsEs,
51
+ "en-US": detailsEn,
52
+ "pt-BR": detailsPt
53
+ };
54
+
55
+ export const COPY = {
56
+ unknownCommand: text(
57
+ "Comando desconocido: {name}. Usa run, list, show, init o docs.",
58
+ "Unknown command: {name}. Use run, list, show, init, or docs.",
59
+ "Comando desconhecido: {name}. Use run, list, show, init ou docs."
60
+ ),
61
+ unknownCheck: text(
62
+ "Check desconocido: {id}",
63
+ "Unknown check: {id}",
64
+ "Check desconhecido: {id}"
65
+ ),
66
+ none: text(
67
+ "Ningún check coincide con esos filtros.",
68
+ "No check matches those filters.",
69
+ "Nenhum check corresponde a esses filtros."
70
+ ),
71
+ showOne: text(
72
+ "show necesita un solo ID.",
73
+ "show needs exactly one ID.",
74
+ "show precisa de um único ID."
75
+ ),
76
+ manyConfigs: text(
77
+ "Hay varios YAML ({files}). Pasa --config con el archivo que quieres.",
78
+ "Several YAML files ({files}). Pass --config with the one you want.",
79
+ "Há vários YAML ({files}). Passe --config com o arquivo que você quer."
80
+ ),
81
+ ambiguous: text(
82
+ "Hay varios canales. Pasa --channel-id para aplicar esas flags.",
83
+ "There are several channels. Pass --channel-id to apply those flags.",
84
+ "Há vários canais. Passe --channel-id para aplicar essas flags."
85
+ ),
86
+ unknownChannel: text(
87
+ "No existe un canal con channel_id {id} en el YAML.",
88
+ "No channel with channel_id {id} exists in the YAML.",
89
+ "Não existe um canal com channel_id {id} no YAML."
90
+ ),
91
+ badPayment: text(
92
+ "--payment solo acepta cash o sandbox.",
93
+ "--payment only accepts cash or sandbox.",
94
+ "--payment só aceita cash ou sandbox."
95
+ ),
96
+ badFormat: text(
97
+ "--format solo acepta text o json.",
98
+ "--format only accepts text or json.",
99
+ "--format só aceita text ou json."
100
+ ),
101
+ badUrl: text(
102
+ "--{flag} tiene que ser una URL http o https.",
103
+ "--{flag} must be an http or https URL.",
104
+ "--{flag} tem que ser uma URL http ou https."
105
+ ),
106
+ badYaml: text(
107
+ "No pude leer {path}: {detail}",
108
+ "Could not read {path}: {detail}",
109
+ "Não consegui ler {path}: {detail}"
110
+ ),
111
+ preflight: text(
112
+ "El canal {channel} está desplegado y le falta {field}. No se ejecutó ningún check.",
113
+ "Channel {channel} is deployed and is missing {field}. No checks ran.",
114
+ "O canal {channel} está implantado e falta {field}. Nenhum check rodou."
115
+ ),
116
+ wrote: text(
117
+ "Configuración lista: {path}",
118
+ "Configuration ready: {path}",
119
+ "Configuração pronta: {path}"
120
+ ),
121
+ deployedAsk: text(
122
+ "¿El canal {channel} está desplegado? (sí/no) ",
123
+ "Is channel {channel} deployed? (yes/no) ",
124
+ "O canal {channel} está implantado? (sim/não) "
125
+ ),
126
+ answerYesNo: text(
127
+ "Responde sí o no para el canal {channel}. No adivino si está desplegado.",
128
+ "Answer yes or no for channel {channel}. I will not guess if it is deployed.",
129
+ "Responda sim ou não para o canal {channel}. Não adivinho se está implantado."
130
+ ),
131
+ askUrl: text(
132
+ "URL pública de {channel}: ",
133
+ "Public URL of {channel}: ",
134
+ "URL pública de {channel}: "
135
+ ),
136
+ askDashboard: text(
137
+ "URL del dashboard de {channel}: ",
138
+ "Dashboard URL of {channel}: ",
139
+ "URL do dashboard de {channel}: "
140
+ ),
141
+ askApi: text(
142
+ "URL de la API de {channel}: ",
143
+ "API URL of {channel}: ",
144
+ "URL da API de {channel}: "
145
+ ),
146
+ askStore: text(
147
+ "store-id de {channel}: ",
148
+ "store-id of {channel}: ",
149
+ "store-id de {channel}: "
150
+ ),
151
+ askFulfillment: text(
152
+ "fulfillment-id de {channel}: ",
153
+ "fulfillment-id of {channel}: ",
154
+ "fulfillment-id de {channel}: "
155
+ ),
156
+ askPayment: text(
157
+ "Pago de {channel} (cash o sandbox): ",
158
+ "Payment of {channel} (cash or sandbox): ",
159
+ "Pagamento de {channel} (cash ou sandbox): "
160
+ ),
161
+ askReceiver: text(
162
+ "URL del receptor de webhooks de {channel}: ",
163
+ "Webhook receiver URL of {channel}: ",
164
+ "URL do receptor de webhooks de {channel}: "
165
+ ),
166
+ askEvidence: text(
167
+ "Ruta de evidencia de {channel}: ",
168
+ "Evidence path of {channel}: ",
169
+ "Caminho de evidência de {channel}: "
170
+ ),
171
+ badAnswerPayment: text(
172
+ "El pago tiene que ser cash o sandbox.",
173
+ "Payment has to be cash or sandbox.",
174
+ "O pagamento tem que ser cash ou sandbox."
175
+ ),
176
+ docsLine: text(
177
+ "La documentación está aquí: {url}",
178
+ "Documentation: {url}",
179
+ "A documentação está aqui: {url}"
180
+ ),
181
+ orderMissing: text(
182
+ "El canal {channel} le falta {field}. No se creó ninguna orden.",
183
+ "Channel {channel} is missing {field}. No order was created.",
184
+ "O canal {channel} falta {field}. Nenhum pedido foi criado."
185
+ ),
186
+ orderNoChannel: text(
187
+ "Falta channel_id para elegir el canal. No se creó ninguna orden.",
188
+ "channel_id is missing, so no channel was chosen. No order was created.",
189
+ "Falta channel_id para escolher o canal. Nenhum pedido foi criado."
190
+ ),
191
+ orderSkipped: text(
192
+ "ORD-01 solo corre cuando lo nombras. No se creó ninguna orden.",
193
+ "ORD-01 runs only when you name it. No order was created.",
194
+ "ORD-01 só roda quando você o nomeia. Nenhum pedido foi criado."
195
+ ),
196
+ orderNoFix: text(
197
+ "No hay un arreglo automático para ORD-01. No se creó ninguna orden.",
198
+ "There is no automatic repair for ORD-01. No order was created.",
199
+ "Não há um reparo automático para ORD-01. Nenhum pedido foi criado."
200
+ ),
201
+ unbound: text(
202
+ "Este check todavía no tiene flujo en esta versión.",
203
+ "This check has no flow in this version yet.",
204
+ "Este check ainda não tem fluxo nesta versão."
205
+ ),
206
+ noManifests: text(
207
+ "Este repo no tiene apps/*/src/channel-manifest.json.",
208
+ "This repo has no apps/*/src/channel-manifest.json.",
209
+ "Este repo não tem apps/*/src/channel-manifest.json."
210
+ ),
211
+ duplicateId: text(
212
+ "channel_id {id} está en {left} y en {right}.",
213
+ "channel_id {id} is in {left} and in {right}.",
214
+ "channel_id {id} está em {left} e em {right}."
215
+ ),
216
+ noSafeFix: text(
217
+ "No hay un arreglo automático que no invente un channel_id. No cambié ningún archivo.",
218
+ "There is no automatic repair that does not invent a channel_id. No file was changed.",
219
+ "Não há um reparo automático que não invente um channel_id. Nenhum arquivo mudou."
220
+ ),
221
+ flowMissing: text(
222
+ "El canal {channel} le falta {field}.",
223
+ "Channel {channel} is missing {field}.",
224
+ "O canal {channel} falta {field}."
225
+ ),
226
+ flowNoChannel: text(
227
+ "Falta channel_id para elegir el canal.",
228
+ "channel_id is missing, so no channel was chosen.",
229
+ "Falta channel_id para escolher o canal."
230
+ ),
231
+ noSafeChange: text(
232
+ "No hay un arreglo automático que no invente datos. No cambié nada.",
233
+ "There is no automatic repair that does not invent data. Nothing was changed.",
234
+ "Não há um reparo automático que não invente dados. Nada foi alterado."
235
+ ),
236
+ namedOrder: text(
237
+ "{id} solo corre cuando lo nombras. No se creó ninguna orden.",
238
+ "{id} runs only when you name it. No order was created.",
239
+ "{id} só roda quando você o nomeia. Nenhum pedido foi criado."
240
+ ),
241
+ strictSkipped: text(
242
+ "Modo estricto: hay checks omitidos.",
243
+ "Strict mode: some checks were skipped.",
244
+ "Modo estrito: há checks omitidos."
245
+ ),
246
+ yes: text("sí", "yes", "sim"),
247
+ no: text("no", "no", "não")
248
+ } as const;
@@ -0,0 +1,182 @@
1
+ import type { Copy, Locale } from "../../../core/i18n";
2
+ import { translate } from "../../../core/i18n";
3
+ import type { LiveSession } from "../live/live.session";
4
+ import { createLiveSession } from "../live/live.session";
5
+ import type { CheckFail, CheckSkip } from "../live/result";
6
+ import { channelLabel, paymentOf } from "./config";
7
+ import { COPY } from "./copy";
8
+ import { liveConfigOf } from "./order";
9
+ import type { CheckBinding, Outcome, RunCtx } from "./runner";
10
+ import type { YamlChannel } from "./yaml";
11
+
12
+ export const textOf = (value: string | null | undefined) => {
13
+ return value?.trim() ?? "";
14
+ };
15
+
16
+ export const isRecord = (value: unknown): value is Record<string, unknown> => {
17
+ return !!value && typeof value === "object" && !Array.isArray(value);
18
+ };
19
+
20
+ export const isOutcome = (value: unknown): value is Outcome => {
21
+ if (!isRecord(value)) return false;
22
+ const status = value.status;
23
+ if (status !== "pass" && status !== "fail" && status !== "skip") return false;
24
+ return true;
25
+ };
26
+
27
+ export const say = (
28
+ locale: Locale,
29
+ status: Outcome["status"],
30
+ copy: Copy,
31
+ vars?: Record<string, string | number>
32
+ ): Outcome => {
33
+ return { status, detail: translate(locale, copy, vars) };
34
+ };
35
+
36
+ export const chooseChannel = (ctx: RunCtx): YamlChannel | Outcome => {
37
+ const { channels, channelId, locale } = ctx;
38
+ if (channelId) {
39
+ const found = channels.find(item => item.channel_id === channelId);
40
+ if (found) return found;
41
+ const detail = translate(locale, COPY.unknownChannel, { id: channelId });
42
+ return { status: "fail", detail };
43
+ }
44
+ if (channels.length !== 1) {
45
+ return say(locale, "fail", COPY.flowNoChannel);
46
+ }
47
+ const only = channels[0];
48
+ if (!only) return say(locale, "fail", COPY.flowNoChannel);
49
+ return only;
50
+ };
51
+
52
+ export const fieldGap = (
53
+ channel: YamlChannel,
54
+ env: NodeJS.ProcessEnv,
55
+ fields: readonly string[]
56
+ ) => {
57
+ for (const field of fields) {
58
+ if (field.startsWith("FIRESPARK_")) {
59
+ if (!textOf(env[field])) return field;
60
+ continue;
61
+ }
62
+ if (field === "payment") {
63
+ const payment = channel.payment ? paymentOf(channel.payment) : null;
64
+ if (!payment) return "payment";
65
+ continue;
66
+ }
67
+ const bag = channel as unknown as Record<string, unknown>;
68
+ const value = bag[field];
69
+ if (typeof value !== "string" || !value.trim()) return field;
70
+ }
71
+ return "";
72
+ };
73
+
74
+ export const prepare = (ctx: RunCtx, fields: readonly string[]) => {
75
+ const chosen = chooseChannel(ctx);
76
+ if (isOutcome(chosen)) return chosen;
77
+ const field = fieldGap(chosen, ctx.env, fields);
78
+ if (!field) return chosen;
79
+ const channel = channelLabel(chosen);
80
+ const vars = { channel, field };
81
+ return say(ctx.locale, "fail", COPY.flowMissing, vars);
82
+ };
83
+
84
+ export const fromLive = (
85
+ locale: Locale,
86
+ result: CheckFail | CheckSkip
87
+ ): Outcome => {
88
+ const detail = translate(locale, result.reason.copy, result.reason.vars);
89
+ if (result.status === "skip") return { status: "skip", detail };
90
+ return { status: "fail", detail: `${result.hop}: ${detail}` };
91
+ };
92
+
93
+ export const openLive = (ctx: RunCtx, channel: YamlChannel): LiveSession => {
94
+ const config = liveConfigOf(channel, ctx.env);
95
+ return createLiveSession(config, { fetch }, ctx.redactor);
96
+ };
97
+
98
+ export const rowsOf = (value: unknown) => {
99
+ if (!Array.isArray(value)) return [];
100
+ return value.filter(isRecord);
101
+ };
102
+
103
+ export const queryOf = (path: string, pairs: Record<string, string>) => {
104
+ const params = new URLSearchParams();
105
+ for (const [key, value] of Object.entries(pairs)) {
106
+ if (value) params.set(key, value);
107
+ }
108
+ const query = params.toString();
109
+ if (!query) return path;
110
+ return `${path}?${query}`;
111
+ };
112
+
113
+ export const isLocalHost = (value: string) => {
114
+ try {
115
+ const host = new URL(value).hostname.toLowerCase();
116
+ if (host === "localhost" || host === "0.0.0.0") return true;
117
+ if (host === "127.0.0.1" || host === "::1") return true;
118
+ return host.endsWith(".local");
119
+ } catch {
120
+ return true;
121
+ }
122
+ };
123
+
124
+ export const withQuery = (url: string, key: string, value: string) => {
125
+ const parsed = new URL(url);
126
+ parsed.searchParams.set(key, value);
127
+ return parsed.toString();
128
+ };
129
+
130
+ export const readPage = async (url: string) => {
131
+ const signal = AbortSignal.timeout(15000);
132
+ const response = await fetch(url, { signal });
133
+ const body = await response.text();
134
+ return { status: response.status, body };
135
+ };
136
+
137
+ export const namedOrderSkip = (ctx: RunCtx, id: string): Outcome | null => {
138
+ if (ctx.named.has(id)) return null;
139
+ return say(ctx.locale, "skip", COPY.namedOrder, { id });
140
+ };
141
+
142
+ export const trpcCall = async (
143
+ base: string,
144
+ cookie: string,
145
+ procedure: string,
146
+ input: unknown
147
+ ) => {
148
+ const root = base.replace(/\/$/, "");
149
+ const url = `${root}/api/trpc/${procedure}`;
150
+ const headers = { "content-type": "application/json", cookie };
151
+ const signal = AbortSignal.timeout(15000);
152
+ const body = JSON.stringify({ json: input });
153
+ const init = { method: "POST", headers, body, signal };
154
+ try {
155
+ const response = await fetch(url, init);
156
+ const text = await response.text();
157
+ return { status: response.status, text };
158
+ } catch {
159
+ return { status: 0, text: "" };
160
+ }
161
+ };
162
+
163
+ export const withSafeFix = (
164
+ run: (ctx: RunCtx) => Promise<Outcome>
165
+ ): CheckBinding => {
166
+ const fix = async (ctx: RunCtx): Promise<Outcome> => {
167
+ const result = await run(ctx);
168
+ if (result.status !== "fail") return result;
169
+ const note = translate(ctx.locale, COPY.noSafeChange);
170
+ const detail = result.detail ? `${note} ${result.detail}` : note;
171
+ const failed: Outcome = { status: "fail", detail };
172
+ return failed;
173
+ };
174
+ return { kind: "script", run, fix };
175
+ };
176
+
177
+ export const ACTIVE = "ACTIVE";
178
+
179
+ export const isActive = (row: Record<string, unknown>) => {
180
+ if (!("status" in row)) return true;
181
+ return row.status === ACTIVE;
182
+ };