@fluid-app/fluid-cli-setup 0.1.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 ADDED
@@ -0,0 +1,20 @@
1
+ # @fluid-app/fluid-cli-setup
2
+
3
+ Fluid CLI plugin that lists and shows the guided setups published at
4
+ https://docs.fluid.app/setup/overview.
5
+
6
+ ```bash
7
+ fluid setup list # the available guided setups
8
+ fluid setup show custom-domain # its steps, the command each runs, and its docs page
9
+ fluid setup run custom-domain # do it interactively in the terminal
10
+ ```
11
+
12
+ `run` hands over to the guide's plugin walk when the guide names one (its
13
+ frontmatter `setup.walk`, e.g. `fluid connect walk exigo`). Otherwise it steps
14
+ through the guide's commands. It runs a command for you when the command needs
15
+ no `<values>`, lets you mark a step done, go back, or stop, and repeats a step
16
+ like a status check until you mark it done.
17
+
18
+ Output is JSON. Each step says whether it changes live configuration
19
+ (`changes`) and, for a step you repeat, the result that means it is done
20
+ (`doneWhen`).
@@ -0,0 +1,9 @@
1
+ import { Command } from "commander";
2
+ import { FluidPlugin } from "@fluid-app/fluid-cli";
3
+
4
+ //#region src/index.d.ts
5
+ declare function createSetupCommand(program?: Command): Command;
6
+ declare const plugin: FluidPlugin;
7
+ //#endregion
8
+ export { createSetupCommand, plugin as default };
9
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;iBAwBgB,kBAAA,CAAmB,OAAA,GAAU,OAAA,GAAU,OAAA;AAAA,cA+DjD,MAAA,EAAQ,WAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,276 @@
1
+ import { Command } from "commander";
2
+ import { assertInteractive, terminalPrompter } from "@fluid-app/fluid-cli";
3
+ import { parse } from "yaml";
4
+ //#region src/guides.ts
5
+ const DOCS_SOURCE = (process.env["FLUID_DOCS_SOURCE"] ?? "https://raw.githubusercontent.com/fluid-commerce/mintlify-starter/main").replace(/\/$/, "");
6
+ const DOCS_SITE = "https://docs.fluid.app";
7
+ const SETUP_GROUP = "Guided setup";
8
+ var SetupGuideError = class extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "SetupGuideError";
12
+ }
13
+ };
14
+ /** How long one docs read may take, body included, before it is reported. */
15
+ const FETCH_TIMEOUT_MS = 15e3;
16
+ async function fetchText(fetchImpl, url) {
17
+ const controller = new AbortController();
18
+ let timedOut = false;
19
+ const deadline = setTimeout(() => {
20
+ timedOut = true;
21
+ controller.abort();
22
+ }, FETCH_TIMEOUT_MS);
23
+ try {
24
+ const response = await fetchImpl(url, { signal: controller.signal });
25
+ if (!response.ok) throw new SetupGuideError(`Couldn't read ${url} (HTTP ${response.status}).`);
26
+ return await response.text();
27
+ } catch (error) {
28
+ if (timedOut) throw new SetupGuideError(`Timed out reading ${url} after ${FETCH_TIMEOUT_MS / 1e3}s. Check your connection and try again.`);
29
+ throw error;
30
+ } finally {
31
+ clearTimeout(deadline);
32
+ }
33
+ }
34
+ /** Page paths in the docs' "Guided setup" group, overview excluded. */
35
+ function setupPagesFromNavigation(docsJson) {
36
+ const pages = [];
37
+ const visit = (node) => {
38
+ if (Array.isArray(node)) {
39
+ node.forEach(visit);
40
+ return;
41
+ }
42
+ if (!node || typeof node !== "object") return;
43
+ const group = node;
44
+ if (group.group === SETUP_GROUP && Array.isArray(group.pages)) {
45
+ for (const page of group.pages) if (typeof page === "string" && !page.endsWith("/overview")) pages.push(page);
46
+ return;
47
+ }
48
+ Object.values(node).forEach(visit);
49
+ };
50
+ visit(docsJson?.navigation);
51
+ return pages;
52
+ }
53
+ function frontmatter(source) {
54
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(source);
55
+ if (!match) throw new SetupGuideError("The page has no frontmatter.");
56
+ const data = parse(match[1]);
57
+ if (!data || typeof data !== "object") throw new SetupGuideError("The page's frontmatter isn't an object.");
58
+ return data;
59
+ }
60
+ function text(value, field) {
61
+ if (typeof value !== "string" || value.trim() === "") throw new SetupGuideError(`The guide is missing \`${field}\`.`);
62
+ return value;
63
+ }
64
+ function walkCommand(value, page) {
65
+ if (value === void 0 || value === null) return null;
66
+ if (typeof value !== "string" || !/^fluid( [a-z][\w-]*)+ walk( [\w-]+)*$/.test(value)) throw new SetupGuideError(`${page}'s \`setup.walk\` must be a \`fluid <command> … walk\` command.`);
67
+ return value;
68
+ }
69
+ /** Parse a guide page's source into its summary and steps. */
70
+ function parseSetupGuide(page, source) {
71
+ const data = frontmatter(source);
72
+ const setup = data["setup"];
73
+ if (!setup || typeof setup !== "object") throw new SetupGuideError(`${page} has no \`setup\` block.`);
74
+ const rawSteps = setup["steps"];
75
+ if (!Array.isArray(rawSteps) || rawSteps.length === 0) throw new SetupGuideError(`${page} lists no setup steps.`);
76
+ const steps = rawSteps.map((raw, index) => {
77
+ const step = raw ?? {};
78
+ const at = `setup.steps[${index}]`;
79
+ return {
80
+ id: text(step["id"], `${at}.id`),
81
+ title: text(step["title"], `${at}.title`),
82
+ run: text(step["run"], `${at}.run`),
83
+ changes: step["changes"] === true,
84
+ repeat: step["repeat"] === true,
85
+ doneWhen: typeof step["done_when"] === "string" ? step["done_when"] : null
86
+ };
87
+ });
88
+ return {
89
+ id: text(setup["id"], "setup.id"),
90
+ title: text(data["title"], "title"),
91
+ description: typeof data["description"] === "string" ? data["description"] : "",
92
+ cli: text(setup["cli"], "setup.cli"),
93
+ url: `${DOCS_SITE}/${page}`,
94
+ markdownUrl: `${DOCS_SITE}/${page}.md`,
95
+ steps,
96
+ walk: walkCommand(setup["walk"], page)
97
+ };
98
+ }
99
+ /** Read every guide in the Guided setup group. One unreadable or malformed
100
+ * page is reported, not fatal, so it can't hide the others. */
101
+ async function loadGuides(fetchImpl) {
102
+ const pages = setupPagesFromNavigation(JSON.parse(await fetchText(fetchImpl, `${DOCS_SOURCE}/docs.json`)));
103
+ const results = await Promise.allSettled(pages.map(async (page) => parseSetupGuide(page, await fetchText(fetchImpl, `${DOCS_SOURCE}/${page}.mdx`))));
104
+ const guides = [];
105
+ const problems = [];
106
+ results.forEach((result, index) => {
107
+ if (result.status === "fulfilled") guides.push(result.value);
108
+ else {
109
+ const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
110
+ problems.push(`${pages[index]}: ${reason}`);
111
+ }
112
+ });
113
+ return {
114
+ guides,
115
+ problems
116
+ };
117
+ }
118
+ const warnOnStderr = (message) => {
119
+ process.stderr.write(JSON.stringify({ warning: message }) + "\n");
120
+ };
121
+ async function listSetupGuides(fetchImpl = globalThis.fetch, warn = warnOnStderr) {
122
+ const { guides, problems } = await loadGuides(fetchImpl);
123
+ problems.forEach((problem) => warn(`Skipped a guide: ${problem}`));
124
+ return guides.map(({ id, title, description, cli, url }) => ({
125
+ id,
126
+ title,
127
+ description,
128
+ cli,
129
+ url
130
+ }));
131
+ }
132
+ async function getSetupGuide(id, fetchImpl = globalThis.fetch) {
133
+ const { guides, problems } = await loadGuides(fetchImpl);
134
+ const guide = guides.find((candidate) => candidate.id === id);
135
+ if (!guide) throw new SetupGuideError(`No guided setup "${id}". Available: ${guides.map((candidate) => candidate.id).join(", ") || "none"}.${problems.length ? ` Some guides couldn't be read: ${problems.join("; ")}.` : ""}`);
136
+ return guide;
137
+ }
138
+ //#endregion
139
+ //#region src/run.ts
140
+ /** `<hostname>` and the like: values a person has to fill in. */
141
+ const PLACEHOLDER = /<[^>]+>/;
142
+ /** The words after `fluid` in a command line. */
143
+ function commandArgs(command) {
144
+ return command.trim().split(/\s+/).slice(1);
145
+ }
146
+ function missingPlugin(guide) {
147
+ return /* @__PURE__ */ new Error(`This guide needs ${guide.cli}. Install it with \`npm install -g ${guide.cli}\`, then run this again.`);
148
+ }
149
+ async function runGuide(guide, deps) {
150
+ if (guide.walk) {
151
+ const args = commandArgs(guide.walk);
152
+ if (!deps.hasCommand(args[0])) throw missingPlugin(guide);
153
+ deps.prompter.note(`${guide.title}: ${guide.url}`);
154
+ const ok = await deps.runCommand(args);
155
+ return {
156
+ guide: guide.id,
157
+ mode: "walk",
158
+ completed: [],
159
+ finished: ok,
160
+ failed: ok ? [] : [guide.id]
161
+ };
162
+ }
163
+ const domain = commandArgs(guide.steps[0].run)[0];
164
+ if (domain && !deps.hasCommand(domain)) throw missingPlugin(guide);
165
+ const { prompter } = deps;
166
+ prompter.note(`${guide.title} (${guide.url})`);
167
+ prompter.note("Run each step from here, or mark it done once you've run it yourself. Ctrl-C stops.");
168
+ const completed = [];
169
+ const failed = /* @__PURE__ */ new Set();
170
+ let index = 0;
171
+ while (index < guide.steps.length) {
172
+ const step = guide.steps[index];
173
+ const needsValues = PLACEHOLDER.test(step.run);
174
+ prompter.note(`\nStep ${index + 1} of ${guide.steps.length}: ${step.title}\n ${step.run}` + (step.doneWhen ? `\n Done when: ${step.doneWhen}` : "") + (needsValues ? "\n Fill in the <values> and run it in another terminal; the docs page shows examples." : ""));
175
+ const choice = await prompter.menu("What next?", [
176
+ ...needsValues ? [] : [{
177
+ title: step.changes ? "Run it (changes live config)" : "Run it",
178
+ value: "run"
179
+ }],
180
+ {
181
+ title: "It's done. Next step",
182
+ value: "next"
183
+ },
184
+ {
185
+ title: "← Back",
186
+ value: "back",
187
+ disabled: index === 0
188
+ },
189
+ {
190
+ title: "Stop for now",
191
+ value: "stop"
192
+ }
193
+ ]);
194
+ if (choice === null || choice === "stop") break;
195
+ if (choice === "back") {
196
+ index -= 1;
197
+ continue;
198
+ }
199
+ if (choice === "run") {
200
+ if (!await deps.runCommand(commandArgs(step.run))) {
201
+ failed.add(step.id);
202
+ prompter.note("That command failed (see its error above). Fix it and run the step again, or mark it done if you finished it another way.");
203
+ continue;
204
+ }
205
+ failed.delete(step.id);
206
+ if (step.repeat) continue;
207
+ }
208
+ failed.delete(step.id);
209
+ if (!completed.includes(step.id)) completed.push(step.id);
210
+ index += 1;
211
+ }
212
+ return {
213
+ guide: guide.id,
214
+ mode: "steps",
215
+ completed,
216
+ finished: index >= guide.steps.length,
217
+ failed: [...failed]
218
+ };
219
+ }
220
+ //#endregion
221
+ //#region src/index.ts
222
+ function print(data) {
223
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
224
+ }
225
+ /** Failures print a JSON `error` on stderr and exit non-zero, like the
226
+ * commands the guides run. */
227
+ function run(action) {
228
+ return action().catch((err) => {
229
+ process.stderr.write(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }) + "\n");
230
+ process.exitCode = 1;
231
+ });
232
+ }
233
+ function createSetupCommand(program) {
234
+ const setup = new Command("setup").description("Guided setups from docs.fluid.app: the steps and the commands to run");
235
+ setup.command("list").description("List the available guided setups").action(() => run(async () => print(await listSetupGuides())));
236
+ setup.command("show <id>").description("Show a guided setup's steps, the command each runs, and its docs page").action((id) => run(async () => print(await getSetupGuide(id))));
237
+ setup.command("run <id>").description("Do a guided setup interactively: hands over to the plugin's walk when it has one, otherwise steps through its commands").action((id) => run(async () => {
238
+ assertInteractive("`fluid setup show <id>` and run each step's command");
239
+ const guide = await getSetupGuide(id);
240
+ const root = program ?? setup.parent;
241
+ if (!root) throw new Error("fluid setup run needs the fluid program.");
242
+ const result = await runGuide(guide, {
243
+ prompter: terminalPrompter(),
244
+ runCommand: async (args) => {
245
+ const before = process.exitCode;
246
+ process.exitCode = void 0;
247
+ let ok = true;
248
+ try {
249
+ await root.parseAsync(args, { from: "user" });
250
+ ok = !process.exitCode;
251
+ } catch (error) {
252
+ ok = false;
253
+ process.stderr.write(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }) + "\n");
254
+ } finally {
255
+ process.exitCode = before;
256
+ }
257
+ return ok;
258
+ },
259
+ hasCommand: (name) => root.commands.some((c) => c.name() === name)
260
+ });
261
+ print(result);
262
+ if (result.failed.length > 0) process.exitCode = 1;
263
+ }));
264
+ return setup;
265
+ }
266
+ const plugin = {
267
+ name: "@fluid-app/fluid-cli-setup",
268
+ version: "0.1.0",
269
+ register(ctx) {
270
+ ctx.program.addCommand(createSetupCommand(ctx.program));
271
+ }
272
+ };
273
+ //#endregion
274
+ export { createSetupCommand, plugin as default };
275
+
276
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/guides.ts","../src/run.ts","../src/index.ts"],"sourcesContent":["// Reads the guided setups published in the Fluid docs.\n//\n// Each guide is a page in the docs' \"Guided setup\" group whose frontmatter\n// carries a `setup` block: the steps in order, the CLI command each runs,\n// and whether it changes live configuration. The published site's markdown\n// view drops frontmatter, so the page source is read from the public docs\n// repository instead. No credentials are sent to either host.\n\nimport { parse } from \"yaml\";\n\nconst DOCS_SOURCE = (\n process.env[\"FLUID_DOCS_SOURCE\"] ??\n \"https://raw.githubusercontent.com/fluid-commerce/mintlify-starter/main\"\n).replace(/\\/$/, \"\");\nconst DOCS_SITE = \"https://docs.fluid.app\";\nconst SETUP_GROUP = \"Guided setup\";\n\nexport interface SetupStep {\n id: string;\n title: string;\n run: string;\n changes: boolean;\n repeat: boolean;\n doneWhen: string | null;\n}\n\nexport interface SetupGuideSummary {\n id: string;\n title: string;\n description: string;\n /** npm package that provides the guide's commands. */\n cli: string;\n url: string;\n}\n\nexport interface SetupGuide extends SetupGuideSummary {\n /** The page as markdown: prerequisites, status meanings, troubleshooting. */\n markdownUrl: string;\n steps: SetupStep[];\n /** The plugin command that walks a person through the whole guide\n * interactively, e.g. `fluid connect walk exigo`; null when the guide has\n * none and `fluid setup run` steps through its commands instead. */\n walk: string | null;\n}\n\nexport class SetupGuideError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"SetupGuideError\";\n }\n}\n\ntype Fetch = typeof globalThis.fetch;\n\n/** How long one docs read may take, body included, before it is reported. */\nexport const FETCH_TIMEOUT_MS = 15_000;\n\nasync function fetchText(fetchImpl: Fetch, url: string): Promise<string> {\n const controller = new AbortController();\n let timedOut = false;\n const deadline = setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, FETCH_TIMEOUT_MS);\n try {\n const response = await fetchImpl(url, { signal: controller.signal });\n if (!response.ok) {\n throw new SetupGuideError(\n `Couldn't read ${url} (HTTP ${response.status}).`,\n );\n }\n return await response.text();\n } catch (error) {\n if (timedOut) {\n throw new SetupGuideError(\n `Timed out reading ${url} after ${FETCH_TIMEOUT_MS / 1000}s. Check your connection and try again.`,\n );\n }\n throw error;\n } finally {\n clearTimeout(deadline);\n }\n}\n\ninterface NavGroup {\n group?: unknown;\n pages?: unknown[];\n}\n\n/** Page paths in the docs' \"Guided setup\" group, overview excluded. */\nexport function setupPagesFromNavigation(docsJson: unknown): string[] {\n const pages: string[] = [];\n const visit = (node: unknown): void => {\n if (Array.isArray(node)) {\n node.forEach(visit);\n return;\n }\n if (!node || typeof node !== \"object\") return;\n const group = node as NavGroup;\n if (group.group === SETUP_GROUP && Array.isArray(group.pages)) {\n for (const page of group.pages) {\n if (typeof page === \"string\" && !page.endsWith(\"/overview\")) {\n pages.push(page);\n }\n }\n return;\n }\n Object.values(node).forEach(visit);\n };\n visit((docsJson as { navigation?: unknown })?.navigation);\n return pages;\n}\n\nfunction frontmatter(source: string): Record<string, unknown> {\n const match = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---/.exec(source);\n if (!match) throw new SetupGuideError(\"The page has no frontmatter.\");\n const data: unknown = parse(match[1]!);\n if (!data || typeof data !== \"object\") {\n throw new SetupGuideError(\"The page's frontmatter isn't an object.\");\n }\n return data as Record<string, unknown>;\n}\n\nfunction text(value: unknown, field: string): string {\n if (typeof value !== \"string\" || value.trim() === \"\") {\n throw new SetupGuideError(`The guide is missing \\`${field}\\`.`);\n }\n return value;\n}\n\nfunction walkCommand(value: unknown, page: string): string | null {\n if (value === undefined || value === null) return null;\n if (typeof value !== \"string\" || !/^fluid( [a-z][\\w-]*)+ walk( [\\w-]+)*$/.test(value)) {\n throw new SetupGuideError(\n `${page}'s \\`setup.walk\\` must be a \\`fluid <command> … walk\\` command.`,\n );\n }\n return value;\n}\n\n/** Parse a guide page's source into its summary and steps. */\nexport function parseSetupGuide(page: string, source: string): SetupGuide {\n const data = frontmatter(source);\n const setup = data[\"setup\"] as Record<string, unknown> | undefined;\n if (!setup || typeof setup !== \"object\") {\n throw new SetupGuideError(`${page} has no \\`setup\\` block.`);\n }\n const rawSteps = setup[\"steps\"];\n if (!Array.isArray(rawSteps) || rawSteps.length === 0) {\n throw new SetupGuideError(`${page} lists no setup steps.`);\n }\n const steps = rawSteps.map((raw, index): SetupStep => {\n const step = (raw ?? {}) as Record<string, unknown>;\n const at = `setup.steps[${index}]`;\n return {\n id: text(step[\"id\"], `${at}.id`),\n title: text(step[\"title\"], `${at}.title`),\n run: text(step[\"run\"], `${at}.run`),\n changes: step[\"changes\"] === true,\n repeat: step[\"repeat\"] === true,\n doneWhen:\n typeof step[\"done_when\"] === \"string\" ? step[\"done_when\"] : null,\n };\n });\n return {\n id: text(setup[\"id\"], \"setup.id\"),\n title: text(data[\"title\"], \"title\"),\n description:\n typeof data[\"description\"] === \"string\" ? data[\"description\"] : \"\",\n cli: text(setup[\"cli\"], \"setup.cli\"),\n url: `${DOCS_SITE}/${page}`,\n markdownUrl: `${DOCS_SITE}/${page}.md`,\n steps,\n walk: walkCommand(setup[\"walk\"], page),\n };\n}\n\ninterface LoadedGuides {\n guides: SetupGuide[];\n /** Pages that couldn't be read or parsed, with the reason. */\n problems: string[];\n}\n\n/** Read every guide in the Guided setup group. One unreadable or malformed\n * page is reported, not fatal, so it can't hide the others. */\nasync function loadGuides(fetchImpl: Fetch): Promise<LoadedGuides> {\n const docsJson: unknown = JSON.parse(\n await fetchText(fetchImpl, `${DOCS_SOURCE}/docs.json`),\n );\n const pages = setupPagesFromNavigation(docsJson);\n const results = await Promise.allSettled(\n pages.map(async (page) =>\n parseSetupGuide(\n page,\n await fetchText(fetchImpl, `${DOCS_SOURCE}/${page}.mdx`),\n ),\n ),\n );\n const guides: SetupGuide[] = [];\n const problems: string[] = [];\n results.forEach((result, index) => {\n if (result.status === \"fulfilled\") guides.push(result.value);\n else {\n const reason =\n result.reason instanceof Error\n ? result.reason.message\n : String(result.reason);\n problems.push(`${pages[index]}: ${reason}`);\n }\n });\n return { guides, problems };\n}\n\ntype Warn = (message: string) => void;\n\nconst warnOnStderr: Warn = (message) => {\n process.stderr.write(JSON.stringify({ warning: message }) + \"\\n\");\n};\n\nexport async function listSetupGuides(\n fetchImpl: Fetch = globalThis.fetch,\n warn: Warn = warnOnStderr,\n): Promise<SetupGuideSummary[]> {\n const { guides, problems } = await loadGuides(fetchImpl);\n problems.forEach((problem) => warn(`Skipped a guide: ${problem}`));\n return guides.map(({ id, title, description, cli, url }) => ({\n id,\n title,\n description,\n cli,\n url,\n }));\n}\n\nexport async function getSetupGuide(\n id: string,\n fetchImpl: Fetch = globalThis.fetch,\n): Promise<SetupGuide> {\n const { guides, problems } = await loadGuides(fetchImpl);\n const guide = guides.find((candidate) => candidate.id === id);\n if (!guide) {\n const known = guides.map((candidate) => candidate.id).join(\", \") || \"none\";\n const skipped = problems.length\n ? ` Some guides couldn't be read: ${problems.join(\"; \")}.`\n : \"\";\n throw new SetupGuideError(\n `No guided setup \"${id}\". Available: ${known}.${skipped}`,\n );\n }\n return guide;\n}\n","// `fluid setup run <guide>`: a guided setup, interactively. A guide whose\n// plugin has a walk hands over to it; any other guide is stepped through\n// here, one command at a time.\n\nimport type { Prompter } from \"@fluid-app/fluid-cli\";\n\nimport type { SetupGuide } from \"./guides.js\";\n\nexport interface RunDeps {\n prompter: Prompter;\n /** Run a `fluid …` command in this process, given its words after `fluid`.\n * False when it failed, whether it threw or set a non-zero exit code. */\n runCommand: (args: string[]) => Promise<boolean>;\n /** Whether a top-level command (e.g. `connect`) is installed. */\n hasCommand: (name: string) => boolean;\n}\n\nexport interface RunResult {\n guide: string;\n /** `walk` when the guide's plugin walk took over. */\n mode: \"walk\" | \"steps\";\n /** Step ids marked done or run, in order. */\n completed: string[];\n finished: boolean;\n /** Steps whose last run failed and that weren't then marked done. */\n failed: string[];\n}\n\n/** `<hostname>` and the like: values a person has to fill in. */\nconst PLACEHOLDER = /<[^>]+>/;\n\n/** The words after `fluid` in a command line. */\nexport function commandArgs(command: string): string[] {\n return command.trim().split(/\\s+/).slice(1);\n}\n\nfunction missingPlugin(guide: SetupGuide): Error {\n return new Error(\n `This guide needs ${guide.cli}. Install it with \\`npm install -g ${guide.cli}\\`, then run this again.`,\n );\n}\n\nexport async function runGuide(\n guide: SetupGuide,\n deps: RunDeps,\n): Promise<RunResult> {\n if (guide.walk) {\n const args = commandArgs(guide.walk);\n if (!deps.hasCommand(args[0]!)) throw missingPlugin(guide);\n deps.prompter.note(`${guide.title}: ${guide.url}`);\n const ok = await deps.runCommand(args);\n return {\n guide: guide.id,\n mode: \"walk\",\n completed: [],\n finished: ok,\n failed: ok ? [] : [guide.id],\n };\n }\n\n const domain = commandArgs(guide.steps[0]!.run)[0];\n if (domain && !deps.hasCommand(domain)) throw missingPlugin(guide);\n\n const { prompter } = deps;\n prompter.note(`${guide.title} (${guide.url})`);\n prompter.note(\"Run each step from here, or mark it done once you've run it yourself. Ctrl-C stops.\");\n const completed: string[] = [];\n const failed = new Set<string>();\n let index = 0;\n\n while (index < guide.steps.length) {\n const step = guide.steps[index]!;\n const needsValues = PLACEHOLDER.test(step.run);\n prompter.note(\n `\\nStep ${index + 1} of ${guide.steps.length}: ${step.title}\\n ${step.run}` +\n (step.doneWhen ? `\\n Done when: ${step.doneWhen}` : \"\") +\n (needsValues\n ? \"\\n Fill in the <values> and run it in another terminal; the docs page shows examples.\"\n : \"\"),\n );\n const choice = await prompter.menu(\"What next?\", [\n ...(needsValues\n ? []\n : [\n {\n title: step.changes ? \"Run it (changes live config)\" : \"Run it\",\n value: \"run\" as const,\n },\n ]),\n { title: \"It's done. Next step\", value: \"next\" as const },\n { title: \"← Back\", value: \"back\" as const, disabled: index === 0 },\n { title: \"Stop for now\", value: \"stop\" as const },\n ]);\n\n if (choice === null || choice === \"stop\") break;\n if (choice === \"back\") {\n index -= 1;\n continue;\n }\n if (choice === \"run\") {\n const ok = await deps.runCommand(commandArgs(step.run));\n if (!ok) {\n failed.add(step.id);\n prompter.note(\"That command failed (see its error above). Fix it and run the step again, or mark it done if you finished it another way.\");\n continue;\n }\n // It ran, so an earlier failure of this step no longer stands.\n failed.delete(step.id);\n // A repeated step (e.g. checking status) stays put until marked done.\n if (step.repeat) continue;\n }\n failed.delete(step.id);\n if (!completed.includes(step.id)) completed.push(step.id);\n index += 1;\n }\n\n return {\n guide: guide.id,\n mode: \"steps\",\n completed,\n finished: index >= guide.steps.length,\n failed: [...failed],\n };\n}\n","import { Command } from \"commander\";\nimport type { FluidPlugin } from \"@fluid-app/fluid-cli\";\nimport { assertInteractive, terminalPrompter } from \"@fluid-app/fluid-cli\";\n\nimport { getSetupGuide, listSetupGuides } from \"./guides.js\";\nimport { runGuide } from \"./run.js\";\n\nfunction print(data: unknown): void {\n process.stdout.write(JSON.stringify(data, null, 2) + \"\\n\");\n}\n\n/** Failures print a JSON `error` on stderr and exit non-zero, like the\n * commands the guides run. */\nfunction run(action: () => Promise<void>): Promise<void> {\n return action().catch((err: unknown) => {\n process.stderr.write(\n JSON.stringify({\n error: err instanceof Error ? err.message : String(err),\n }) + \"\\n\",\n );\n process.exitCode = 1;\n });\n}\n\nexport function createSetupCommand(program?: Command): Command {\n const setup = new Command(\"setup\").description(\n \"Guided setups from docs.fluid.app: the steps and the commands to run\",\n );\n\n setup\n .command(\"list\")\n .description(\"List the available guided setups\")\n .action(() => run(async () => print(await listSetupGuides())));\n\n setup\n .command(\"show <id>\")\n .description(\n \"Show a guided setup's steps, the command each runs, and its docs page\",\n )\n .action((id: string) => run(async () => print(await getSetupGuide(id))));\n\n setup\n .command(\"run <id>\")\n .description(\n \"Do a guided setup interactively: hands over to the plugin's walk when it has one, otherwise steps through its commands\",\n )\n .action((id: string) =>\n run(async () => {\n assertInteractive(\"`fluid setup show <id>` and run each step's command\");\n const guide = await getSetupGuide(id);\n const root = program ?? setup.parent;\n if (!root) throw new Error(\"fluid setup run needs the fluid program.\");\n const result = await runGuide(guide, {\n prompter: terminalPrompter(),\n runCommand: async (args) => {\n // Commands report failure by throwing or by setting the exit\n // code; check both, and leave the code clean for the next step.\n const before = process.exitCode;\n process.exitCode = undefined;\n let ok = true;\n try {\n await root.parseAsync(args, { from: \"user\" });\n ok = !process.exitCode;\n } catch (error) {\n ok = false;\n process.stderr.write(\n JSON.stringify({\n error: error instanceof Error ? error.message : String(error),\n }) + \"\\n\",\n );\n } finally {\n process.exitCode = before;\n }\n return ok;\n },\n hasCommand: (name) => root.commands.some((c) => c.name() === name),\n });\n print(result);\n // A failed command or walk fails `setup run` too, so a script\n // checking the exit status sees it.\n if (result.failed.length > 0) process.exitCode = 1;\n }),\n );\n\n return setup;\n}\n\nconst plugin: FluidPlugin = {\n name: \"@fluid-app/fluid-cli-setup\",\n version: \"0.1.0\",\n register(ctx) {\n ctx.program.addCommand(createSetupCommand(ctx.program));\n },\n};\n\nexport default plugin;\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@fluid-app/fluid-cli-setup",
3
+ "version": "0.1.0",
4
+ "description": "Fluid CLI plugin that lists and shows the guided setups published on docs.fluid.app",
5
+ "files": [
6
+ "dist",
7
+ "README.md"
8
+ ],
9
+ "type": "module",
10
+ "main": "./dist/index.mjs",
11
+ "types": "./dist/index.d.mts",
12
+ "exports": {
13
+ ".": {
14
+ "import": {
15
+ "types": "./dist/index.d.mts",
16
+ "default": "./dist/index.mjs"
17
+ }
18
+ }
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "dependencies": {
24
+ "@fluid-app/fluid-cli": "0.1.35",
25
+ "commander": "^12.0.0",
26
+ "yaml": "^2.8.2"
27
+ },
28
+ "devDependencies": {
29
+ "@fluid-app/typescript-config": "0.0.0",
30
+ "@swc/core": "^1.15.18",
31
+ "@swc/jest": "^0.2.39",
32
+ "@types/jest": "^29.5.14",
33
+ "@types/node": "24.10.12",
34
+ "jest": "^29.7.0",
35
+ "tsdown": "^0.21.0",
36
+ "typescript": "^5"
37
+ },
38
+ "engines": {
39
+ "node": ">=24.0.0"
40
+ },
41
+ "nx": {
42
+ "targets": {
43
+ "typecheck": {
44
+ "dependsOn": [
45
+ {
46
+ "target": "build",
47
+ "projects": [
48
+ "@fluid-app/fluid-cli"
49
+ ]
50
+ }
51
+ ]
52
+ },
53
+ "test": {
54
+ "dependsOn": [
55
+ {
56
+ "target": "build",
57
+ "projects": [
58
+ "@fluid-app/fluid-cli"
59
+ ]
60
+ }
61
+ ]
62
+ },
63
+ "build": {
64
+ "dependsOn": [
65
+ "typecheck",
66
+ {
67
+ "target": "build",
68
+ "projects": [
69
+ "@fluid-app/fluid-cli"
70
+ ]
71
+ }
72
+ ]
73
+ }
74
+ }
75
+ },
76
+ "scripts": {
77
+ "build": "tsdown",
78
+ "dev": "tsdown --watch",
79
+ "typecheck": "tsgo --noEmit",
80
+ "lint": "oxlint",
81
+ "test": "jest"
82
+ }
83
+ }