@flisk-sh/flisk 1.3.204 → 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 (64) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/cli/dist/cli.js +251 -90
  3. package/cli/dist/commands/doctor.js +29 -15
  4. package/cli/dist/commands/project.js +108 -60
  5. package/cli/dist/commands/resource.js +3 -1
  6. package/cli/dist/commands/up.js +8 -5
  7. package/cli/dist/generator/template-engine.js +17 -9
  8. package/cli/dist/utils/cli-command.js +3 -1
  9. package/cli/dist/utils/colima.js +23 -4
  10. package/cli/dist/utils/demo-stage.js +315 -0
  11. package/cli/dist/utils/doctor-fix.js +35 -9
  12. package/cli/dist/utils/machine-output.js +27 -0
  13. package/cli/dist/utils/native-windows.js +20 -0
  14. package/cli/dist/utils/one-command.js +15 -8
  15. package/cli/dist/utils/project-templates.js +1 -0
  16. package/cli/dist-bundle/chunk-3QQGL3NQ.js +35 -0
  17. package/cli/dist-bundle/chunk-4W6OXDLK.js +17 -0
  18. package/cli/dist-bundle/chunk-53M5D4KE.js +2 -0
  19. package/cli/dist-bundle/{chunk-BS6G2SLM.js → chunk-5CFVN3W3.js} +1 -1
  20. package/cli/dist-bundle/{chunk-RFHMXJTK.js → chunk-6Y6G7PSD.js} +1 -1
  21. package/cli/dist-bundle/chunk-7C5VA6CW.js +57 -0
  22. package/cli/dist-bundle/chunk-D2BXXJDM.js +7 -0
  23. package/cli/dist-bundle/chunk-GANRXKBM.js +2 -0
  24. package/cli/dist-bundle/chunk-GIMHTZNR.js +2 -0
  25. package/cli/dist-bundle/chunk-GOEBYLFN.js +2 -0
  26. package/cli/dist-bundle/chunk-LI3BXAX5.js +2 -0
  27. package/cli/dist-bundle/{chunk-WMF4AFAE.js → chunk-NCEK46NX.js} +10 -10
  28. package/cli/dist-bundle/chunk-ODVBKN6S.js +2 -0
  29. package/cli/dist-bundle/{chunk-Z6UD7UZH.js → chunk-PHI5AT3H.js} +2 -2
  30. package/cli/dist-bundle/chunk-XSPMLWAE.js +10 -0
  31. package/cli/dist-bundle/chunk-YZRPX76N.js +2 -0
  32. package/cli/dist-bundle/cli.js +269 -267
  33. package/cli/dist-bundle/cold-preflight-4I24DQLW.js +2 -0
  34. package/cli/dist-bundle/{colima-4ID4BGHC.js → colima-SVZX2UML.js} +1 -1
  35. package/cli/dist-bundle/demo-stage-EOMANLAD.js +7 -0
  36. package/cli/dist-bundle/native-windows-X5XVLFJS.js +2 -0
  37. package/cli/dist-bundle/one-command-P6XVAXWV.js +5 -0
  38. package/cli/dist-bundle/open-6374RNL6.js +2 -0
  39. package/cli/dist-bundle/project-IRWFRXWA.js +2 -0
  40. package/cli/dist-bundle/project-templates-JDRE7AVL.js +2 -0
  41. package/cli/dist-bundle/services-VIIBMWD4.js +2 -0
  42. package/cli/dist-bundle/tilt-port-owner-AN3ITRTR.js +2 -0
  43. package/cli/dist-bundle/url-actions-D6ROTHZG.js +2 -0
  44. package/cli/package.json +4 -4
  45. package/engine/assets/docker/wake-gateway/Dockerfile +1 -1
  46. package/engine/topologies/platform/docker/compose/compose.star +20 -18
  47. package/engine/topologies/platform/docker/compose/unsafe_value.star +23 -0
  48. package/engine/topologies/platform/docker/networking/traefik.star +19 -0
  49. package/package.json +2 -2
  50. package/cli/dist-bundle/chunk-CBFVK3TF.js +0 -21
  51. package/cli/dist-bundle/chunk-CF46CD6R.js +0 -6
  52. package/cli/dist-bundle/chunk-DTIHDMED.js +0 -5
  53. package/cli/dist-bundle/chunk-EPBYRJE3.js +0 -2
  54. package/cli/dist-bundle/chunk-GD5676UE.js +0 -52
  55. package/cli/dist-bundle/chunk-MKJNQTIN.js +0 -2
  56. package/cli/dist-bundle/chunk-QSPFSZCS.js +0 -34
  57. package/cli/dist-bundle/chunk-ZVGY7COV.js +0 -4
  58. package/cli/dist-bundle/cold-preflight-FXOZ5SR5.js +0 -2
  59. package/cli/dist-bundle/one-command-YP6L6MMM.js +0 -5
  60. package/cli/dist-bundle/open-UQ6DF3XN.js +0 -2
  61. package/cli/dist-bundle/project-GTYHCCFG.js +0 -2
  62. package/cli/dist-bundle/spinner-YLB6FYQH.js +0 -2
  63. package/cli/dist-bundle/tilt-port-owner-KLNX2ZI2.js +0 -2
  64. /package/cli/dist-bundle/{devtools-ZUHXW33W.js → devtools-MGITLOWZ.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  Notable changes to the `flisk` CLI. Versions match [npm](https://www.npmjs.com/package/@flisk-sh/flisk?activeTab=versions) and the [binary releases](https://github.com/flisk-sh/releases/releases).
4
4
 
5
+ ## 1.3.195 (2026-10-21)
6
+
7
+ - `flisk demo` and `flisk restaurant` now clone the demo template into a fixed directory (`~/.flisk/flisk-demo/`) instead of creating `tdk-restaurant-example-N` directories in the current workspace. Previous runs are cleaned up automatically. The `restaurant` template still creates a named directory in the current folder when used explicitly.
8
+
9
+ ## 1.3.194 (2026-10-21)
10
+
11
+ - `flisk open` now opens the frontend of the project that was started, not the current working directory. The one-command flow (`npx @flisk-sh/flisk demo`) boots the cloned project and opens its frontend automatically.
12
+
13
+ ## 1.3.193 (2026-10-21)
14
+
15
+ - New `flisk open` command: opens the first ready frontend in the browser. Works with `--json` output and integrates with the one-command flow.
16
+
17
+ ## 1.3.192 (2026-10-21)
18
+
19
+ - Fixed the one-command flow (`npx @flisk-sh/flisk demo`): the cloned project now boots in place and the browser opens when the stack is up. The CLI copy was updated from "tdk" to "Flisk" throughout.
20
+
21
+ ## 1.3.191 (2026-10-21)
22
+
23
+ - One command from scratch: `npx @flisk-sh/flisk demo` (or `restaurant`, `saas`, `erp`, etc.) clones a starter, installs the CLI globally, boots the stack, and opens the frontend.
24
+ - CLI rebrand: all user-facing copy changed from "tdk" / "TDK" to "Flisk" / "flisk" (package scope, homepage, repository, command names, error messages, docs).
25
+ - Internal: `cli/src/utils/one-command.ts` added to run the stack and detect the frontend URL.
26
+
27
+ ## 1.3.190 (2026-10-21)
28
+
29
+ - Package metadata corrected: `repository`, `bugs`, and `homepage` in `package.json` now point at `flisk-sh/flisk` and `https://flisk.sh` (were still pointing at the decommissioned `tdk-landscape/tdk-cli-core` and `tdk-landscape.github.io`).
30
+
5
31
  ## 1.3.189 (2026-10-21)
6
32
 
7
33
  - `flisk resource` now reports a malformed `service.json` with the file path and the reason it could not be parsed, instead of a raw JSON error (#1685).
package/cli/dist/cli.js CHANGED
@@ -1,10 +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 } from "node:fs";
5
- import { resolve } from "node:path";
4
+ import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { isAbsolute, join, resolve } from "node:path";
6
7
  import { cwd } from "node:process";
7
8
  import chalk from "chalk";
9
+ import pkg from "../package.json" with { type: "json" };
8
10
  import { showHelp } from "./commands/help.js";
9
11
  import { createProgram } from "./program.js";
10
12
  import { ensureGlobalInstall } from "./utils/cli-command.js";
@@ -16,11 +18,10 @@ const program = createProgram();
16
18
  if (process.argv.length === 2) {
17
19
  const result = await runColdPreflight();
18
20
  console.log(formatColdPreflight(result));
19
- await ensureGlobalInstall();
20
21
  console.log("\nCommands: flisk doctor | flisk project --yes | flisk --help");
21
- console.log("\nOne command from scratch (installs, clones, boots and opens the browser):");
22
- console.log(" npx @flisk-sh/flisk demo");
23
- 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`);
24
25
  process.exit(result.ok ? 0 : 1);
25
26
  }
26
27
  // One command from scratch: flisk <template> | flisk --template <name> --project <dir>
@@ -28,116 +29,221 @@ if (process.argv.length === 2) {
28
29
  const templateArg = readOptionValue(process.argv, "--template") ?? bareTemplateArg(process.argv);
29
30
  const projectArg = readOptionValue(process.argv, "--project");
30
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
+ }
31
41
  const { cloneProjectTemplate, templateTargetDir } = await import("./commands/project.js");
32
- const { assertMachineReadyOrExit, detectRuntimePrerequisites, formatRuntimeNextSteps } = await import("./utils/cold-preflight.js");
42
+ const { assertMachineReadyOrExit } = await import("./utils/cold-preflight.js");
33
43
  await assertMachineReadyOrExit({ scope: "scaffold" });
34
- const runtimeNextSteps = formatRuntimeNextSteps(detectRuntimePrerequisites());
35
44
  // A second demo in the same folder gets its own directory instead of failing.
36
45
  const { nextAvailableProjectDir } = await import("./utils/one-command.js");
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");
37
51
  const baseName = templateTargetDir(templateArg, projectArg);
38
- const targetName = nextAvailableProjectDir(baseName, (candidate) => {
39
- const path = resolve(cwd(), candidate);
40
- return existsSync(path) && readdirSync(path).length > 0;
41
- });
42
- const destination = resolve(cwd(), targetName);
43
- if (targetName !== baseName) {
44
- console.log(chalk.gray(`The folder ${baseName} is taken, using ${targetName} instead.`));
52
+ const fixedDir = projectArg ? undefined : PROJECT_TEMPLATES[templateArg]?.fixedDir;
53
+ const keep = process.argv.includes("--keep");
54
+ let targetName;
55
+ if (fixedDir) {
56
+ // A template with a fixed directory always lands in the same place, so a demo never
57
+ // leaves a new folder behind in whatever folder the command was typed in.
58
+ targetName = join(homedir(), ".flisk", fixedDir);
45
59
  }
46
- await cloneProjectTemplate(templateArg, targetName, runtimeNextSteps);
47
- const install = ensureGlobalInstall();
48
- if (install === "installed") {
49
- console.log("\n\u2714 flisk is installed globally, so `flisk` works from any folder.");
60
+ else {
61
+ targetName = nextAvailableProjectDir(baseName, (candidate) => {
62
+ const path = resolve(cwd(), candidate);
63
+ return existsSync(path) && readdirSync(path).length > 0;
64
+ });
65
+ if (targetName !== baseName) {
66
+ console.log(chalk.gray(`The folder ${baseName} is taken, using ${targetName} instead.`));
67
+ }
50
68
  }
51
- const binPath = process.argv[1];
52
- if (binPath && existsSync(destination)) {
53
- const { initializeProject, installServiceDependencies, runStackQuiet } = await import("./utils/one-command.js");
54
- const { openFirstFrontend } = await import("./commands/open.js");
55
- const { startSpinner } = await import("./utils/spinner.js");
56
- // Docker on Colima is only usable while the VM is up, and a stopped VM looks like a
57
- // broken Docker daemon. Say so before the long wait instead of after it.
58
- const { checkColima } = await import("./utils/colima.js");
59
- const colima = checkColima();
60
- if (colima.isColima && !colima.running) {
61
- console.log(chalk.yellow(colima.problem ?? "The Colima VM is not running."));
62
- console.log(chalk.gray(` Fix: ${colima.fix ?? "colima start"}`));
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;
73
+ if (fixedDir && existsSync(destination)) {
74
+ if (!existsSync(join(destination, ".tdk", "project.json"))) {
75
+ console.log(chalk.yellow(`${destination} already exists and is not a Flisk project.`));
76
+ console.log(chalk.gray(" Remove it, or use --project <dir> to put the demo somewhere else."));
63
77
  process.exit(1);
64
78
  }
65
79
  // A stack for this project may already be running (the same example in another folder
66
80
  // shares its compose project). Then there is nothing to start: open what is up.
67
- const { findReadyFrontend } = await import("./commands/open.js");
68
- const running = await findReadyFrontend(destination, 0);
81
+ const running = await pickReadyFrontend(await listFrontendTargets(destination), 0);
69
82
  if (running) {
70
- console.log(chalk.gray(`A stack for this project is already running (${running.name}).`));
71
- const { openFirstFrontend } = await import("./commands/open.js");
72
- await openFirstFrontend(destination);
83
+ console.log(chalk.gray(`The demo is already running: ${running.url}`));
84
+ await openUrl(running.url);
73
85
  process.exit(0);
74
86
  }
75
- // A cloned starter has no project configuration yet, and `flisk up` refuses to run
76
- // without one, so the project is set up before the stack starts.
77
- const setupSpinner = startSpinner("Setting up the project...");
78
- const setup = await initializeProject(binPath, destination);
79
- if (!setup.ok) {
80
- setupSpinner.fail("The project setup failed.");
81
- console.log(chalk.yellow(setup.error ?? "flisk project --yes did not finish."));
82
- process.exit(1);
83
- }
84
- setupSpinner.succeed("The project is ready.");
85
- // A fresh clone has no host dependencies, and the stack typechecks the services on the
86
- // host, so install them before the stack starts.
87
- const installSpinner = startSpinner("Installing the service dependencies...");
88
- const installs = await installServiceDependencies(destination);
89
- const failed = installs.filter((entry) => !entry.ok);
90
- if (failed.length > 0) {
91
- installSpinner.fail("Some services could not install their dependencies.");
92
- for (const entry of failed) {
93
- console.log(chalk.yellow(` - ${entry.dir}: run "bun install" there`));
94
- }
87
+ if (keep) {
88
+ reused = true;
95
89
  }
96
90
  else {
97
- installSpinner.succeed("The service dependencies are installed.");
91
+ rmSync(destination, { recursive: true, force: true });
92
+ }
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 });
98
133
  }
99
- const spinner = startSpinner("Starting the stack (the first build takes a few minutes)...");
100
- const started = Date.now();
101
- // Show what the stack is doing instead of a silent spinner: the resource states, the
102
- // elapsed time and where to watch the live logs. The Tilt port is not fixed (another
103
- // project may hold the default), so the ports that answer are looked at.
104
- const { readStackProgress, formatProgress } = await import("./utils/stack-progress.js");
105
- const { classifyTiltPorts } = await import("./utils/tilt-port-owner.js");
106
- const candidates = Array.from({ length: 10 }, (_, index) => 10350 + index);
107
- let watching = true;
108
- void (async () => {
109
- while (watching) {
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 {
110
192
  const { running } = await classifyTiltPorts(candidates, destination, async (port) => (await readStackProgress(port)) !== undefined);
111
193
  for (const port of running) {
112
194
  const progress = await readStackProgress(port);
113
195
  if (progress) {
114
- 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
+ });
115
202
  break;
116
203
  }
117
204
  }
118
- await new Promise((resolve) => setTimeout(resolve, 2000));
119
205
  }
120
- })();
121
- const outcome = await runStackQuiet(binPath, destination);
122
- watching = false;
123
- const tiltUiUrl = outcome.data?.tiltUrl ?? "http://localhost:10350";
124
- if (!outcome.ok) {
125
- spinner.fail("The stack did not come up.");
126
- console.log(chalk.yellow(outcome.error ?? "The stack stopped before it was ready."));
127
- console.log(chalk.gray("Run `flisk up` in that folder to see the full output."));
128
- process.exit(1);
129
- }
130
- spinner.succeed("The stack is up.");
131
- console.log(chalk.gray(` Tilt UI (live build logs): ${tiltUiUrl}`));
132
- console.log(chalk.gray(` Container logs: docker compose logs -f (in ${destination})`));
133
- // The browser only opens when a frontend actually answers, the same way `flisk open` does.
134
- const opened = await openFirstFrontend(destination).catch((error) => {
135
- console.log(chalk.yellow(`Could not open a browser: ${error instanceof Error ? error.message : String(error)}`));
136
- return null;
137
- });
138
- if (!opened) {
139
- 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
+ });
140
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."));
141
247
  }
142
248
  process.exit(0);
143
249
  }
@@ -167,3 +273,58 @@ function readOptionValue(args, name) {
167
273
  }
168
274
  return undefined;
169
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);