@flisk-sh/flisk 1.3.205 → 1.3.206

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 (48) hide show
  1. package/cli/dist/cli.js +230 -92
  2. package/cli/dist/commands/doctor.js +29 -15
  3. package/cli/dist/commands/project.js +104 -58
  4. package/cli/dist/commands/up.js +8 -5
  5. package/cli/dist/generator/template-engine.js +17 -9
  6. package/cli/dist/utils/cli-command.js +3 -1
  7. package/cli/dist/utils/demo-stage.js +315 -0
  8. package/cli/dist/utils/native-windows.js +20 -0
  9. package/cli/dist/utils/one-command.js +15 -8
  10. package/cli/dist-bundle/chunk-3QQGL3NQ.js +35 -0
  11. package/cli/dist-bundle/chunk-4W6OXDLK.js +17 -0
  12. package/cli/dist-bundle/chunk-53M5D4KE.js +2 -0
  13. package/cli/dist-bundle/{chunk-4TMLNLYG.js → chunk-5CFVN3W3.js} +1 -1
  14. package/cli/dist-bundle/{chunk-P2MFLNY2.js → chunk-6Y6G7PSD.js} +1 -1
  15. package/cli/dist-bundle/chunk-7C5VA6CW.js +57 -0
  16. package/cli/dist-bundle/chunk-D2BXXJDM.js +7 -0
  17. package/cli/dist-bundle/chunk-GANRXKBM.js +2 -0
  18. package/cli/dist-bundle/chunk-GOEBYLFN.js +2 -0
  19. package/cli/dist-bundle/chunk-LI3BXAX5.js +2 -0
  20. package/cli/dist-bundle/{chunk-BHQDQQYQ.js → chunk-NCEK46NX.js} +10 -10
  21. package/cli/dist-bundle/chunk-ODVBKN6S.js +2 -0
  22. package/cli/dist-bundle/{chunk-5G7E6CO2.js → chunk-PHI5AT3H.js} +2 -2
  23. package/cli/dist-bundle/chunk-XSPMLWAE.js +10 -0
  24. package/cli/dist-bundle/cli.js +265 -263
  25. package/cli/dist-bundle/cold-preflight-4I24DQLW.js +2 -0
  26. package/cli/dist-bundle/demo-stage-EOMANLAD.js +7 -0
  27. package/cli/dist-bundle/native-windows-X5XVLFJS.js +2 -0
  28. package/cli/dist-bundle/one-command-P6XVAXWV.js +5 -0
  29. package/cli/dist-bundle/open-6374RNL6.js +2 -0
  30. package/cli/dist-bundle/project-IRWFRXWA.js +2 -0
  31. package/cli/dist-bundle/services-VIIBMWD4.js +2 -0
  32. package/cli/dist-bundle/tilt-port-owner-AN3ITRTR.js +2 -0
  33. package/cli/dist-bundle/url-actions-D6ROTHZG.js +2 -0
  34. package/cli/package.json +1 -1
  35. package/package.json +1 -1
  36. package/cli/dist-bundle/chunk-67M3DX6J.js +0 -34
  37. package/cli/dist-bundle/chunk-CF46CD6R.js +0 -6
  38. package/cli/dist-bundle/chunk-CLQSJP6M.js +0 -21
  39. package/cli/dist-bundle/chunk-DTIHDMED.js +0 -5
  40. package/cli/dist-bundle/chunk-IQNUVQH5.js +0 -52
  41. package/cli/dist-bundle/chunk-WVSI4XL5.js +0 -4
  42. package/cli/dist-bundle/chunk-YNJTA77W.js +0 -2
  43. package/cli/dist-bundle/cold-preflight-R53YXYOC.js +0 -2
  44. package/cli/dist-bundle/one-command-YP6L6MMM.js +0 -5
  45. package/cli/dist-bundle/open-PKNOQQXA.js +0 -2
  46. package/cli/dist-bundle/project-I5QVEGNB.js +0 -2
  47. package/cli/dist-bundle/spinner-YLB6FYQH.js +0 -2
  48. package/cli/dist-bundle/tilt-port-owner-Y5LHXWPS.js +0 -2
