@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,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
+ };
@@ -0,0 +1,346 @@
1
+ import { access, readFile, readdir, writeFile } from "node:fs/promises";
2
+ import { isAbsolute, resolve } from "node:path";
3
+
4
+ import type { Locale } from "../../../core/i18n";
5
+ import { translate } from "../../../core/i18n";
6
+ import { UsageError, readString } from "../../../core/options";
7
+ import type { OptionValues } from "../../../core/options";
8
+ import { COPY } from "./copy";
9
+ import { identityFromLegacy, identityOf, listManifests } from "./manifests";
10
+ import { emptyChannel, parseDoctorYaml, writeDoctorYaml } from "./yaml";
11
+ import type { DoctorFile, YamlChannel } from "./yaml";
12
+
13
+ export interface TargetFlags {
14
+ url?: string;
15
+ dashboardUrl?: string;
16
+ apiUrl?: string;
17
+ storeId?: string;
18
+ fulfillmentId?: string;
19
+ payment?: string;
20
+ receiver?: string;
21
+ evidence?: string;
22
+ envName?: string;
23
+ merchantId?: string;
24
+ channelId?: string;
25
+ deployed: boolean;
26
+ }
27
+
28
+ const URL_FLAGS = ["url", "dashboard-url", "api-url", "receiver"] as const;
29
+
30
+ export const REQUIRED_FIELDS = [
31
+ "url",
32
+ "dashboard_url",
33
+ "api_url",
34
+ "store_id",
35
+ "fulfillment_id",
36
+ "payment",
37
+ "receiver",
38
+ "evidence"
39
+ ] as const;
40
+
41
+ export type RequiredField = (typeof REQUIRED_FIELDS)[number];
42
+
43
+ export const CREDENTIAL_ENV = [
44
+ "FIRESPARK_DOCTOR_CUSTOMER_EMAIL",
45
+ "FIRESPARK_DOCTOR_CUSTOMER_PASSWORD"
46
+ ] as const;
47
+
48
+ const httpUrl = (value: string) => /^https?:\/\//i.test(value);
49
+
50
+ export const paymentOf = (value: string) => {
51
+ const text = value.trim().toLowerCase();
52
+ if (text === "cash" || text === "efectivo") return "cash";
53
+ if (text === "sandbox") return "sandbox";
54
+ return null;
55
+ };
56
+
57
+ export const readTargets = (values: OptionValues, locale: Locale) => {
58
+ const raw = (name: string) => readString(values, name);
59
+ for (const flag of URL_FLAGS) {
60
+ const value = raw(flag);
61
+ if (value && !httpUrl(value)) {
62
+ throw new UsageError(translate(locale, COPY.badUrl, { flag }));
63
+ }
64
+ }
65
+ const paymentText = raw("payment");
66
+ const payment = paymentText ? paymentOf(paymentText) : undefined;
67
+ if (paymentText && !payment) {
68
+ throw new UsageError(translate(locale, COPY.badPayment));
69
+ }
70
+ const flags: TargetFlags = {
71
+ url: raw("url"),
72
+ dashboardUrl: raw("dashboard-url"),
73
+ apiUrl: raw("api-url"),
74
+ storeId: raw("store-id"),
75
+ fulfillmentId: raw("fulfillment-id"),
76
+ payment: payment ?? undefined,
77
+ receiver: raw("receiver"),
78
+ evidence: raw("evidence"),
79
+ envName: raw("env"),
80
+ merchantId: raw("merchant-id"),
81
+ channelId: raw("channel-id"),
82
+ deployed: values.deployed === true
83
+ };
84
+ return flags;
85
+ };
86
+
87
+ const hasTarget = (flags: TargetFlags) => {
88
+ const { deployed, channelId, ...rest } = flags;
89
+ void channelId;
90
+ if (deployed) return true;
91
+ return Object.values(rest).some(value => value !== undefined);
92
+ };
93
+
94
+ const fill = (channel: YamlChannel, flags: TargetFlags): YamlChannel => {
95
+ const next: YamlChannel = { ...channel };
96
+ if (flags.url) next.url = flags.url;
97
+ if (flags.dashboardUrl) next.dashboard_url = flags.dashboardUrl;
98
+ if (flags.apiUrl) next.api_url = flags.apiUrl;
99
+ if (flags.storeId) next.store_id = flags.storeId;
100
+ if (flags.fulfillmentId) next.fulfillment_id = flags.fulfillmentId;
101
+ if (flags.payment) next.payment = flags.payment;
102
+ if (flags.receiver) next.receiver = flags.receiver;
103
+ if (flags.evidence) next.evidence = flags.evidence;
104
+ if (flags.envName) next.env = flags.envName;
105
+ if (flags.merchantId) next.merchant_id = flags.merchantId;
106
+ if (flags.deployed) next.deployed = true;
107
+ if (flags.channelId && channel.channel_id === flags.channelId) {
108
+ next.channel_id = flags.channelId;
109
+ }
110
+ return next;
111
+ };
112
+
113
+ export const applyTargets = (
114
+ channels: YamlChannel[],
115
+ flags: TargetFlags,
116
+ locale: Locale
117
+ ) => {
118
+ if (!hasTarget(flags)) return channels;
119
+ if (flags.channelId) {
120
+ const index = channels.findIndex(c => c.channel_id === flags.channelId);
121
+ if (index < 0) {
122
+ const id = flags.channelId;
123
+ throw new UsageError(translate(locale, COPY.unknownChannel, { id }));
124
+ }
125
+ return channels.map((channel, i) => {
126
+ return i === index ? fill(channel, flags) : channel;
127
+ });
128
+ }
129
+ if (channels.length !== 1) {
130
+ throw new UsageError(translate(locale, COPY.ambiguous));
131
+ }
132
+ const only = channels[0];
133
+ if (!only) return channels;
134
+ return [fill(only, flags)];
135
+ };
136
+
137
+ export const channelLabel = (channel: YamlChannel) => {
138
+ if (channel.name.trim()) return channel.name.trim();
139
+ if (channel.channel_id) return channel.channel_id;
140
+ return channel.template || "channel";
141
+ };
142
+
143
+ const missingField = (channel: YamlChannel): RequiredField | null => {
144
+ for (const field of REQUIRED_FIELDS) {
145
+ if (field === "payment") continue;
146
+ const value = channel[field];
147
+ if (typeof value !== "string" || value.trim() === "") return field;
148
+ }
149
+ const payment = channel.payment ? paymentOf(channel.payment) : null;
150
+ if (!payment) return "payment";
151
+ return null;
152
+ };
153
+
154
+ export interface PreflightGap {
155
+ channel: string;
156
+ field: string;
157
+ }
158
+
159
+ export const preflightGap = (
160
+ channels: YamlChannel[],
161
+ env: NodeJS.ProcessEnv
162
+ ): PreflightGap | null => {
163
+ for (const channel of channels) {
164
+ if (!channel.deployed) continue;
165
+ const field = missingField(channel);
166
+ if (field) return { channel: channelLabel(channel), field };
167
+ for (const name of CREDENTIAL_ENV) {
168
+ const value = env[name];
169
+ if (!value || !value.trim()) {
170
+ return { channel: channelLabel(channel), field: name };
171
+ }
172
+ }
173
+ }
174
+ return null;
175
+ };
176
+
177
+ export const fileExists = async (path: string) => {
178
+ try {
179
+ await access(path);
180
+ return true;
181
+ } catch {
182
+ return false;
183
+ }
184
+ };
185
+
186
+ export const resolveConfigPath = async (
187
+ cwd: string,
188
+ flag: string | undefined,
189
+ locale: Locale
190
+ ) => {
191
+ if (flag) return isAbsolute(flag) ? flag : resolve(cwd, flag);
192
+ let names: string[] = [];
193
+ try {
194
+ names = await readdir(cwd);
195
+ } catch {
196
+ names = [];
197
+ }
198
+ const found = names.filter(name => name.endsWith(".doctor.yml")).sort();
199
+ if (found.length > 1) {
200
+ const files = found.join(", ");
201
+ throw new UsageError(translate(locale, COPY.manyConfigs, { files }));
202
+ }
203
+ const only = found[0];
204
+ if (only) return resolve(cwd, only);
205
+ return resolve(cwd, ".firespark.doctor.yml");
206
+ };
207
+
208
+ export const readConfig = async (path: string, locale: Locale) => {
209
+ const body = await readFile(path, "utf8");
210
+ return parseDoctorYaml(body, path, locale);
211
+ };
212
+
213
+ export const writeConfig = async (path: string, file: DoctorFile) => {
214
+ await writeFile(path, writeDoctorYaml(file), "utf8");
215
+ };
216
+
217
+ const YES = new Set(["si", "sí", "yes", "y", "s", "sim"]);
218
+ const NO = new Set(["no", "n", "nao", "não"]);
219
+
220
+ const foldAnswer = (value: string) => value.trim().toLowerCase();
221
+
222
+ export interface Asker {
223
+ ask: (question: string) => Promise<string>;
224
+ interactive: boolean;
225
+ }
226
+
227
+ const askYes = async (channel: YamlChannel, locale: Locale, asker: Asker) => {
228
+ const labelText = channelLabel(channel);
229
+ const question = translate(locale, COPY.deployedAsk, { channel: labelText });
230
+ for (let attempt = 0; attempt < 3; attempt += 1) {
231
+ const answer = foldAnswer(await asker.ask(question));
232
+ if (YES.has(answer)) return true;
233
+ if (NO.has(answer)) return false;
234
+ }
235
+ const vars = { channel: labelText };
236
+ throw new UsageError(translate(locale, COPY.answerYesNo, vars));
237
+ };
238
+
239
+ const ASK: Record<RequiredField, (typeof COPY)[keyof typeof COPY]> = {
240
+ url: COPY.askUrl,
241
+ dashboard_url: COPY.askDashboard,
242
+ api_url: COPY.askApi,
243
+ store_id: COPY.askStore,
244
+ fulfillment_id: COPY.askFulfillment,
245
+ payment: COPY.askPayment,
246
+ receiver: COPY.askReceiver,
247
+ evidence: COPY.askEvidence
248
+ };
249
+
250
+ const askField = async (
251
+ channel: YamlChannel,
252
+ field: RequiredField,
253
+ locale: Locale,
254
+ asker: Asker
255
+ ) => {
256
+ const labelText = channelLabel(channel);
257
+ const question = translate(locale, ASK[field], { channel: labelText });
258
+ const answer = (await asker.ask(question)).trim();
259
+ if (!answer) return null;
260
+ if (field === "payment") {
261
+ const payment = paymentOf(answer);
262
+ if (!payment)
263
+ throw new UsageError(translate(locale, COPY.badAnswerPayment));
264
+ return payment;
265
+ }
266
+ if (field === "url" || field === "dashboard_url" || field === "api_url") {
267
+ if (!httpUrl(answer)) {
268
+ throw new UsageError(translate(locale, COPY.badUrl, { flag: field }));
269
+ }
270
+ }
271
+ if (field === "receiver" && !httpUrl(answer)) {
272
+ throw new UsageError(translate(locale, COPY.badUrl, { flag: "receiver" }));
273
+ }
274
+ return answer;
275
+ };
276
+
277
+ const promptChannel = async (
278
+ channel: YamlChannel,
279
+ flags: TargetFlags,
280
+ locale: Locale,
281
+ asker: Asker,
282
+ askDeployed: boolean
283
+ ) => {
284
+ const next: YamlChannel = { ...channel };
285
+ const marked = flags.deployed && flags.channelId === channel.channel_id;
286
+ const sole = flags.deployed && !flags.channelId;
287
+ if (askDeployed && !next.deployed && asker.interactive && !marked && !sole) {
288
+ next.deployed = await askYes(next, locale, asker);
289
+ }
290
+ if (!next.deployed || !asker.interactive) return next;
291
+ for (const field of REQUIRED_FIELDS) {
292
+ const value = next[field];
293
+ if (typeof value === "string" && value.trim()) continue;
294
+ const answer = await askField(next, field, locale, asker);
295
+ if (answer) next[field] = answer;
296
+ }
297
+ return next;
298
+ };
299
+
300
+ export const promptAll = async (
301
+ channels: YamlChannel[],
302
+ flags: TargetFlags,
303
+ locale: Locale,
304
+ asker: Asker,
305
+ askDeployed: boolean
306
+ ) => {
307
+ const next: YamlChannel[] = [];
308
+ for (const channel of channels) {
309
+ const args = [channel, flags, locale, asker, askDeployed] as const;
310
+ const filled = await promptChannel(...args);
311
+ next.push(filled);
312
+ }
313
+ return next;
314
+ };
315
+
316
+ export const identitiesFromRepo = async (root: string) => {
317
+ const files = await listManifests(root);
318
+ const channels: YamlChannel[] = [];
319
+ for (const file of files) {
320
+ const read = file.kind === "json" ? identityOf : identityFromLegacy;
321
+ const channel = read(file.body);
322
+ if (channel) channels.push(channel);
323
+ }
324
+ return channels;
325
+ };
326
+
327
+ export const createConfig = async (
328
+ root: string,
329
+ flags: TargetFlags,
330
+ locale: Locale,
331
+ asker: Asker
332
+ ) => {
333
+ const seeded = await identitiesFromRepo(root);
334
+ let seed = seeded;
335
+ const empty = seeded.length === 0;
336
+ const targeted = hasTarget(flags) || flags.channelId;
337
+ const needsBlank = empty && targeted;
338
+ if (needsBlank) {
339
+ const blank = emptyChannel();
340
+ if (flags.channelId) blank.channel_id = flags.channelId;
341
+ seed = [blank];
342
+ }
343
+ const withFlags = applyTargets(seed, flags, locale);
344
+ const channels = await promptAll(withFlags, flags, locale, asker, true);
345
+ return { channels } satisfies DoctorFile;
346
+ };