@flisk-sh/flisk 1.3.191 → 1.3.195

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/dist/cli.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { existsSync } 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";
@@ -26,7 +27,7 @@ if (process.argv.length === 2) {
26
27
  const templateArg = readOptionValue(process.argv, "--template") ?? (await bareTemplateArg(process.argv));
27
28
  const projectArg = readOptionValue(process.argv, "--project");
28
29
  if (templateArg) {
29
- const { cloneProjectTemplate } = await import("./commands/project.js");
30
+ const { cloneProjectTemplate, templateTargetDir } = await import("./commands/project.js");
30
31
  const { assertMachineReadyOrExit, detectRuntimePrerequisites, formatRuntimeNextSteps } = await import("./utils/cold-preflight.js");
31
32
  const { PROJECT_TEMPLATES } = await import("./utils/project-templates.js");
32
33
  await assertMachineReadyOrExit({ scope: "scaffold" });
@@ -36,18 +37,19 @@ if (templateArg) {
36
37
  if (install === "installed") {
37
38
  console.log("\n\u2714 flisk is installed globally, so `flisk` works from any folder.");
38
39
  }
39
- const destination = resolve(cwd(), projectArg || templateArg);
40
+ const destination = resolve(cwd(), templateTargetDir(templateArg, projectArg));
40
41
  const binPath = process.argv[1];
41
42
  if (binPath && existsSync(destination)) {
42
- const { runStack, findFrontendUrl } = await import("./utils/one-command.js");
43
- const { openUrl } = await import("./utils/url-actions.js");
43
+ const { runStack } = await import("./utils/one-command.js");
44
+ const { openFirstFrontend } = await import("./commands/open.js");
44
45
  console.log(`\n\u{1F680} Starting the stack in ${destination}...\n`);
45
- const result = await runStack(binPath, destination);
46
- const url = findFrontendUrl(result.output);
47
- if (url) {
48
- console.log(`\n\u{1F310} Opening ${url}`);
49
- await openUrl(url);
50
- }
46
+ await runStack(binPath, destination, process.env, undefined, () => {
47
+ // The stack printed its URLs: open the first frontend that answers, the same way
48
+ // `flisk open` does.
49
+ void openFirstFrontend(destination).catch((error) => {
50
+ console.log(chalk.yellow(`\nCould not open a browser: ${error instanceof Error ? error.message : String(error)}`));
51
+ });
52
+ });
51
53
  }
52
54
  process.exit(0);
53
55
  }
@@ -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
  },
@@ -0,0 +1,85 @@
1
+ // Copyright (c) 2026 Flisk Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ import chalk from "chalk";
4
+ import { Command } from "commander";
5
+ import { discoverResourcesWithProblems } from "../utils/services.js";
6
+ import { showDetail, showStep } from "../utils/formatting.js";
7
+ import { getHostPortPlan } from "../utils/host-port-config.js";
8
+ import { findProjectRoot } from "../utils/paths.js";
9
+ import { resolveServicePath, resolveSubdomainBases } from "../utils/service-urls.js";
10
+ import { readProjectName } from "../utils/shared-infra.js";
11
+ import { openUrl } from "../utils/url-actions.js";
12
+ /** The frontends of the current project, with the URL each one is published under. */
13
+ export async function listFrontendTargets(projectRoot) {
14
+ const { resources } = discoverResourcesWithProblems(projectRoot);
15
+ const plan = await getHostPortPlan(projectRoot, { inspectDocker: false });
16
+ const { appBase } = resolveSubdomainBases(plan?.ingressHttp, projectRoot);
17
+ return resources
18
+ .filter((resource) => resource.config?.appType === "frontend")
19
+ .map((resource) => ({
20
+ name: resource.name,
21
+ url: `${appBase}${resolveServicePath(resource)}`,
22
+ }));
23
+ }
24
+ /** True when the URL answers. A booting frontend needs a moment, so this is asked more than once. */
25
+ export async function isFrontendReady(url, timeoutMs = 2000) {
26
+ try {
27
+ const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
28
+ return response.status < 500;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ /** The first frontend that answers, or the first one when none is up yet. */
35
+ export async function pickReadyFrontend(targets, waitMs = 30000) {
36
+ if (targets.length === 0)
37
+ return undefined;
38
+ const deadline = Date.now() + waitMs;
39
+ while (true) {
40
+ for (const target of targets) {
41
+ if (await isFrontendReady(target.url))
42
+ return target;
43
+ }
44
+ if (Date.now() >= deadline)
45
+ return targets[0];
46
+ await new Promise((resolve) => setTimeout(resolve, 2000));
47
+ }
48
+ }
49
+ /**
50
+ * Opens the first ready frontend of the project in the browser. Returns the URL it opened, or
51
+ * null when the project has no frontend.
52
+ */
53
+ export async function openFirstFrontend(projectRootOverride) {
54
+ const projectRoot = projectRootOverride ?? findProjectRoot();
55
+ if (!projectRoot) {
56
+ console.log(chalk.yellow("Not in a Flisk project, so there is no frontend to open."));
57
+ console.log(chalk.gray("Run `flisk project --yes` first, or `flisk open` inside a project."));
58
+ return null;
59
+ }
60
+ const targets = await listFrontendTargets(projectRoot);
61
+ if (targets.length === 0) {
62
+ console.log(chalk.yellow("This project has no frontend service to open."));
63
+ console.log(chalk.gray("Start the stack with `flisk up` and add a frontend with `flisk resource`."));
64
+ return null;
65
+ }
66
+ const projectName = readProjectName(projectRoot);
67
+ showStep(`Opening the first frontend of ${projectName ?? "this project"}`);
68
+ for (const target of targets) {
69
+ console.log(chalk.gray(` - ${target.name}: ${target.url}`));
70
+ }
71
+ const first = await pickReadyFrontend(targets);
72
+ if (!first)
73
+ return null;
74
+ const opened = await openUrl(first.url);
75
+ console.log(opened
76
+ ? chalk.green(`\nOpened ${first.url}`)
77
+ : chalk.yellow(`\nCould not open a browser. Open ${first.url} yourself.`));
78
+ showDetail("Run `flisk open` again to open the next frontend.");
79
+ return first.url;
80
+ }
81
+ export const openCommand = new Command("open")
82
+ .description("Open the first frontend of the project in your browser")
83
+ .action(async () => {
84
+ await openFirstFrontend();
85
+ });
@@ -78,6 +78,14 @@ function getDefaultCoreServices() {
78
78
  .filter((f) => f.enabled_by_default && f.category === "core")
79
79
  .map((f) => f.name);
80
80
  }
81
+ /** The directory a template lands in: the requested path, or the template's repository name. */
82
+ export function templateTargetDir(templateName, targetDir) {
83
+ if (targetDir)
84
+ return targetDir;
85
+ const template = PROJECT_TEMPLATES[templateName];
86
+ const fromRepo = template?.repo.split("/").pop() ?? templateName;
87
+ return fromRepo.replace(/\.git$/, "");
88
+ }
81
89
  export async function cloneProjectTemplate(templateName, targetDir, runtimeNextSteps) {
82
90
  const template = PROJECT_TEMPLATES[templateName];
83
91
  if (!template) {
@@ -89,7 +97,7 @@ export async function cloneProjectTemplate(templateName, targetDir, runtimeNextS
89
97
  console.log(chalk.gray("\nOr run `flisk project` with no template for a blank project."));
90
98
  process.exit(1);
91
99
  }
92
- const dirName = targetDir || (template.repo.split("/").pop() ?? templateName).replace(/\.git$/, "");
100
+ const dirName = templateTargetDir(templateName, targetDir);
93
101
  if (!isPathSafe(dirName)) {
94
102
  errorFactories.invalidPath(dirName).exit();
95
103
  }
@@ -17,6 +17,7 @@ import { restartCommand } from "./restart.js";
17
17
  import { runtimeCommand } from "./runtime.js";
18
18
  import { stackCommand } from "./stack.js";
19
19
  import { stacksCommand } from "./stacks.js";
20
+ import { openCommand } from "./open.js";
20
21
  import { statusCommand } from "./status.js";
21
22
  import { uiCommand } from "./ui.js";
22
23
  import { upCommand } from "./up.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,
@@ -19,10 +19,11 @@ export function findFrontendUrl(output) {
19
19
  return undefined;
20
20
  }
21
21
  /**
22
- * Runs `flisk up` in the project directory and returns its output. The output is streamed to
23
- * the terminal as it arrives, so the stack keeps its live progress.
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.
24
25
  */
25
- export function runStack(binPath, cwd, env = process.env, spawnFn = spawn) {
26
+ export function runStack(binPath, cwd, env = process.env, spawnFn = spawn, onFrontendUrl) {
26
27
  return new Promise((resolve) => {
27
28
  const child = spawnFn(process.execPath, [binPath, "up"], {
28
29
  cwd,
@@ -30,10 +31,18 @@ export function runStack(binPath, cwd, env = process.env, spawnFn = spawn) {
30
31
  stdio: ["inherit", "pipe", "inherit"],
31
32
  });
32
33
  let output = "";
34
+ let announced = false;
33
35
  child.stdout?.on("data", (chunk) => {
34
36
  const text = String(chunk);
35
37
  output += text;
36
38
  process.stdout.write(text);
39
+ if (!announced) {
40
+ const url = findFrontendUrl(output);
41
+ if (url) {
42
+ announced = true;
43
+ onFrontendUrl?.(url);
44
+ }
45
+ }
37
46
  });
38
47
  child.on("error", (error) => resolve({ code: 1, output: `${output}\n${error.message}` }));
39
48
  child.on("close", (code) => resolve({ code, output }));
@@ -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,5 @@
1
+ import{createRequire as __tdkRequire}from'node:module';const require=__tdkRequire(import.meta.url);
2
+ import{Va as w,eb as A,f as P,m as T}from"./chunk-DTJ6JCHV.js";import{execFileSync as x}from"node:child_process";import{existsSync as G,mkdirSync as W,readFileSync as D,realpathSync as Y,writeFileSync as q}from"node:fs";import{join as S,resolve as V,sep as X}from"node:path";import{createServer as U}from"node:net";import{connect as K}from"node:net";function N(e,s=500){return new Promise(t=>{let r=K({port:e,host:"127.0.0.1"}),o=n=>{r.destroy(),t(n)};r.setTimeout(s,()=>o(!1)),r.once("connect",()=>o(!0)),r.once("error",()=>o(!1))})}var J={ingressHttp:{start:8080,end:8180},ingressHttps:{start:8443,end:8543},postgres:{start:15432,end:15532}},E={ingressHttp:"TDK_HTTP_PORT",ingressHttps:"TDK_HTTPS_PORT",postgres:"TDK_POSTGRES_PORT"},H={ingressHttp:80,ingressHttps:443,postgres:5432},k={ingressHttp:H.ingressHttp,ingressHttps:H.ingressHttps};function j(e,s){return e==="EACCES"&&s==="darwin"}async function b(e){return await N(e)?!1:new Promise(s=>{let t=U();t.once("error",r=>{s(j(r.code,process.platform))}),t.listen(e,"127.0.0.1",()=>t.close(()=>s(!0)))})}function B(e,s){let t=Number(s);if(!Number.isInteger(t)||t<1||t>65535)throw new Error(`${e} must be an integer between 1 and 65535 (received ${JSON.stringify(s)})`);return t}async function O(e={}){let s=e.env??process.env,t=e.isAvailable??b,r={ingressHttp:!1,ingressHttps:!1,postgres:!1},o={ingressHttp:"",ingressHttps:"",postgres:""},n={explicit:r},c=new Set;for(let a of["ingressHttp","ingressHttps","postgres"]){let f=s[E[a]],i=f!==void 0&&f.trim()!=="",u=e.ranges?.[a]??J[a],p=k[a],h=i?[B(E[a],f)]:[...p===void 0?[]:[p],...Array.from({length:u.end-u.start+1},(g,_)=>u.start+_)],d;for(let g of h)if(!c.has(g)){if(await t(g)){d=g;break}if(i)throw new Error(`${E[a]}=${g} is already in use. Choose a free host port and retry.`)}if(d===void 0)throw new Error(`No available host port in ${u.start}-${u.end} for ${a}. Set ${E[a]} to a free port and retry.`);c.add(d),n[a]=d,r[a]=i,o[a]=i?"explicit override":d===p?"requested port is free":`selected from fallback range ${u.start}-${u.end}`}return{...n,requested:{...H},reason:o}}function ue(e){return`Host ports: HTTP ${e.ingressHttp}, HTTPS ${e.ingressHttps}, Postgres ${e.postgres}`}var R=S(".tdk",".tdk-out","host-ports.json");function Q(e,s,t){return e.split(`
3
+ `).some(r=>{let[o="",n=""]=r.split("|"),c=o.toLowerCase(),a=s.toLowerCase();return(c.startsWith(`${a}_`)||c.startsWith(`${a}-`))&&n.includes(`:${t}->`)})}function F(e){let s=S(e,R);if(!G(s))return null;try{let t=JSON.parse(D(s,"utf-8"));if(!t||typeof t!="object")return null;let r=t;return!Number.isInteger(r.ingressHttp)||!Number.isInteger(r.ingressHttps)||!Number.isInteger(r.postgres)||!r.requested||!r.explicit||!r.reason?null:r}catch{return null}}function Se(e,s){let t=S(e,R);W(S(e,".tdk",".tdk-out"),{recursive:!0}),q(t,`${JSON.stringify(s,null,2)}
4
+ `,{encoding:"utf-8",mode:384})}function _e(e){process.env.TDK_HTTP_PORT=String(e.ingressHttp),process.env.TDK_HTTPS_PORT=String(e.ingressHttps),process.env.TDK_POSTGRES_PORT=String(e.postgres)}var z="com.docker.compose.project.working_dir";function Z(e,s,t){let r=new Set([V(s)]);try{r.add(Y(s))}catch{}return e.split(`
5
+ `).some(o=>{let[,n="",c=""]=o.split("|");return!c||!n.includes(`:${t}->`)?!1:[...r].some(a=>c===a||c.startsWith(`${a}${X}`))})}function Te(e,s,t=()=>x("docker",["ps","--format",`{{.Names}}|{{.Ports}}|{{.Label "${z}"}}`],{encoding:"utf-8",timeout:5e3})){let r=F(e);if(!r||r.ingressHttp===s)return s;try{if(Z(t(),e,r.ingressHttp))return r.ingressHttp}catch{}return s}async function Ee(e,s={}){let t=F(e),r=["TDK_HTTP_PORT","TDK_HTTPS_PORT","TDK_POSTGRES_PORT"].some(i=>process.env[i]);if(s.inspectDocker===!1&&t&&!r)return t;let o="";try{let i=JSON.parse(D(S(e,".tdk",T),"utf-8"));o=String(i?.project?.name??"tdk-project").replace(/-/g,"_").toLowerCase()}catch{o="tdk_project"}let n="";if(s.inspectDocker!==!1)try{n=x("docker",["ps","--format","{{.Names}}|{{.Ports}}"],{encoding:"utf-8",timeout:5e3})}catch{}let c=i=>Q(n,o,i),a=s.isPortFree??b,f=async i=>await a(i)||c(i);if(t&&!r){for(let i of[t.ingressHttp,t.ingressHttps,t.postgres])if(!await f(i))return O({isAvailable:f});for(let[i,u]of Object.entries(k)){let p=t[i];if(u!==void 0&&p!==u&&await f(u))return O({isAvailable:f})}return t}return O({isAvailable:f})}import{existsSync as ee,readFileSync as v}from"node:fs";import{join as l}from"node:path";var $=["postgres",P],L=["pre_alpha","alpha","beta","out_of_scope"];function te(e){return $.includes(e)}function C(e){try{return JSON.parse(v(l(e,".tdk",T),"utf-8"))}catch{return}}function m(e){try{return ee(e)?v(e,"utf-8"):void 0}catch{return}}function I(e,s){let t=o=>{let n=o;for(;n<e.length&&/\s/.test(e[n]);)n+=1;return n},r=0;for(;;){let o=e.indexOf(s,r);if(o===-1)return;let n=t(o+s.length);if(e[n]==="="&&(n=t(n+1),e[n]==="[")){let c=e.indexOf("]",n+1);return c===-1?void 0:[...e.slice(n+1,c).matchAll(/"([^"]+)"|'([^']+)'/g)].map(a=>a[1]??a[2]??"")}r=o+s.length}}function M(e){let s=l(e,".tdk",".tdk-out","Tiltfile"),t=m(s);if(t)return I(t,"ALWAYS_ENABLED_INFRA")}function se(e){let s=l(e,".tdk",".tdk-out","spec.master"),t=m(s);if(t!==void 0)return/"database-management"\s*:\s*True/.test(t)}function re(e){let s=l(e,".tdk",".tdk-out"),t=m(l(s,"Tiltfile")),r=m(l(s,"spec.master")),o=m(l(s,"tdk-cli-ext","discovery","config.star")),n=m(l(s,"tdk-cli-ext","engine","topologies","tilt","config","profiles.star"));if(!t||!r||!o||!n)return!1;let c=r.match(/PRE_ALPHA_RESOURCES\s*=\s*\{([^}]*)\}/)?.[1],a=I(o,"CORE_INFRA");return/FOCUS_MODE\s*,\s*FOCUS_ENABLED_ALL\s*,\s*FOCUS_ENABLED_RESOURCES\s*=\s*Config\.apply_focus\(cfg\)/.test(t)&&/if FOCUS_MODE and FOCUS_ENABLED_ALL\s*:/.test(t)&&/"[^"]+"\s*:\s*True|'[^']+'\s*:\s*True/.test(c??"")&&(a?.includes("postgres")??!1)&&/["']postgres["']\s*:\s*["']database-management["']/.test(o)&&/for infra in CORE_INFRA_EXPORT\s*:/.test(n)&&/needed\[INFRA_STACK_MAP_EXPORT\[infra\]\]\s*=\s*True/.test(n)}function ne(e){let s=C(e);if(!s)return!1;let t=s.phases??{};for(let o of L){let n=t[o]?.enabledStacks;if(Array.isArray(n)&&n.includes(P))return!0}let r=s.always_enabled_infra??A;return Array.isArray(r)&&r.includes(P)}function oe(e,s){let t=new Set;for(let n of $)t.add(n);for(let n of s)t.add(n.name),n.stack&&t.add(n.stack);let r=C(e);if(r){let n=r.phases??{};for(let a of L){let f=n[a]?.enabledStacks;if(Array.isArray(f))for(let i of f)t.add(i)}let c=r.always_enabled_infra??A;if(Array.isArray(c))for(let a of c)t.add(a)}let o=M(e);if(Array.isArray(o))for(let n of o)t.add(n);return t}function be(e,s=w(e)){let t=ne(e),r=M(e),o=Array.isArray(r)&&r.includes(P),n=se(e)===!0,c=t||o||n,a=!t&&re(e),f=oe(e,s),i=[],u=[];for(let d of s){let g=d.config?.dependsOn??[],_=!1;for(let y of g){if(te(y)){_=!0;continue}f.has(y)||u.push({resource:d.name,name:y})}_&&i.push(d.name)}let p=c||i.length>0,h=c?"feature":i.length>0?"dependsOn":"none";return{featureOn:c,featureOnFromProjectJson:t,featureOnFromTiltfile:o,featureOnFromSpecMaster:n,focusWouldEnableDatabaseManagement:a,dependsOnUsers:i,willStart:p,reason:h,unknownDependsOnNames:u}}export{O as a,ue as b,F as c,Se as d,_e as e,Te as f,Ee as g,$ as h,be as i};