package/cli/dist/cli.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // Copyright (c) 2026 Flisk Landscape contributors
3
3
  // SPDX-License-Identifier: MIT
4
- import { existsSync, readdirSync, rmSync } from "node:fs";
4
+ import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
6
6
  import { isAbsolute, join, resolve } from "node:path";
7
7
  import { cwd } from "node:process";
8
8
  import chalk from "chalk";
9
+ import pkg from "../package.json" with { type: "json" };
9
10
  import { showHelp } from "./commands/help.js";
10
11
  import { createProgram } from "./program.js";
11
12
  import { ensureGlobalInstall } from "./utils/cli-command.js";
@@ -17,11 +18,10 @@ const program = createProgram();
17
18
  if (process.argv.length === 2) {
18
19
  const result = await runColdPreflight();
19
20
  console.log(formatColdPreflight(result));
20
- await ensureGlobalInstall();
21
21
  console.log("\nCommands: flisk doctor | flisk project --yes | flisk --help");
22
- console.log("\nOne command from scratch (installs, clones, boots and opens the browser):");
23
- console.log(" npx @flisk-sh/flisk demo");
24
- console.log(" npx @flisk-sh/flisk restaurant --project=my-stack");
22
+ console.log("\nOne command from scratch (clones, boots and opens the browser):");
23
+ console.log(` npx -y @flisk-sh/flisk@latest demo`);
24
+ console.log(` npx -y @flisk-sh/flisk@latest restaurant --project=my-stack`);
25
25
  process.exit(result.ok ? 0 : 1);
26
26
  }
27
27
  // One command from scratch: flisk <template> | flisk --template <name> --project <dir>
