@flisk-sh/flisk 1.3.192 → 1.3.198

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 (37) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +4 -4
  3. package/cli/dist/cli.js +33 -9
  4. package/cli/dist/commands/help.js +1 -0
  5. package/cli/dist/commands/mcp.js +4 -1
  6. package/cli/dist/commands/open.js +104 -0
  7. package/cli/dist/commands/registry.js +2 -0
  8. package/cli/dist/commands/ui.js +1 -1
  9. package/cli/dist/commands/upgrade.js +2 -3
  10. package/cli/dist/mcp/tools.js +5 -2
  11. package/cli/dist/utils/cli-command.js +11 -2
  12. package/cli/dist/utils/doctor-fix.js +1 -1
  13. package/cli/dist/utils/one-command.js +66 -19
  14. package/cli/dist/utils/service-urls.js +6 -3
  15. package/cli/dist-bundle/chunk-CF46CD6R.js +6 -0
  16. package/cli/dist-bundle/chunk-DTIHDMED.js +5 -0
  17. package/cli/dist-bundle/chunk-DWHA5YKT.js +435 -0
  18. package/cli/dist-bundle/chunk-KBGQ4NJE.js +52 -0
  19. package/cli/dist-bundle/chunk-SB5WAMSU.js +5 -0
  20. package/cli/dist-bundle/chunk-US4D5GJJ.js +21 -0
  21. package/cli/dist-bundle/chunk-WPQOTHB5.js +4 -0
  22. package/cli/dist-bundle/{chunk-QR6O2A7L.js → chunk-YUARXGF4.js} +2 -2
  23. package/cli/dist-bundle/cli.js +251 -257
  24. package/cli/dist-bundle/cold-preflight-5TVSNLIV.js +2 -0
  25. package/cli/dist-bundle/one-command-TKXMWXCK.js +4 -0
  26. package/cli/dist-bundle/open-5SUD6LKB.js +2 -0
  27. package/cli/dist-bundle/project-5GTJYFAE.js +2 -0
  28. package/cli/dist-bundle/spinner-YLB6FYQH.js +2 -0
  29. package/cli/package.json +1 -1
  30. package/package.json +1 -1
  31. package/cli/dist-bundle/chunk-GOEBYLFN.js +0 -2
  32. package/cli/dist-bundle/chunk-RHSMS45X.js +0 -438
  33. package/cli/dist-bundle/chunk-XYFVXSEO.js +0 -75
  34. package/cli/dist-bundle/cold-preflight-ZJAU3E7K.js +0 -2
  35. package/cli/dist-bundle/one-command-A4R272U3.js +0 -4
  36. package/cli/dist-bundle/project-FOAR7E55.js +0 -2
  37. package/cli/dist-bundle/url-actions-D6ROTHZG.js +0 -2
