@flisk-sh/flisk 1.3.192 → 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";
@@ -40,11 +41,14 @@ if (templateArg) {
40
41
  const binPath = process.argv[1];
41
42
  if (binPath && existsSync(destination)) {
42
43
  const { runStack } = await import("./utils/one-command.js");
43
- const { openUrl } = await import("./utils/url-actions.js");
44
+ const { openFirstFrontend } = await import("./commands/open.js");
44
45
  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}`);
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
+ });
48
52
  });
49
53
  }
50
54
  process.exit(0);
@@ -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
+ });
@@ -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,
@@ -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};