@@ -29,15 +29,28 @@ if (process.argv.length === 2) {
29
29
  const templateArg = readOptionValue(process.argv, "--template") ?? bareTemplateArg(process.argv);
30
30
  const projectArg = readOptionValue(process.argv, "--project");
31
31
  if (templateArg) {
32
+ // Native Windows cannot boot a stack, so the one-command flow refuses before it clones a
33
+ // template, installs the CLI globally or writes into the home directory.
34
+ const { nativeWindowsStackRefusal } = await import("./utils/native-windows.js");
35
+ const platformRefusal = nativeWindowsStackRefusal(process.platform, process.env.TDK_ALLOW_NATIVE_WINDOWS);
36
+ if (platformRefusal) {
37
+ console.error(chalk.red(`❌ ${platformRefusal}`));
38
+ console.error(chalk.gray(" Run it from Ubuntu in WSL2. Guide: docs/wsl2.md"));
39
+ process.exit(1);
40
+ }
32
41
  const { cloneProjectTemplate, templateTargetDir } = await import("./commands/project.js");
33
- const { assertMachineReadyOrExit, detectRuntimePrerequisites, formatRuntimeNextSteps } = await import("./utils/cold-preflight.js");
42
+ const { assertMachineReadyOrExit } = await import("./utils/cold-preflight.js");
34
43
  await assertMachineReadyOrExit({ scope: "scaffold" });
35
- const runtimeNextSteps = formatRuntimeNextSteps(detectRuntimePrerequisites());
36
44
  // A second demo in the same folder gets its own directory instead of failing.
37
45
  const { nextAvailableProjectDir } = await import("./utils/one-command.js");
38
46
  const { PROJECT_TEMPLATES } = await import("./utils/project-templates.js");
47
+ const { listFrontendTargets, pickReadyFrontend } = await import("./commands/open.js");
48
+ const { discoverResourcesWithProblems } = await import("./utils/services.js");
49
+ const { openUrl } = await import("./utils/url-actions.js");
50
+ const { createDemoStage } = await import("./utils/demo-stage.js");
39
51
  const baseName = templateTargetDir(templateArg, projectArg);
40
52
  const fixedDir = projectArg ? undefined : PROJECT_TEMPLATES[templateArg]?.fixedDir;
53
+ const keep = process.argv.includes("--keep");
41
54
  let targetName;
42
55
  if (fixedDir) {
43
56
  // A template with a fixed directory always lands in the same place, so a demo never
@@ -54,113 +67,183 @@ if (templateArg) {
54
67
  }
55
68
  }
56
69
  const destination = isAbsolute(targetName) ? targetName : resolve(cwd(), targetName);
70
+ // Reusing an existing checkout is what keeps a second take free: the clone and the
71
+ // dependency install are skipped, so the recording starts on the interesting part.
72
+ let reused = false;
57
73
  if (fixedDir && existsSync(destination)) {
58
- // The demo owns this directory, so a second run starts from a fresh copy of it.
59
- if (existsSync(join(destination, ".tdk", "project.json"))) {
60
- console.log(chalk.gray(`Refreshing the demo in ${destination}...`));
61
- rmSync(destination, { recursive: true, force: true });
62
- }
63
- else {
74
+ if (!existsSync(join(destination, ".tdk", "project.json"))) {
64
75
  console.log(chalk.yellow(`${destination} already exists and is not a Flisk project.`));
65
76
  console.log(chalk.gray(" Remove it, or use --project <dir> to put the demo somewhere else."));
66
77
  process.exit(1);
67
78
  }
68
- }
69
- await cloneProjectTemplate(templateArg, targetName, runtimeNextSteps);
70
- const install = ensureGlobalInstall();
71
- if (install === "installed") {
72
- console.log("\n\u2714 flisk is installed globally, so `flisk` works from any folder.");
73
- }
74
- const binPath = process.argv[1];
75
- if (binPath && existsSync(destination)) {
76
- const { initializeProject, installServiceDependencies, runStackQuiet } = await import("./utils/one-command.js");
77
- const { openFirstFrontend } = await import("./commands/open.js");
78
- const { startSpinner } = await import("./utils/spinner.js");
79
- // Docker on Colima is only usable while the VM is up, and a stopped VM looks like a
80
- // broken Docker daemon. Say so before the long wait instead of after it.
81
- const { checkColima } = await import("./utils/colima.js");
82
- const colima = checkColima();
83
- if (colima.isColima && !colima.running) {
84
- console.log(chalk.yellow(colima.problem ?? "The Colima VM is not running."));
85
- console.log(chalk.gray(` Fix: ${colima.fix ?? "colima start"}`));
86
- process.exit(1);
87
- }
88
79
  // A stack for this project may already be running (the same example in another folder
89
80
  // shares its compose project). Then there is nothing to start: open what is up.
90
- const { findReadyFrontend } = await import("./commands/open.js");
91
- const running = await findReadyFrontend(destination, 0);
81
+ const running = await pickReadyFrontend(await listFrontendTargets(destination), 0);
92
82
  if (running) {
93
- console.log(chalk.gray(`A stack for this project is already running (${running.name}).`));
94
- const { openFirstFrontend } = await import("./commands/open.js");
95
- await openFirstFrontend(destination);
83
+ console.log(chalk.gray(`The demo is already running: ${running.url}`));
84
+ await openUrl(running.url);
96
85
  process.exit(0);
97
86
  }
98
- // A cloned starter has no project configuration yet, and `flisk up` refuses to run
99
- // without one, so the project is set up before the stack starts.
100
- const setupSpinner = startSpinner("Setting up the project...");
101
- const setup = await initializeProject(binPath, destination);
102
- if (!setup.ok) {
103
- setupSpinner.fail("The project setup failed.");
104
- console.log(chalk.yellow(setup.error ?? "flisk project --yes did not finish."));
105
- process.exit(1);
106
- }
107
- setupSpinner.succeed("The project is ready.");
108
- // A fresh clone has no host dependencies, and the stack typechecks the services on the
109
- // host, so install them before the stack starts.
110
- const installSpinner = startSpinner("Installing the service dependencies...");
111
- const installs = await installServiceDependencies(destination);
112
- const failed = installs.filter((entry) => !entry.ok);
113
- if (failed.length > 0) {
114
- installSpinner.fail("Some services could not install their dependencies.");
115
- for (const entry of failed) {
116
- console.log(chalk.yellow(` - ${entry.dir}: run "bun install" there`));
117
- }
87
+ if (keep) {
88
+ reused = true;
118
89
  }
119
90
  else {
120
- installSpinner.succeed("The service dependencies are installed.");
91
+ rmSync(destination, { recursive: true, force: true });
121
92
  }
122
- const spinner = startSpinner("Starting the stack (the first build takes a few minutes)...");
123
- const started = Date.now();
124
- // Show what the stack is doing instead of a silent spinner: the resource states, the
125
- // elapsed time and where to watch the live logs. The Tilt port is not fixed (another
126
- // project may hold the default), so the ports that answer are looked at.
127
- const { readStackProgress, formatProgress } = await import("./utils/stack-progress.js");
128
- const { classifyTiltPorts } = await import("./utils/tilt-port-owner.js");
129
- const candidates = Array.from({ length: 10 }, (_, index) => 10350 + index);
130
- let watching = true;
131
- void (async () => {
132
- while (watching) {
93
+ }
94
+ // Docker on Colima is only usable while the VM is up, and a stopped VM looks like a
95
+ // broken Docker daemon. Say so before the long wait instead of after it.
96
+ const { checkColima } = await import("./utils/colima.js");
97
+ const colima = checkColima();
98
+ if (colima.isColima && !colima.running) {
99
+ console.log(chalk.yellow(colima.problem ?? "The Colima VM is not running."));
100
+ console.log(chalk.gray(` Fix: ${colima.fix ?? "colima start"}`));
101
+ process.exit(1);
102
+ }
103
+ let phases = [
104
+ {
105
+ id: "fetch",
106
+ label: reused ? "Reusing the demo" : "Fetching the demo",
107
+ status: "active",
108
+ },
109
+ { id: "config", label: "Generating project config", status: "pending" },
110
+ { id: "deps", label: "Installing dependencies", status: "pending" },
111
+ { id: "start", label: "Starting services", status: "pending" },
112
+ ];
113
+ const stage = createDemoStage({
114
+ title: "Flisk demo",
115
+ subtitle: `${templateArg} starter`,
116
+ phases,
117
+ servicesReady: 0,
118
+ servicesTotal: 0,
119
+ servicesBuilding: 0,
120
+ elapsedMs: 0,
121
+ });
122
+ const setPhase = (id, status, elapsedMs) => {
123
+ phases = phases.map((phase) => phase.id === id ? { ...phase, status, elapsedMs: elapsedMs ?? phase.elapsedMs } : phase);
124
+ stage.update({ phases });
125
+ };
126
+ const logLines = [`flisk ${pkg.version} ${templateArg}`, `cwd: ${destination}`];
127
+ // 1. Clone (or reuse) and install the CLI. Both are silent so the frame on screen is the
128
+ // only thing that moves.
129
+ const fetchStarted = Date.now();
130
+ try {
131
+ if (!reused) {
132
+ await cloneProjectTemplate(templateArg, targetName, [], { quiet: true });
133
+ }
134
+ }
135
+ catch (error) {
136
+ stage.fail(error instanceof Error ? error.message : String(error), "Check that git is installed and that you are online, then run the command again.");
137
+ stage.stop();
138
+ process.exit(1);
139
+ }
140
+ const install = ensureGlobalInstall();
141
+ setPhase("fetch", "done", Date.now() - fetchStarted);
142
+ const binPath = process.argv[1];
143
+ if (!binPath || !existsSync(destination)) {
144
+ stage.fail("The demo could not find the CLI entry point or the project folder.", "Reinstall the Flisk CLI package and run the command again.");
145
+ stage.stop();
146
+ process.exit(1);
147
+ }
148
+ const { initializeProject, installServiceDependencies, runStackQuiet } = await import("./utils/one-command.js");
149
+ // 2. A cloned starter has no project configuration yet, and `flisk up` refuses to run
150
+ // without one, so the project is set up before the stack starts.
151
+ const configStarted = Date.now();
152
+ const setup = await initializeProject(binPath, destination);
153
+ logLines.push("--- project setup ---", setup.output ?? "");
154
+ if (!setup.ok) {
155
+ const logPath = writeLog(logLines);
156
+ stage.fail(setup.error ?? "The project setup did not finish.", "Run `flisk project --yes` in the project folder to see what it printed.", logPath);
157
+ stage.stop();
158
+ process.exit(1);
159
+ }
160
+ setPhase("config", "done", Date.now() - configStarted);
161
+ // The true service count comes from the project on disk, so the frame says "0 of 6"
162
+ // while the stack is starting instead of "no services found".
163
+ const discovered = discoverResourcesWithProblems(destination).resources.length;
164
+ stage.update({ servicesTotal: discovered });
165
+ // 3. A fresh clone has no host dependencies, and the stack typechecks the services on the
166
+ // host, so install them before the stack starts.
167
+ const depsStarted = Date.now();
168
+ const installs = await installServiceDependencies(destination);
169
+ const failedInstalls = installs.filter((entry) => !entry.ok);
170
+ for (const entry of installs)
171
+ logLines.push(`install ${entry.dir}: ${entry.ok ? "ok" : "failed"}`);
172
+ if (failedInstalls.length > 0) {
173
+ stage.fail(`${failedInstalls.length} of ${installs.length} services could not install their dependencies.`, `Run "bun install" in ${failedInstalls[0]?.dir ?? "the failed services"}.`, writeLog(logLines));
174
+ stage.stop();
175
+ process.exit(1);
176
+ }
177
+ setPhase("deps", "done", Date.now() - depsStarted);
178
+ // 4. Start the stack and report what Tilt is doing. The Tilt port is not fixed (another
179
+ // project may hold the default), so the ports that answer are looked at.
180
+ const startStarted = Date.now();
181
+ setPhase("start", "active");
182
+ const { readStackProgress } = await import("./utils/stack-progress.js");
183
+ const { classifyTiltPorts } = await import("./utils/tilt-port-owner.js");
184
+ const candidates = Array.from({ length: 10 }, (_, index) => 10350 + index);
185
+ let watching = true;
186
+ // Tilt counts every resource it starts, including the database and the proxy. That is the
187
+ // number the progress line shows, so the last frame has to finish on the same one.
188
+ let lastTotal = discovered;
189
+ void (async () => {
190
+ while (watching) {
191
+ try {
133
192
  const { running } = await classifyTiltPorts(candidates, destination, async (port) => (await readStackProgress(port)) !== undefined);
134
193
  for (const port of running) {
135
194
  const progress = await readStackProgress(port);
136
195
  if (progress) {
137
- spinner.text = formatProgress({ ...progress, elapsed: (Date.now() - started) / 1000 });
196
+ lastTotal = progress.total;
197
+ stage.update({
198
+ servicesReady: progress.ready,
199
+ servicesTotal: progress.total,
200
+ servicesBuilding: progress.building,
201
+ });
138
202
  break;
139
203
  }
140
204
  }
141
- await new Promise((resolve) => setTimeout(resolve, 2000));
142
205
  }
143
- })();
144
- const outcome = await runStackQuiet(binPath, destination);
145
- watching = false;
146
- const tiltUiUrl = outcome.data?.tiltUrl ?? "http://localhost:10350";
147
- if (!outcome.ok) {
148
- spinner.fail("The stack did not come up.");
149
- console.log(chalk.yellow(outcome.error ?? "The stack stopped before it was ready."));
150
- console.log(chalk.gray("Run `flisk up` in that folder to see the full output."));
151
- process.exit(1);
152
- }
153
- spinner.succeed("The stack is up.");
154
- console.log(chalk.gray(` Tilt UI (live build logs): ${tiltUiUrl}`));
155
- console.log(chalk.gray(` Container logs: docker compose logs -f (in ${destination})`));
156
- // The browser only opens when a frontend actually answers, the same way `flisk open` does.
157
- const opened = await openFirstFrontend(destination).catch((error) => {
158
- console.log(chalk.yellow(`Could not open a browser: ${error instanceof Error ? error.message : String(error)}`));
159
- return null;
160
- });
161
- if (!opened) {
162
- console.log(chalk.gray(`The stack is running. Open it yourself: cd ${destination} && flisk open`));
206
+ catch {
207
+ // A port that closes mid-read is not a demo failure; the next pass picks it up.
208
+ }
209
+ await new Promise((resolve) => {
210
+ // Never keep the process alive just because the watcher is between passes.
211
+ const timer = setTimeout(resolve, 2000);
212
+ timer.unref?.();
213
+ });
163
214
  }
215
+ })();
216
+ const outcome = await runStackQuiet(binPath, destination);
217
+ watching = false;
218
+ logLines.push("--- stack ---", outcome.raw ?? "");
219
+ if (!outcome.ok) {
220
+ const tiltUiUrl = outcome.data?.tiltUrl ?? "http://localhost:10350";
221
+ stage.fail(firstMachineFailure(outcome.raw) ?? outcome.error ?? "The stack stopped before it was ready.", `Run \`flisk up\` in the project folder, or watch ${tiltUiUrl}.`, writeLog(logLines));
222
+ stage.stop();
223
+ process.exit(1);
224
+ }
225
+ setPhase("start", "done", Date.now() - startStarted);
226
+ stage.update({ servicesReady: lastTotal, servicesTotal: lastTotal });
227
+ // 5. Open the browser only when a frontend actually answers, the same way `flisk open`
228
+ // does. A failed open is not a failed demo: the URL is already on screen.
229
+ const targets = await listFrontendTargets(destination);
230
+ const first = await pickReadyFrontend(targets, 30_000);
231
+ if (first)
232
+ await openUrl(first.url);
233
+ const generated = countFiles(join(destination, ".tdk"));
234
+ const wired = countFiles(destination, ["node_modules", ".git"]);
235
+ stage.update({ subtitle: `${templateArg} starter \u00b7 ${countLabel(wired, "file")} wired` });
236
+ stage.finish({
237
+ urls: (first ? [first.url, ...targets.map((target) => target.url)] : targets.map((t) => t.url))
238
+ .filter((url, index, all) => all.indexOf(url) === index)
239
+ .slice(0, 2),
240
+ summary: `${countLabel(generated, "file")} generated \u00b7 ${countLabel(discovered, "app service")} \u00b7 live reload`,
241
+ hint: "Edit any file and the browser updates.",
242
+ version: pkg.version,
243
+ });
244
+ stage.stop();
245
+ if (install === "failed") {
246
+ console.log(chalk.gray("\n `flisk` is not on your PATH. Next time, run this command again with npx."));
164
247
  }
165
248
  process.exit(0);
166
249
  }
@@ -190,3 +273,58 @@ function readOptionValue(args, name) {
190
273
  }
191
274
  return undefined;
192
275
  }
276
+ /** `1 file`, `240 files`. The frame is read by people, so the count has to agree with itself. */
277
+ function countLabel(count, noun) {
278
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
279
+ }
280
+ /** Counts the files under `root`, skipping the directories in `skip`. */
281
+ function countFiles(root, skip = []) {
282
+ let total = 0;
283
+ const walk = (dir) => {
284
+ try {
285
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
286
+ if (skip.includes(entry.name))
287
+ continue;
288
+ const path = join(dir, entry.name);
289
+ if (entry.isDirectory())
290
+ walk(path);
291
+ else if (entry.isFile())
292
+ total += 1;
293
+ }
294
+ }
295
+ catch {
296
+ // An unreadable directory must not cost the demo its final frame.
297
+ }
298
+ };
299
+ walk(root);
300
+ return total;
301
+ }
302
+ /**
303
+ * The first machine check that failed in the stack output.
304
+ *
305
+ * `up` reports "exited with code 1" when a check fails before Tilt starts, which says
306
+ * nothing. The check line above it says what is actually missing.
307
+ */
308
+ function firstMachineFailure(raw) {
309
+ if (!raw)
310
+ return undefined;
311
+ for (const line of raw.split("\n")) {
312
+ const match = /^\s*FAIL\s+(.+)$/.exec(line);
313
+ if (match?.[1])
314
+ return match[1].trim();
315
+ }
316
+ return undefined;
317
+ }
318
+ /** Writes the child output somewhere a failed run can be inspected. */
319
+ function writeLog(lines) {
320
+ try {
321
+ const dir = join(homedir(), ".flisk", "logs");
322
+ mkdirSync(dir, { recursive: true });
323
+ const path = join(dir, `demo-${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
324
+ writeFileSync(path, lines.join("\n"));
325
+ return path;
326
+ }
327
+ catch {
328
+ return undefined;
329
+ }
330
+ }
@@ -33,6 +33,7 @@ import { discoverResourcesFromRoot } from "../utils/services.js";
33
33
  import { buildSmokePlans, readOnlySmokePlans, runSmokePlans, safeSmokeDeps, } from "../utils/smoke.js";
34
34
  import { evaluateTdkVersionFloor } from "../utils/tdk-version.js";
35
35
  import { findOnPath } from "../utils/which.js";
36
+ import { checkWindowsDockerMode, checkWindowsHostConfiguration, checkWindowsRuntimeTools, } from "../utils/windows-doctor.js";
36
37
  export { checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, summarizeServiceProbes, summarizeTiltBuildError, } from "../utils/doctor-runtime.js";
37
38
  export { checkStarlarkLoadExports };
38
39
  // A wedged Docker daemon makes `docker ps` block forever instead of failing,
@@ -1227,22 +1228,35 @@ export const doctorCommand = new Command("doctor")
1227
1228
  }
1228
1229
  const host = detectHost();
1229
1230
  if (process.platform === "win32") {
1231
+ const nativeWindowsFinding = {
1232
+ name: "Native Windows support",
1233
+ didPass: false,
1234
+ message: NATIVE_WINDOWS_DOCTOR_MESSAGE,
1235
+ fix: "Use WSL2 Ubuntu with Docker Desktop integration. Guide: docs/wsl2.md",
1236
+ };
1237
+ // Keep Windows diagnostics behind the win32 platform check (Linux/macOS unchanged).
1238
+ const windowsChecks = [
1239
+ nativeWindowsFinding,
1240
+ checkWindowsRuntimeTools(),
1241
+ checkWindowsDockerMode(),
1242
+ await checkWindowsHostConfiguration(),
1243
+ ];
1244
+ let windowsPortPlan = null;
1245
+ try {
1246
+ windowsPortPlan = await createHostPortPlan();
1247
+ }
1248
+ catch {
1249
+ /* The native-Windows platform finding remains the primary failure. */
1250
+ }
1230
1251
  if (options.json) {
1231
- let windowsPortPlan = null;
1232
- try {
1233
- windowsPortPlan = await createHostPortPlan();
1234
- }
1235
- catch {
1236
- /* The native-Windows platform finding remains the primary failure. */
1237
- }
1238
- console.log(JSON.stringify(createDoctorReport([
1239
- {
1240
- name: "Native Windows support",
1241
- didPass: false,
1242
- message: NATIVE_WINDOWS_DOCTOR_MESSAGE,
1243
- fix: "Use WSL2 Ubuntu with Docker Desktop integration. Guide: docs/wsl2.md",
1244
- },
1245
- ], Boolean(findProjectRoot()), [], windowsPortPlan, { ...host, canUp: false })));
1252
+ console.log(JSON.stringify(createDoctorReport(windowsChecks, Boolean(findProjectRoot()), [], windowsPortPlan, {
1253
+ ...host,
1254
+ canUp: false,
1255
+ })));
1256
+ }
1257
+ else {
1258
+ printDoctorResults(windowsChecks);
1259
+ console.log("");
1246
1260
  }
1247
1261
  console.error(NATIVE_WINDOWS_DOCTOR_MESSAGE);
1248
1262
  process.exit(1);