package/CHANGELOG.md CHANGED
@@ -152,7 +152,7 @@ Notable changes to the `flisk` CLI. Versions match [npm](https://www.npmjs.com/p
152
152
 
153
153
  ## 1.3.111 (2026-10-05)
154
154
 
155
- - `flisk config regenerate` now restores a root `.tiltignore` that Flisk wrote (it carries the `SYSTEM-GENERATED BY Flisk CLI` header) and that was edited by hand, and prints `Restored: .tiltignore → project root (hand edits discarded)`. Before, it only copied the file when none existed, so `flisk config verify` kept failing with `Out of sync: .tiltignore (run 'flisk config regenerate')` after the advice was followed. A root `.tiltignore` without the header is still never overwritten, and plain `flisk project --yes` still does not overwrite an existing one. Docs: `docs/adopt-tdk.md` now quotes the current `flisk doctor` unknown-field wording. (#615)
155
+ - `flisk config regenerate` now restores a root `.tiltignore` that Flisk wrote (it carries the `SYSTEM-GENERATED BY Flisk CLI` header) and that was edited by hand, and prints `Restored: .tiltignore → project root (hand edits discarded)`. Before, it only copied the file when none existed, so `flisk config verify` kept failing with `Out of sync: .tiltignore (run 'flisk config regenerate')` after the advice was followed. A root `.tiltignore` without the header is still never overwritten, and plain `flisk project --yes` still does not overwrite an existing one. Docs: `docs/adopt-flisk.md` now quotes the current `flisk doctor` unknown-field wording. (#615)
156
156
  - `flisk doctor` now checks the project's own resource configs for duplicates. Two resources with the same `appName` fail (`Duplicate resource names`, naming both `service.json` paths): the name becomes the Compose service and Traefik router, so they collide, and before nothing reported it. Two backend, mcp or routed bring-your-own resources on the same `port` give a warning, not a failure (`Duplicate resource ports`): service ports are not published on the host and each service has its own container, so the pair can run, but it is usually a copied `service.json`. Frontends, workers, libraries and a missing `port` are ignored. Fixes #76. (#609)
157
157
  - `flisk doctor` no longer fails with `N resources without a package.json` for a bring-your-own resource. A bring-your-own resource (`--image`, `--dockerfile` or `buildContext`) is a plain container: its image is pulled or built from its own Dockerfile, never from the generated Dockerfile that copies a `package.json`, so the check was a false positive that made `flisk doctor` (and CI recipes that gate on it) exit non-zero right after wrapping an existing service. Backend, frontend, worker, migrator, mcp, library and sdk resources without a `package.json` still fail; Python, Go and Rust backends were already exempt. The other doctor checks were run on a project with only bring-your-own resources and report nothing. Fixes #599. (#600)
158
158
  - A scaffolded frontend now says why `bun run build` / `bun run dev` cannot run on the host before `flisk up`: the `.autogenerated/vite.config.*.autogenerated.ts` files its scripts point at are written by the engine (Tilt) when `flisk up` loads the resource, and neither `flisk resource` nor `flisk config regenerate` creates them. Before, `vite` stopped with an esbuild `Could not resolve` stack trace. New `predev`/`prebuild` scripts print what is missing and how to get it; once the file exists (and inside the engine image build) they pass silently. Existing frontends need the two scripts added by hand. Backend, worker and mcp scaffolds are unchanged. Fixes #592. (#594)
package/README.md CHANGED
@@ -196,7 +196,7 @@ Teams that run `flisk up` on their own code (listed with their consent in [ADOPT
196
196
  - [MakeOffer](https://www.linkedin.com/company/makeofferio), Flisk 1.3.180
197
197
  - [Red Moon Digital](https://redmoon.digital/), Flisk 1.3.81
198
198
 
199
- Using Flisk too? [Tell us](https://github.com/flisk-sh/flisk/issues/new?template=we-use-tdk.yml) or [add a row to ADOPTERS.md](ADOPTERS.md).
199
+ Using Flisk too? [Tell us](https://github.com/flisk-sh/flisk/issues/new?template=we-use-flisk.yml) or [add a row to ADOPTERS.md](ADOPTERS.md).
200
200
 
201
201
  If Flisk saves you time, a ⭐ on [this repo](https://github.com/flisk-sh/flisk) helps other developers find it.
202
202
 
@@ -328,7 +328,7 @@ Runnable examples show Flisk on real stacks. Start with the smallest one:
328
328
 
329
329
  ## Use Flisk with your AI agent
330
330
 
331
- The repository ships agent skills for common Flisk work, including `tdk-doctor` (environment checks) and `tdk-troubleshoot` (fixing `flisk` and Tilt errors), under [`.agents/skills`](.agents/skills). Agents working in this repo also follow [AGENTS.md](AGENTS.md). Use these skills to let your agent check your setup and explain failures instead of guessing at Docker or Tilt configuration.
331
+ The repository ships agent skills for common Flisk work, including `flisk-doctor` (environment checks) and `flisk-troubleshoot` (fixing `flisk` and Tilt errors), under [`.agents/skills`](.agents/skills). Agents working in this repo also follow [AGENTS.md](AGENTS.md). Use these skills to let your agent check your setup and explain failures instead of guessing at Docker or Tilt configuration.
332
332
 
333
333
  ## Installation
334
334
 
@@ -358,7 +358,7 @@ Set `TDK_DEBUG=1` before running `flisk up --verbose` to include Starlark debug
358
358
  - [CLI command cheat sheet](cli/README.md#cheat-sheet)
359
359
  - [Mega cheat sheet: every command and flag](docs/cheat-sheet.md)
360
360
  - [Documentation index](docs/README.md)
361
- - [Pilot Flisk on a real repository](docs/adopt-tdk.md)
361
+ - [Pilot Flisk on a real repository](docs/adopt-flisk.md)
362
362
  - [Running AI agents in Flisk](docs/agents/ai-agents.md)
363
363
  - [FAQ for agencies and multi-client teams](docs/faq-teams.md)
364
364
  - [Working alongside Helm](docs/with-helm.md)
@@ -384,7 +384,7 @@ Set `TDK_DEBUG=1` before running `flisk up --verbose` to include Starlark debug
384
384
  - [Architecture and repository map](docs/project-overview.md)
385
385
  - [Scale fixture measurements and caveats](docs/benchmarks/scale-bench.md)
386
386
  - [Benchmark report: warm and cold start charts, history](https://flisk-sh.github.io/flisk/) (updated on every change to `main` that affects start-up)
387
- - [How to run the start-time benchmark](scripts/benchmark/README.md#tdk-up-warm-start-benchmark)
387
+ - [How to run the start-time benchmark](scripts/benchmark/README.md#flisk-up-warm-start-benchmark)
388
388
 
389
389
  ## History
390
390
 
package/cli/dist/cli.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // Copyright (c) 2026 Flisk Landscape contributors
3
3
  // SPDX-License-Identifier: MIT
4
- import { existsSync } from "node:fs";
4
+ import { existsSync, readdirSync } from "node:fs";
5
5
  import { resolve } from "node:path";
6
6
  import { cwd } from "node:process";
7
+ import chalk from "chalk";
7
8
  import { showHelp } from "./commands/help.js";
8
9
  import { createProgram } from "./program.js";
9
10
  import { ensureGlobalInstall } from "./utils/cli-command.js";
@@ -31,21 +32,44 @@ if (templateArg) {
31
32
  const { PROJECT_TEMPLATES } = await import("./utils/project-templates.js");
32
33
  await assertMachineReadyOrExit({ scope: "scaffold" });
33
34
  const runtimeNextSteps = formatRuntimeNextSteps(detectRuntimePrerequisites());
34
- await cloneProjectTemplate(templateArg, projectArg, runtimeNextSteps);
35
+ // A second demo in the same folder gets its own directory instead of failing.
36
+ const { nextAvailableProjectDir } = await import("./utils/one-command.js");
37
+ 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.`));
45
+ }
46
+ await cloneProjectTemplate(templateArg, targetName, runtimeNextSteps);
35
47
  const install = ensureGlobalInstall();
36
48
  if (install === "installed") {
37
49
  console.log("\n\u2714 flisk is installed globally, so `flisk` works from any folder.");
38
50
  }
39
- const destination = resolve(cwd(), templateTargetDir(templateArg, projectArg));
40
51
  const binPath = process.argv[1];
41
52
  if (binPath && existsSync(destination)) {
42
- const { runStack } = await import("./utils/one-command.js");
43
- const { openUrl } = await import("./utils/url-actions.js");
44
- console.log(`\n\u{1F680} Starting the stack in ${destination}...\n`);
45
- await runStack(binPath, destination, process.env, undefined, (url) => {
46
- void openUrl(url);
47
- console.log(`\n\u{1F310} Opening ${url}`);
53
+ const { runStackQuiet } = await import("./utils/one-command.js");
54
+ const { openFirstFrontend } = await import("./commands/open.js");
55
+ const { startSpinner } = await import("./utils/spinner.js");
56
+ const spinner = startSpinner("Starting the stack (the first build takes a minute)...");
57
+ const outcome = await runStackQuiet(binPath, destination);
58
+ if (!outcome.ok) {
59
+ spinner.fail("The stack did not come up.");
60
+ console.log(chalk.yellow(outcome.error ?? "The stack stopped before it was ready."));
61
+ console.log(chalk.gray("Run `flisk up` in that folder to see the full output."));
62
+ process.exit(1);
63
+ }
64
+ spinner.succeed("The stack is up.");
65
+ // The browser only opens when a frontend actually answers, the same way `flisk open` does.
66
+ const opened = await openFirstFrontend(destination).catch((error) => {
67
+ console.log(chalk.yellow(`Could not open a browser: ${error instanceof Error ? error.message : String(error)}`));
68
+ return null;
48
69
  });
70
+ if (!opened) {
71
+ console.log(chalk.gray(`The stack is running. Open it yourself: cd ${destination} && flisk open`));
72
+ }
49
73
  }
50
74
  process.exit(0);
51
75
  }
@@ -57,6 +57,7 @@ const COMMAND_GROUPS = [
57
57
  { name: "down", desc: "Stop all tilt resources", alias: "" },
58
58
  { name: "restart", desc: "Restart a running resource in the stack", alias: "<resource>" },
59
59
  { name: "status", desc: "Show status of services and stacks", alias: "" },
60
+ { name: "open", desc: "Open the first ready frontend in your browser", alias: "" },
60
61
  { name: "logs", desc: "Print recent logs from the running stack", alias: "--json" },
61
62
  ],
62
63
  },
@@ -8,7 +8,10 @@ import { getPackageVersion } from "../utils/paths.js";
8
8
  export const mcpCommand = new Command("mcp")
9
9
  .description("Run a Model Context Protocol server over stdio for coding agents")
10
10
  .action(async () => {
11
- const server = createMcpServer(createTdkTools(), { name: "flisk", version: getPackageVersion() });
11
+ const server = createMcpServer(createTdkTools(), {
12
+ name: "flisk",
13
+ version: getPackageVersion(),
14
+ });
12
15
  // Stdout is the protocol channel. Nothing else may write to it; diagnostics go to stderr.
13
16
  const lines = createInterface({ input: process.stdin });
14
17
  const pending = new Set();
@@ -0,0 +1,104 @@
1
+ // Copyright (c) 2026 Flisk Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ import chalk from "chalk";
4
+ import { Command } from "commander";
5
+ import { showDetail, showStep } from "../utils/formatting.js";
6
+ import { getHostPortPlan } from "../utils/host-port-config.js";
7
+ import { findProjectRoot } from "../utils/paths.js";
8
+ import { resolveServicePath, resolveSubdomainBases } from "../utils/service-urls.js";
9
+ import { discoverResourcesWithProblems } from "../utils/services.js";
10
+ import { readProjectName } from "../utils/shared-infra.js";
11
+ import { startSpinner } from "../utils/spinner.js";
12
+ import { openUrl } from "../utils/url-actions.js";
13
+ /** The frontends of the current project, with the URL each one is published under. */
14
+ export async function listFrontendTargets(projectRoot) {
15
+ const { resources } = discoverResourcesWithProblems(projectRoot);
16
+ const plan = await getHostPortPlan(projectRoot, { inspectDocker: false });
17
+ const { appBase } = resolveSubdomainBases(plan?.ingressHttp, projectRoot);
18
+ return resources
19
+ .filter((resource) => resource.config?.appType === "frontend")
20
+ .map((resource) => ({
21
+ name: resource.name,
22
+ url: `${appBase}${resolveServicePath(resource)}`,
23
+ }));
24
+ }
25
+ /** True when the URL answers. A booting frontend needs a moment, so this is asked more than once. */
26
+ export async function isFrontendReady(url, timeoutMs = 2000) {
27
+ try {
28
+ const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
29
+ return response.status < 500;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ /**
36
+ * The first frontend that answers. Returns undefined when none answers within the wait, so the
37
+ * caller can say so instead of opening a URL that refuses the connection.
38
+ */
39
+ export async function pickReadyFrontend(targets, waitMs = 30000, onWait) {
40
+ if (targets.length === 0)
41
+ return undefined;
42
+ const started = Date.now();
43
+ const deadline = started + waitMs;
44
+ while (true) {
45
+ for (const target of targets) {
46
+ if (await isFrontendReady(target.url))
47
+ return target;
48
+ }
49
+ const elapsed = Date.now() - started;
50
+ if (elapsed >= waitMs)
51
+ return undefined;
52
+ onWait?.(elapsed);
53
+ await new Promise((resolve) => setTimeout(resolve, 2000));
54
+ }
55
+ }
56
+ /**
57
+ * Opens the first ready frontend of the project in the browser. Returns the URL it opened, or
58
+ * null when the project has no frontend.
59
+ */
60
+ export async function openFirstFrontend(projectRootOverride) {
61
+ const projectRoot = projectRootOverride ?? findProjectRoot();
62
+ if (!projectRoot) {
63
+ console.log(chalk.yellow("Not in a Flisk project, so there is no frontend to open."));
64
+ console.log(chalk.gray("Run `flisk project --yes` first, or `flisk open` inside a project."));
65
+ return null;
66
+ }
67
+ const targets = await listFrontendTargets(projectRoot);
68
+ if (targets.length === 0) {
69
+ console.log(chalk.yellow("This project has no frontend service to open."));
70
+ console.log(chalk.gray("Start the stack with `flisk up` and add a frontend with `flisk resource`."));
71
+ return null;
72
+ }
73
+ const projectName = readProjectName(projectRoot);
74
+ showStep(`Opening the first frontend of ${projectName ?? "this project"}`);
75
+ for (const target of targets) {
76
+ console.log(chalk.gray(` - ${target.name}: ${target.url}`));
77
+ }
78
+ const spinner = startSpinner("Waiting for the frontend to answer...");
79
+ const first = await pickReadyFrontend(targets, 120000, (elapsed) => {
80
+ spinner.text = `Waiting for the frontend to answer... ${Math.round(elapsed / 1000)}s`;
81
+ });
82
+ if (!first) {
83
+ spinner.fail("No frontend answered yet.");
84
+ console.log(chalk.yellow("The stack is not serving the frontend yet."));
85
+ console.log(chalk.gray(" - Start the stack in another terminal: flisk up"));
86
+ console.log(chalk.gray(" - Then run this again: flisk open"));
87
+ for (const target of targets) {
88
+ console.log(chalk.gray(` - ${target.name}: ${target.url}`));
89
+ }
90
+ return null;
91
+ }
92
+ spinner.succeed(`The frontend is up: ${first.name}`);
93
+ const opened = await openUrl(first.url);
94
+ console.log(opened
95
+ ? chalk.green(`\nOpened ${first.url}`)
96
+ : chalk.yellow(`\nCould not open a browser. Open ${first.url} yourself.`));
97
+ showDetail("Run `flisk open` again to open the next frontend.");
98
+ return first.url;
99
+ }
100
+ export const openCommand = new Command("open")
101
+ .description("Open the first frontend of the project in your browser")
102
+ .action(async () => {
103
+ await openFirstFrontend();
104
+ });
@@ -9,6 +9,7 @@ import { logsCommand } from "./logs.js";
9
9
  import { maintainersCommand } from "./maintainers.js";
10
10
  import { mcpCommand } from "./mcp.js";
11
11
  import { networksCommand } from "./networks.js";
12
+ import { openCommand } from "./open.js";
12
13
  import { projectCommand } from "./project.js";
13
14
  import { projectsCommand } from "./projects.js";
14
15
  import { resourceCommand } from "./resource.js";
@@ -34,6 +35,7 @@ export function getRegisteredCommands() {
34
35
  logsCommand,
35
36
  ejectCommand,
36
37
  importCommand,
38
+ openCommand,
37
39
  statusCommand,
38
40
  stackCommand,
39
41
  resourceCommand,
@@ -45,7 +45,7 @@ export const LoadingScreen = ({ message = "Discovering resources...", animated =
45
45
  // biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
46
46
  const ErrorScreen = ({ error, onRetry }) => {
47
47
  const theme = useTUITheme();
48
- return (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: theme.error, children: "Could Not Load Services" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { color: theme.error, children: [theme.ascii ? "[x]" : "\u2717", " ", error] }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: theme.muted, children: "Troubleshooting:" }), _jsx(Text, { color: theme.muted, children: " 1. Check that every service.json is valid JSON" }), _jsx(Text, { color: theme.muted, children: " 2. Run flisk from your project (the folder with the Tiltfile)" }), _jsx(Text, { color: theme.muted, children: " 3. Try: flisk status --verbose" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: theme.accent, children: "Press [r] to retry or [q] to quit" })] }));
48
+ return (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: theme.error, children: "Could Not Load Services" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { color: theme.error, children: [theme.ascii ? "[x]" : "\u2717", " ", error] }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: theme.muted, children: "Troubleshooting:" }), _jsx(Text, { color: theme.muted, children: " 1. Check that every service.json is valid JSON" }), _jsxs(Text, { color: theme.muted, children: [" ", "2. Run flisk from your project (the folder with the Tiltfile)"] }), _jsx(Text, { color: theme.muted, children: " 3. Try: flisk status --verbose" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: theme.accent, children: "Press [r] to retry or [q] to quit" })] }));
49
49
  };
50
50
  const EmptyState = ({ message }) => {
51
51
  const theme = useTUITheme();
@@ -7,6 +7,7 @@ import { basename, dirname, join, resolve, win32 } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import chalk from "chalk";
9
9
  import { Command } from "commander";
10
+ import { LEGACY_PACKAGE } from "../utils/cli-command.js";
10
11
  import { addYesOption, handleDryRun } from "../utils/command-helpers.js";
11
12
  import { PACKAGE_JSON } from "../utils/constants.js";
12
13
  import { getErrorMessage, logVerbose, showErrorAndExit } from "../utils/errors.js";
@@ -363,9 +364,7 @@ export async function getLatestVersion() {
363
364
  * before it starts. The removal is best effort: the package is often not installed at all.
364
365
  */
365
366
  function removeLegacyInstallation(manager) {
366
- const command = manager === "npm"
367
- ? "npm uninstall -g @tdk-landscape/tdk-cli-core"
368
- : "bun remove -g @tdk-landscape/tdk-cli-core";
367
+ const command = manager === "npm" ? `npm uninstall -g ${LEGACY_PACKAGE}` : `bun remove -g ${LEGACY_PACKAGE}`;
369
368
  try {
370
369
  execSync(command, { stdio: ["ignore", "ignore", "ignore"], timeout: 60000 });
371
370
  }
@@ -79,7 +79,10 @@ export function toToolResult(run) {
79
79
  return {
80
80
  data: {
81
81
  errors: [
82
- { code: "NO_JSON", message: run.stderr.trim() || `flisk exited with code ${run.exitCode}` },
82
+ {
83
+ code: "NO_JSON",
84
+ message: run.stderr.trim() || `flisk exited with code ${run.exitCode}`,
85
+ },
83
86
  ],
84
87
  },
85
88
  isError: true,
@@ -120,7 +123,7 @@ export const defaultUpDeps = {
120
123
  },
121
124
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
122
125
  now: () => Date.now(),
123
- logFile: () => join(tmpdir(), `tdk-mcp-up-${Date.now()}.log`),
126
+ logFile: () => join(tmpdir(), `flisk-mcp-up-${Date.now()}.log`),
124
127
  };
125
128
  export function buildUpArgs(args) {
126
129
  const stack = optionalName(args.stack, "stack");
@@ -7,7 +7,15 @@ import { delimiter, join } from "node:path";
7
7
  export const CLI_COMMAND = "flisk";
8
8
  /** The npm package that installs it. */
9
9
  export const CLI_PACKAGE = "@flisk-sh/flisk";
10
- /** The package the CLI was published under before the rename. It owns the old `flisk` command. */
10
+ /** The repository the CLI is released from. */
11
+ export const CLI_REPOSITORY = "https://github.com/flisk-sh/flisk";
12
+ /**
13
+ * The package the CLI was published under before the rename. It owns the old `flisk` command.
14
+ *
15
+ * The old name stays on purpose: that package is still published, and it is the one a
16
+ * machine has to remove before the current package can own the command. Naming the
17
+ * current package here would uninstall the CLI instead of the leftover install.
18
+ */
11
19
  export const LEGACY_PACKAGE = "@tdk-landscape/tdk-cli-core";
12
20
  /** True when the command can be typed as-is, so `flisk ...` resolves. */
13
21
  export function isCommandOnPath(command = CLI_COMMAND, env = process.env) {
@@ -71,7 +79,8 @@ export function readyNextStep(env = process.env, cwd = process.cwd()) {
71
79
  return (`The \`${CLI_COMMAND}\` command is not on PATH (this run is not a global install). ` +
72
80
  `Install it with \`npm install -g ${CLI_PACKAGE}\`, then run \`${CLI_COMMAND} project --yes\`. ` +
73
81
  `If npm reports EEXIST for \`flisk\`, the old package still owns that command: ` +
74
- `run \`npm uninstall -g ${LEGACY_PACKAGE}\` first.`);
82
+ `run \`npm uninstall -g ${LEGACY_PACKAGE}\` first. ` +
83
+ `The current release is published from ${CLI_REPOSITORY}.`);
75
84
  }
76
85
  if (isEmptyWorkspace(cwd)) {
77
86
  return `Next: ${CLI_COMMAND} project --yes`;
@@ -366,7 +366,7 @@ export async function checkDuplicateTdkInstalls(installs) {
366
366
  didPass: false,
367
367
  isWarning: true,
368
368
  message: `${found.length} flisk installs on PATH${versions.size > 1 ? " with different versions" : ""}: ${list}`,
369
- fix: "Keep one: remove the others, or run flisk upgrade. The first on PATH is the one your shell runs; tools that launch /usr/local/bin/tdk directly may use another",
369
+ fix: "Keep one: remove the others, or run flisk upgrade. The first on PATH is the one your shell runs; tools that launch /usr/local/bin/flisk directly may use another",
370
370
  };
371
371
  }
372
372
  /**
@@ -18,33 +18,80 @@ export function findFrontendUrl(output) {
18
18
  }
19
19
  return undefined;
20
20
  }
21
+ /** Reads the `{schemaVersion, data, errors}` envelope `flisk up --json` prints when it is done. */
22
+ export function parseStackEnvelope(output) {
23
+ for (const line of output.split("\n")) {
24
+ const trimmed = line.trim();
25
+ if (!trimmed.startsWith("{"))
26
+ continue;
27
+ try {
28
+ const parsed = JSON.parse(trimmed);
29
+ const data = parsed.data ?? {};
30
+ const ok = data.ok === true;
31
+ return {
32
+ ok,
33
+ error: ok ? undefined : (parsed.errors?.[0]?.message ?? "the stack did not come up"),
34
+ raw: output,
35
+ };
36
+ }
37
+ catch {
38
+ // Not the envelope; keep looking.
39
+ }
40
+ }
41
+ return undefined;
42
+ }
21
43
  /**
22
- * Runs `flisk up` in the project directory and streams its output to the terminal. The stack
23
- * keeps running in the foreground, so the caller is told about the frontend URL as soon as it
24
- * shows up instead of waiting for the process to exit.
44
+ * Starts the stack quietly and reports when it is ready.
45
+ *
46
+ * `flisk up --json` prints one JSON object when the stack is up (or has failed) and keeps
47
+ * Tilt's own build logs out of the way, so the caller can show a progress line instead of
48
+ * thousands of Docker and Traefik lines. The stack keeps running in the background.
25
49
  */
26
- export function runStack(binPath, cwd, env = process.env, spawnFn = spawn, onFrontendUrl) {
50
+ export function runStackQuiet(binPath, cwd, env = process.env, spawnFn = spawn) {
27
51
  return new Promise((resolve) => {
28
- const child = spawnFn(process.execPath, [binPath, "up"], {
52
+ const child = spawnFn(process.execPath, [binPath, "up", "--json"], {
29
53
  cwd,
30
54
  env,
31
- stdio: ["inherit", "pipe", "inherit"],
55
+ stdio: ["inherit", "pipe", "pipe"],
32
56
  });
33
57
  let output = "";
34
- let announced = false;
58
+ let settled = false;
59
+ const settle = (outcome) => {
60
+ if (settled)
61
+ return;
62
+ settled = true;
63
+ // The stack outlives this command: let the child run on its own.
64
+ child.unref?.();
65
+ resolve(outcome);
66
+ };
35
67
  child.stdout?.on("data", (chunk) => {
36
- const text = String(chunk);
37
- output += text;
38
- process.stdout.write(text);
39
- if (!announced) {
40
- const url = findFrontendUrl(output);
41
- if (url) {
42
- announced = true;
43
- onFrontendUrl?.(url);
44
- }
45
- }
68
+ output += String(chunk);
69
+ const envelope = parseStackEnvelope(output);
70
+ if (envelope)
71
+ settle(envelope);
72
+ });
73
+ child.stderr?.on("data", (chunk) => {
74
+ output += String(chunk);
46
75
  });
47
- child.on("error", (error) => resolve({ code: 1, output: `${output}\n${error.message}` }));
48
- child.on("close", (code) => resolve({ code, output }));
76
+ child.on("error", (error) => settle({ ok: false, error: error.message, raw: output }));
77
+ child.on("close", (code) => settle({
78
+ ok: false,
79
+ error: `the stack stopped before it was ready (code ${code})`,
80
+ raw: output,
81
+ }));
49
82
  });
50
83
  }
84
+ /**
85
+ * A free directory for the project. A second demo in the same folder gets its own directory
86
+ * (`name-2`, `name-3`, ...) instead of failing because the first one is still there.
87
+ */
88
+ export function nextAvailableProjectDir(base, isTaken) {
89
+ if (!isTaken(base))
90
+ return base;
91
+ for (let suffix = 2; suffix < 1000; suffix += 1) {
92
+ const candidate = `${base}-${suffix}`;
93
+ if (!isTaken(candidate))
94
+ return candidate;
95
+ }
96
+ return base;
97
+ }
@@ -6,7 +6,10 @@ import { PROJECT_JSON } from "./constants.js";
6
6
  import { findProjectRoot } from "./paths.js";
7
7
  import { isApiServiceType } from "./resource-kind.js";
8
8
  export function getProjectName() {
9
- const root = findProjectRoot();
9
+ return getProjectNameFor(findProjectRoot());
10
+ }
11
+ /** The project name of a specific project root, for tools that work outside the current directory. */
12
+ export function getProjectNameFor(root) {
10
13
  if (root) {
11
14
  try {
12
15
  const content = readFileSync(join(root, ".tdk", PROJECT_JSON), "utf-8");
@@ -19,8 +22,8 @@ export function getProjectName() {
19
22
  }
20
23
  return "tdk-project";
21
24
  }
22
- export function resolveSubdomainBases(ingressPort) {
23
- const projectName = getProjectName();
25
+ export function resolveSubdomainBases(ingressPort, projectRoot) {
26
+ const projectName = getProjectNameFor(projectRoot ?? findProjectRoot());
24
27
  const raw = process.env.TDK_SERVICE_BASE_URL ??
25
28
  `http://${projectName}.localhost${ingressPort ? `:${ingressPort}` : ""}`;
26
29
  try {
@@ -0,0 +1,6 @@
1
+ import{createRequire as __tdkRequire}from'node:module';const require=__tdkRequire(import.meta.url);
2
+ function x(r,e,t){let n=r.indexOf(e);if(n===-1)return r;let o=e.length,s=0,i="";do i+=r.slice(s,n)+e+t,s=n+o,n=r.indexOf(e,s);while(n!==-1);return i+=r.slice(s),i}function I(r,e,t,n){let o=0,s="";do{let i=r[n-1]==="\r";s+=r.slice(o,i?n-1:n)+e+(i?`\r
3
+ `:`
4
+ `)+t,o=n+1,n=r.indexOf(`
5
+ `,o)}while(n!==-1);return s+=r.slice(o),s}var F=(r=0)=>e=>`\x1B[${e+r}m`,T=(r=0)=>e=>`\x1B[${38+r};5;${e}m`,R=(r=0)=>(e,t,n)=>`\x1B[${38+r};2;${e};${t};${n}m`,$=r=>`\x1B[58;5;${r<90?r-30:r-90+8}m`,l={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],underlineDouble:["4:2",24],underlineCurly:["4:3",24],underlineDotted:["4:4",24],underlineDashed:["4:5",24],overline:[53,55],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],gray:[90,39],grey:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgGray:[100,49],bgGrey:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]},underlineColor:{underlineBlack:["58;5;0",59],underlineRed:["58;5;1",59],underlineGreen:["58;5;2",59],underlineYellow:["58;5;3",59],underlineBlue:["58;5;4",59],underlineMagenta:["58;5;5",59],underlineCyan:["58;5;6",59],underlineWhite:["58;5;7",59],underlineBlackBright:["58;5;8",59],underlineGray:["58;5;8",59],underlineGrey:["58;5;8",59],underlineRedBright:["58;5;9",59],underlineGreenBright:["58;5;10",59],underlineYellowBright:["58;5;11",59],underlineBlueBright:["58;5;12",59],underlineMagentaBright:["58;5;13",59],underlineCyanBright:["58;5;14",59],underlineWhiteBright:["58;5;15",59]}},M=Object.keys(l.modifier),y=Object.keys(l.color),E=Object.keys(l.bgColor),U=Object.keys(l.underlineColor),S=[...y,...E];function V(){let r=new Map;for(let[e,t]of Object.entries(l)){for(let[n,o]of Object.entries(t))l[n]={open:`\x1B[${o[0]}m`,close:`\x1B[${o[1]}m`},t[n]=l[n],r.set(Number.parseInt(o[0],10),o[1]);Object.defineProperty(l,e,{value:t,enumerable:!1})}return Object.defineProperty(l,"codes",{value:r,enumerable:!1}),l.color.close="\x1B[39m",l.bgColor.close="\x1B[49m",l.underlineColor.close="\x1B[59m",l.color.ansi=F(),l.color.ansi256=T(),l.color.ansi16m=R(),l.bgColor.ansi=F(10),l.bgColor.ansi256=T(10),l.bgColor.ansi16m=R(10),l.underlineColor.ansi=$,l.underlineColor.ansi256=T(20),l.underlineColor.ansi16m=R(20),Object.defineProperties(l,{rgbToAnsi256:{value(e,t,n){return e===t&&t===n?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(t/255*5)+Math.round(n/255*5)},enumerable:!1},hexToRgb:{value(e){let t=/[\da-f]{6}|[\da-f]{3}/i.exec(e.toString(16));if(!t)return[0,0,0];let[n]=t;n.length===3&&(n=[...n].map(s=>s+s).join(""));let o=Number.parseInt(n,16);return[o>>16&255,o>>8&255,o&255]},enumerable:!1},hexToAnsi256:{value:e=>l.rgbToAnsi256(...l.hexToRgb(e)),enumerable:!1},ansi256ToAnsi:{value(e){if(e<8)return 30+e;if(e<16)return 90+(e-8);let t,n,o;if(e>=232)t=((e-232)*10+8)/255,n=t,o=t;else{e-=16;let f=e%36;t=Math.floor(e/36)/5,n=Math.floor(f/6)/5,o=f%6/5}let s=Math.max(t,n,o)*2;if(s===0)return 30;let i=30+(Math.round(o)<<2|Math.round(n)<<1|Math.round(t));return s===2&&(i+=60),i},enumerable:!1},rgbToAnsi:{value:(e,t,n)=>l.ansi256ToAnsi(l.rgbToAnsi256(e,t,n)),enumerable:!1},hexToAnsi:{value:e=>l.ansi256ToAnsi(l.hexToAnsi256(e)),enumerable:!1}}),l}var W=V(),c=W;import A from"node:process";import K from"node:os";import v from"node:tty";function a(r,e=globalThis.Deno?globalThis.Deno.args:A.argv){let t=r.startsWith("-")?"":r.length===1?"-":"--",n=e.indexOf(t+r),o=e.indexOf("--");return n!==-1&&(o===-1||n<o)}var{env:u}=A,g;a("no-color")||a("no-colors")||a("color=false")||a("color=never")?g=0:(a("color")||a("colors")||a("color=true")||a("color=always"))&&(g=1);function L(){return/^\d+$/.test(u.FORCE_COLOR)}function z(){if("FORCE_COLOR"in u){if(u.FORCE_COLOR==="false")return 0;if(u.FORCE_COLOR==="true"||u.FORCE_COLOR.length===0)return 1;if(L())return Math.min(Number.parseInt(u.FORCE_COLOR,10),3)}}function H(r){return r===0?!1:{level:r,hasBasic:!0,has256:r>=2,has16m:r>=3}}function q(r,{streamIsTTY:e,sniffFlags:t=!0}={}){let n=z();n!==void 0&&(g=n);let o=t?g:n;if(o===0)return 0;if(t){if(a("color=16m")||a("color=full")||a("color=truecolor"))return 3;if(a("color=256"))return 2}if(o!==void 0&&L())return o;if("TF_BUILD"in u&&"AGENT_NAME"in u)return 1;if(r&&!e&&o===void 0)return 0;let s=o||0;if(u.TERM==="dumb")return s;if(A.platform==="win32"){let i=K.release().split(".");return Number(i[0])>=10&&Number(i[2])>=10586?Number(i[2])>=14931?3:2:1}if("CI"in u)return["GITHUB_ACTIONS","GITEA_ACTIONS","CIRCLECI"].some(i=>i in u)?3:["TRAVIS","APPVEYOR","GITLAB_CI","BUILDKITE","DRONE"].some(i=>i in u)||u.CI_NAME==="codeship"?1:s;if("TEAMCITY_VERSION"in u)return/^(?:9\.0*[1-9]\d*\.|\d{2,}\.)/.test(u.TEAMCITY_VERSION)?1:0;if(u.COLORTERM==="truecolor"||u.TERM==="xterm-kitty"||u.TERM==="xterm-ghostty"||u.TERM==="wezterm")return 3;if("TERM_PROGRAM"in u){let i=Number.parseInt((u.TERM_PROGRAM_VERSION||"").split(".",1)[0],10);switch(u.TERM_PROGRAM){case"iTerm.app":return i>=3?3:2;case"Apple_Terminal":return 2}}return/-256(?:color)?$/i.test(u.TERM)?2:/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(u.TERM)||"COLORTERM"in u?1:s}function _(r,e={}){let t=q(r,{streamIsTTY:r&&r.isTTY,...e});return H(t)}var J={stdout:_({isTTY:v.isatty(1)}),stderr:_({isTTY:v.isatty(2)})},w=J;var{stdout:j,stderr:G}=w,h=Symbol("GENERATOR"),b=Symbol("STYLER"),O=Symbol("IS_EMPTY"),C=Symbol("LEVEL"),m=Object.create(null),k=r=>{if(!Number.isSafeInteger(r)||r<0||r>3)throw new Error("The `level` should be an integer from 0 to 3")},Q={enumerable:!0,get(){return this[C]},set(r){k(r),this[C]=r}},X=(r,e={})=>{e.level!==void 0&&k(e.level);let t=j?j.level:0;r[C]=e.level===void 0?t:e.level};var Z=r=>{let e=(...t)=>t.join(" ");return X(e,r),Object.setPrototypeOf(e,p.prototype),e};function p(r){return Z(r)}Object.setPrototypeOf(p.prototype,Function.prototype);for(let[r,e]of Object.entries(c))m[r]={get(){let t=B(this,P(e.open,e.close,this[b]),this[O]);return Object.defineProperty(this,r,{value:t}),t}};m.visible={get(){let r=B(this,this[b],!0);return Object.defineProperty(this,"visible",{value:r}),r}};var ee=(r,e)=>{let t=c[e];if(r==="rgb"){let o=(i,f,d)=>t.ansi(c.rgbToAnsi(i,f,d));return[o,o,(i,f,d)=>t.ansi256(c.rgbToAnsi256(i,f,d)),t.ansi16m]}if(r==="hex"){let o=i=>t.ansi(c.hexToAnsi(i));return[o,o,i=>t.ansi256(c.hexToAnsi256(i)),i=>t.ansi16m(...c.hexToRgb(i))]}let n=o=>t.ansi(c.ansi256ToAnsi(o));return[n,n,t.ansi256,t.ansi256]},re=["rgb","hex","ansi256"];for(let r of re){let e=r[0].toUpperCase()+r.slice(1);for(let[t,n]of[[r,"color"],["bg"+e,"bgColor"],["underline"+e,"underlineColor"]]){let{close:o}=c[n],s=ee(r,n);m[t]={get(){let i=function(f,d,D){let Y=s[this.level](f,d,D);return B(this,P(Y,o,this[b]),this[O])};return Object.defineProperty(this,t,{value:i}),i}}}}var te=Object.defineProperties(()=>{},{...m,level:{enumerable:!0,get(){return this[h].level},set(r){this[h].level=r}}}),P=(r,e,t)=>{let n,o;return t===void 0?(n=r,o=e):(n=t.openAll+r,o=e+t.closeAll),{open:r,close:e,openAll:n,closeAll:o,parent:t}},B=(r,e,t)=>{let n=(...o)=>o.length===1?N(n,""+o[0]):o.length===2?N(n,`${o[0]??""} ${o[1]??""}`):N(n,o.join(" "));return Object.setPrototypeOf(n,te),n[h]=r[h]??r,n[b]=e,n[O]=t,n},N=(r,e)=>{if(r[h][C]<=0||!e)return r[O]?"":e;let t=r[b];if(t===void 0)return e;let{openAll:n,closeAll:o}=t;if(e.includes("\x1B"))for(;t!==void 0;)e=x(e,t.close,t.open),t=t.parent;let s=e.indexOf(`
6
+ `);return s!==-1&&(e=I(e,o,n,s)),n+e+o};Object.defineProperties(p.prototype,{...m,level:Q});var ne=p(),he=p({level:G?G.level:0});var be=ne;export{y as a,be as b};
@@ -0,0 +1,5 @@
1
+ import{createRequire as __tdkRequire}from'node:module';const require=__tdkRequire(import.meta.url);
2
+ import{b as n}from"./chunk-CF46CD6R.js";var o=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function w(f){let r=process.stderr,i=r.isTTY===!0,s=0,t=f,a,l=()=>r.write(`\r\x1B[2K${n.cyan(o[s])} ${t}`);i?(l(),a=setInterval(()=>{s=(s+1)%o.length,l()},80)):r.write(`- ${t}
3
+ `);let c=(e,$)=>{a&&clearInterval(a),r.write(`${i?"\r\x1B[2K":""}${e} ${$??t}
4
+ `)};return{get text(){return t},set text(e){t=e,i||r.write(`- ${t}
5
+ `)},succeed:e=>c(n.green("\u2714"),e),fail:e=>c(n.red("\u2716"),e),warn:e=>c(n.yellow("\u26A0"),e)}}export{w as a};