@flisk-sh/flisk 1.4.1 → 1.4.5
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/CHANGELOG.md +4 -0
- package/README.md +421 -92
- package/cli/dist/backend-languages/bun.js +5 -0
- package/cli/dist/backend-languages/go.js +3 -1
- package/cli/dist/cli.js +9 -0
- package/cli/dist/commands/completion.js +8 -1
- package/cli/dist/commands/config.js +130 -55
- package/cli/dist/commands/doctor.js +16 -15
- package/cli/dist/commands/logs.js +1 -0
- package/cli/dist/commands/project.js +1 -0
- package/cli/dist/commands/resource.js +39 -11
- package/cli/dist/commands/status.js +18 -9
- package/cli/dist/commands/up.js +102 -17
- package/cli/dist/frontend-frameworks/next.js +197 -0
- package/cli/dist/frontend-frameworks/react.js +81 -12
- package/cli/dist/frontend-frameworks/registry.js +5 -4
- package/cli/dist/frontend-frameworks/vue.js +82 -12
- package/cli/dist/generator/resource-templates.js +2 -4
- package/cli/dist/generator/template-engine.js +5 -3
- package/cli/dist/utils/cold-preflight.js +3 -2
- package/cli/dist/utils/config-explain.js +197 -0
- package/cli/dist/utils/demo-reset.js +22 -0
- package/cli/dist/utils/discovery-paths.js +7 -1
- package/cli/dist/utils/host-port-plan.js +8 -3
- package/cli/dist/utils/port-conflict-fix.js +16 -14
- package/cli/dist/utils/port-occupant.js +35 -0
- package/cli/dist/utils/project-features.js +7 -16
- package/cli/dist/utils/project-templates.js +8 -8
- package/cli/dist/utils/resource-features.js +5 -5
- package/cli/dist/utils/service-manifest.js +1 -0
- package/cli/dist/utils/service-urls.js +11 -0
- package/cli/dist/utils/startup-report.js +4 -3
- package/cli/dist/utils/tar.js +33 -28
- package/cli/dist/utils/tilt-process.js +4 -3
- package/cli/dist/utils/windows-doctor.js +18 -15
- package/cli/dist-bundle/{chunk-UJXR6MZ2.js → chunk-4J2FLFUA.js} +14 -7
- package/cli/dist-bundle/{chunk-SRN64OPO.js → chunk-BMHPPX2Q.js} +10 -10
- package/cli/dist-bundle/chunk-BP2M4OJ4.js +2 -0
- package/cli/dist-bundle/{chunk-TZNXL3IO.js → chunk-CGTWXWBU.js} +1 -1
- package/cli/dist-bundle/chunk-GPUJ55MI.js +3 -0
- package/cli/dist-bundle/{chunk-KJOCVAMQ.js → chunk-LUUJ4JB7.js} +1 -1
- package/cli/dist-bundle/chunk-PEZQEZVV.js +17 -0
- package/cli/dist-bundle/{chunk-7HR5PVOD.js → chunk-SFOFTVYM.js} +5 -5
- package/cli/dist-bundle/{chunk-RX6O3PG4.js → chunk-TIYTGN5R.js} +1 -1
- package/cli/dist-bundle/{chunk-BB5BZ37Y.js → chunk-U42XZWXB.js} +1 -1
- package/cli/dist-bundle/chunk-ULMQVD7J.js +122 -0
- package/cli/dist-bundle/chunk-V36PT2HF.js +24 -0
- package/cli/dist-bundle/{chunk-7IXBBU27.js → chunk-VMVTECCK.js} +1 -1
- package/cli/dist-bundle/{chunk-M2R464RM.js → chunk-Z5QEE4SW.js} +1 -1
- package/cli/dist-bundle/cli.js +607 -375
- package/cli/dist-bundle/cold-preflight-3BNDOLGS.js +2 -0
- package/cli/dist-bundle/{demo-stage-HIHO4VV3.js → demo-stage-G4W23BXW.js} +1 -1
- package/cli/dist-bundle/{extension-fetch-VEFSCG4B.js → extension-fetch-KIPYDIRM.js} +1 -1
- package/cli/dist-bundle/{open-W3QX35R6.js → open-HQFT6WDC.js} +1 -1
- package/cli/dist-bundle/project-JZO2CPZZ.js +2 -0
- package/cli/dist-bundle/{project-templates-EYSQG2EO.js → project-templates-IMJ3OX5I.js} +1 -1
- package/cli/dist-bundle/{services-RGTQ2LXC.js → services-GLFKZBBZ.js} +1 -1
- package/cli/dist-bundle/{tilt-port-owner-6DO7OJRR.js → tilt-port-owner-PJN4MPYE.js} +1 -1
- package/cli/package.json +1 -1
- package/package.json +2 -1
- package/runtime/engine/topologies/platform/docker/compose/compose.star +40 -4
- package/runtime/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +6 -0
- package/runtime/engine/topologies/platform/docker/dockerfile/next_dockerfile.star +83 -0
- package/runtime/engine/topologies/platform/docker/index.star +4 -0
- package/runtime/engine/topologies/platform/docker/layers/prisma/filesystem_setup.star +7 -0
- package/runtime/engine/topologies/platform/docker/networking/traefik.star +85 -46
- package/runtime/engine/topologies/tilt/common/utils_output.star +1 -0
- package/runtime/engine/topologies/tilt/generators/validators.star +1 -1
- package/runtime/engine/topologies/tilt/generators/vite/frontend.star +2 -2
- package/runtime/engine/topologies/tilt/generators/vite/generators.star +3 -0
- package/runtime/engine/topologies/tilt/manifest/constants.star +3 -0
- package/runtime/engine/topologies/tilt/manifest/parser.star +9 -3
- package/runtime/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +8 -3
- package/runtime/engine/topologies/tilt/resources/orchestrator/generators/dependencies/resolver.star +50 -24
- package/runtime/engine/topologies/tilt/resources/orchestrator/generators/env.star +11 -2
- package/runtime/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +23 -9
- package/cli/dist-bundle/chunk-DUNCELUK.js +0 -24
- package/cli/dist-bundle/chunk-K6SOCO6V.js +0 -3
- package/cli/dist-bundle/chunk-L6U555CJ.js +0 -61
- package/cli/dist-bundle/chunk-VRESY4NR.js +0 -17
- package/cli/dist-bundle/chunk-YL56J2WX.js +0 -2
- package/cli/dist-bundle/cold-preflight-BBDDHWRV.js +0 -2
- package/cli/dist-bundle/project-FW5LL2KM.js +0 -2
|
@@ -61,7 +61,7 @@ export function requireVerifiedDockerVersions(result) {
|
|
|
61
61
|
fix: result.fix ?? "Start Docker and ensure Docker Engine 25+ and Compose 2.20.2+ are installed.",
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
function usesPrisma(projectRoot) {
|
|
64
|
+
export function usesPrisma(projectRoot) {
|
|
65
65
|
try {
|
|
66
66
|
return discoverResourcesFromRoot(projectRoot).some((resource) => {
|
|
67
67
|
return resource.config?.featuresEnabled?.includes("prisma") ?? false;
|
|
@@ -120,9 +120,10 @@ export async function runColdPreflight(opts = {}) {
|
|
|
120
120
|
const bunPath = bunNeeded ? findOnPath("bun") : undefined;
|
|
121
121
|
if (bunPath) {
|
|
122
122
|
try {
|
|
123
|
+
// A cold start of bun can pass 3s on a busy machine; a timeout there reads as "not installed".
|
|
123
124
|
const version = execFileSync(bunPath, ["--version"], {
|
|
124
125
|
encoding: "utf-8",
|
|
125
|
-
timeout:
|
|
126
|
+
timeout: 10_000,
|
|
126
127
|
}).trim();
|
|
127
128
|
bun = bunMeetsFloor(version);
|
|
128
129
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Copyright (c) 2026 Flisk Landscape contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { FLISK_GENERATED_MARKER, ProjectConfigNotFoundError, verifyMasterConfigs, } from "../generator/template-engine.js";
|
|
6
|
+
import { AUTOGENERATED_DIR, PACKAGE_JSON, PROJECT_JSON, SERVICE_JSON, TILTIGNORE_FILE, } from "./constants.js";
|
|
7
|
+
import { FliskError } from "./errors.js";
|
|
8
|
+
import { isFliskGeneratedPath } from "./generated-paths.js";
|
|
9
|
+
/** Snapshot directory `flisk up` writes next to a service's `.autogenerated/` outputs. */
|
|
10
|
+
const CONTRACT_DIR = ".flisk-contract";
|
|
11
|
+
/** Generated service file that is exempt from the hand-edit check. */
|
|
12
|
+
const SERVICE_YAML = "service.yaml";
|
|
13
|
+
/**
|
|
14
|
+
* Explains one project file using the generated-path allowlist and the hand-edit check that `flisk up` runs.
|
|
15
|
+
* `file` resolves against the current directory, as shell paths do. Throws a FliskError for a path outside the
|
|
16
|
+
* project, a missing file, or a directory.
|
|
17
|
+
*/
|
|
18
|
+
export function explainConfigFile(projectRoot, file) {
|
|
19
|
+
const absolute = resolve(file);
|
|
20
|
+
const rel = relative(projectRoot, absolute).split(sep).join("/");
|
|
21
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute(rel)) {
|
|
22
|
+
throw new FliskError(`${file} is outside the project`, ["Pass a file inside the project root"]);
|
|
23
|
+
}
|
|
24
|
+
if (!existsSync(absolute)) {
|
|
25
|
+
throw new FliskError(`File not found: ${rel}`, ["Check the path and try again"]);
|
|
26
|
+
}
|
|
27
|
+
if (!statSync(absolute).isFile()) {
|
|
28
|
+
throw new FliskError(`Not a file: ${rel}`, ["Pass a file, not a directory"]);
|
|
29
|
+
}
|
|
30
|
+
const isEditedNow = isHandEditedNow(projectRoot, rel);
|
|
31
|
+
if (!isFliskGeneratedPath(rel)) {
|
|
32
|
+
return userOwned(rel, isEditedNow);
|
|
33
|
+
}
|
|
34
|
+
const segments = rel.split("/");
|
|
35
|
+
const autogenIndex = segments.indexOf(AUTOGENERATED_DIR);
|
|
36
|
+
if (autogenIndex >= 0) {
|
|
37
|
+
return explainServiceOutput(projectRoot, rel, segments, autogenIndex, isEditedNow);
|
|
38
|
+
}
|
|
39
|
+
// The allowlist names these paths, but the path alone does not show who wrote the file. Flisk creates
|
|
40
|
+
// package.json only when it is missing and never overwrites an existing one, so it is the user's to edit.
|
|
41
|
+
if (rel === PACKAGE_JSON) {
|
|
42
|
+
return {
|
|
43
|
+
file: rel,
|
|
44
|
+
ownership: "user-owned",
|
|
45
|
+
reason: "Flisk creates package.json only when it is missing and never overwrites an existing one.",
|
|
46
|
+
source: null,
|
|
47
|
+
driftOnUp: "none",
|
|
48
|
+
isEditedNow,
|
|
49
|
+
next: ["Edit it directly."],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// A root .tiltignore without Flisk's marker belongs to the team, and Flisk leaves it in place.
|
|
53
|
+
if (rel === TILTIGNORE_FILE &&
|
|
54
|
+
!readFileSync(absolute, "utf-8").includes(FLISK_GENERATED_MARKER)) {
|
|
55
|
+
return {
|
|
56
|
+
file: rel,
|
|
57
|
+
ownership: "user-owned",
|
|
58
|
+
reason: "This .tiltignore has no Flisk generated marker, so Flisk leaves it in place.",
|
|
59
|
+
source: null,
|
|
60
|
+
driftOnUp: "none",
|
|
61
|
+
isEditedNow,
|
|
62
|
+
next: ["Edit it directly."],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (isProjectOutput(rel)) {
|
|
66
|
+
return {
|
|
67
|
+
file: rel,
|
|
68
|
+
ownership: "generated",
|
|
69
|
+
reason: "Generated from .flisk/project.json.",
|
|
70
|
+
source: `.flisk/${PROJECT_JSON}`,
|
|
71
|
+
driftOnUp: "none",
|
|
72
|
+
isEditedNow,
|
|
73
|
+
next: [
|
|
74
|
+
`Edit .flisk/${PROJECT_JSON} instead.`,
|
|
75
|
+
"`flisk config regenerate` rewrites this file from project.json and discards edits.",
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
file: rel,
|
|
81
|
+
ownership: "generated",
|
|
82
|
+
reason: "Written by Flisk; this file does not record the input it is generated from.",
|
|
83
|
+
source: null,
|
|
84
|
+
driftOnUp: "none",
|
|
85
|
+
isEditedNow,
|
|
86
|
+
next: ["Do not edit it; Flisk writes it when it generates outputs."],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function userOwned(rel, isEditedNow) {
|
|
90
|
+
const base = { file: rel, ownership: "user-owned", isEditedNow };
|
|
91
|
+
if (rel === `.flisk/${PROJECT_JSON}`) {
|
|
92
|
+
return {
|
|
93
|
+
...base,
|
|
94
|
+
reason: "Flisk input: the project config that generated outputs are built from.",
|
|
95
|
+
source: null,
|
|
96
|
+
driftOnUp: "none",
|
|
97
|
+
next: ["Edit it, then run `flisk config regenerate`."],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (rel.split("/").pop() === SERVICE_JSON) {
|
|
101
|
+
return {
|
|
102
|
+
...base,
|
|
103
|
+
reason: "Flisk input: the service manifest that generated service files are built from.",
|
|
104
|
+
source: null,
|
|
105
|
+
driftOnUp: "none",
|
|
106
|
+
next: ["Edit it, then run `flisk up` to regenerate the service's outputs."],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
...base,
|
|
111
|
+
reason: "Not in Flisk's generated-file allowlist, so Flisk does not write it.",
|
|
112
|
+
source: null,
|
|
113
|
+
driftOnUp: "none",
|
|
114
|
+
next: ["Edit it directly."],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function explainServiceOutput(projectRoot, rel, segments, autogenIndex, isEditedNow) {
|
|
118
|
+
const serviceDir = segments.slice(0, autogenIndex).join("/");
|
|
119
|
+
const source = serviceDir ? `${serviceDir}/${SERVICE_JSON}` : SERVICE_JSON;
|
|
120
|
+
if (!existsSync(join(projectRoot, source))) {
|
|
121
|
+
return {
|
|
122
|
+
file: rel,
|
|
123
|
+
ownership: "unknown",
|
|
124
|
+
reason: `${AUTOGENERATED_DIR}/ has no ${SERVICE_JSON} beside it, so Flisk cannot tell which input produces this file.`,
|
|
125
|
+
source: null,
|
|
126
|
+
driftOnUp: "unknown",
|
|
127
|
+
isEditedNow,
|
|
128
|
+
next: ["Find where the file came from before editing it."],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const inside = segments.slice(autogenIndex + 1);
|
|
132
|
+
if (inside[0] === CONTRACT_DIR) {
|
|
133
|
+
return {
|
|
134
|
+
file: rel,
|
|
135
|
+
ownership: "generated",
|
|
136
|
+
reason: "Flisk's snapshot of the generated files; `flisk up` reads it to detect hand edits.",
|
|
137
|
+
source,
|
|
138
|
+
driftOnUp: "none",
|
|
139
|
+
isEditedNow,
|
|
140
|
+
next: ["Do not edit it; Flisk writes it when it generates outputs."],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const insidePath = inside.join("/");
|
|
144
|
+
if (insidePath === SERVICE_YAML) {
|
|
145
|
+
return {
|
|
146
|
+
file: rel,
|
|
147
|
+
ownership: "generated",
|
|
148
|
+
reason: "Runtime discovery file written by the Tilt extension; exempt from the hand-edit check.",
|
|
149
|
+
source,
|
|
150
|
+
driftOnUp: "none",
|
|
151
|
+
isEditedNow,
|
|
152
|
+
next: [`Do not edit it; edit ${source} instead.`],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const snapshot = join(projectRoot, serviceDir, AUTOGENERATED_DIR, CONTRACT_DIR, insidePath);
|
|
156
|
+
if (!existsSync(snapshot)) {
|
|
157
|
+
return {
|
|
158
|
+
file: rel,
|
|
159
|
+
ownership: "generated",
|
|
160
|
+
reason: `Generated from ${source}. Flisk has no snapshot of this file, so it cannot tell whether an edit would block flisk up.`,
|
|
161
|
+
source,
|
|
162
|
+
driftOnUp: "unknown",
|
|
163
|
+
isEditedNow,
|
|
164
|
+
next: ["Run `flisk up` to write the snapshots, then explain the file again."],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
file: rel,
|
|
169
|
+
ownership: "generated",
|
|
170
|
+
reason: `Generated from ${source}.`,
|
|
171
|
+
source,
|
|
172
|
+
driftOnUp: "blocks-up",
|
|
173
|
+
isEditedNow,
|
|
174
|
+
next: [
|
|
175
|
+
`Edit ${source} instead, then run \`flisk up\`.`,
|
|
176
|
+
"Discard a hand edit with `flisk config regenerate`.",
|
|
177
|
+
"Keep a hand edit on purpose with `flisk up --ignore-drift`.",
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function isProjectOutput(rel) {
|
|
182
|
+
return (rel.startsWith(".flisk/.flisk-out/") ||
|
|
183
|
+
rel === "Tiltfile" ||
|
|
184
|
+
/^TILT_[^/]+\.star$/.test(rel) ||
|
|
185
|
+
rel === TILTIGNORE_FILE);
|
|
186
|
+
}
|
|
187
|
+
/** The same hand-edit list `flisk up` blocks on, so explain and the gate cannot disagree. */
|
|
188
|
+
function isHandEditedNow(projectRoot, rel) {
|
|
189
|
+
try {
|
|
190
|
+
return verifyMasterConfigs(projectRoot).handEdited.includes(rel);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (error instanceof ProjectConfigNotFoundError)
|
|
194
|
+
return false;
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright (c) 2026 Flisk Landscape contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
/**
|
|
7
|
+
* Stop the stack of an existing demo folder before the folder is deleted. Deleting the folder alone leaves its
|
|
8
|
+
* containers running, and a standalone Traefik that stays up keeps port 80 for the next run.
|
|
9
|
+
* Returns true when a Flisk project was found and its `flisk down` ran.
|
|
10
|
+
*/
|
|
11
|
+
export function stopDemoStack(projectDir, runDown = runFliskDown) {
|
|
12
|
+
if (!existsSync(join(projectDir, ".flisk", "project.json")))
|
|
13
|
+
return false;
|
|
14
|
+
runDown(projectDir);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
function runFliskDown(projectDir) {
|
|
18
|
+
execFileSync(process.execPath, [process.argv[1] ?? "flisk", "down", "--yes"], {
|
|
19
|
+
cwd: projectDir,
|
|
20
|
+
stdio: "inherit",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -52,12 +52,18 @@ export function readDiscoveryPaths(projectRoot) {
|
|
|
52
52
|
const configPath = join(projectRoot, ".flisk", PROJECT_JSON);
|
|
53
53
|
if (!existsSync(configPath))
|
|
54
54
|
return DEFAULT_DISCOVERY_PATHS;
|
|
55
|
+
let reason = "discovery.paths must be an array of strings";
|
|
55
56
|
try {
|
|
56
57
|
const paths = JSON.parse(readFileSync(configPath, "utf-8"))?.discovery?.paths;
|
|
58
|
+
if (paths === undefined)
|
|
59
|
+
return DEFAULT_DISCOVERY_PATHS;
|
|
57
60
|
if (Array.isArray(paths) && paths.every((p) => typeof p === "string"))
|
|
58
61
|
return paths;
|
|
59
62
|
}
|
|
60
|
-
catch {
|
|
63
|
+
catch (error) {
|
|
64
|
+
reason = String(error);
|
|
65
|
+
}
|
|
66
|
+
console.warn(`Ignoring discovery configuration in ${configPath}: ${reason}; using default paths: ${DEFAULT_DISCOVERY_PATHS.join(", ")}`);
|
|
61
67
|
return DEFAULT_DISCOVERY_PATHS;
|
|
62
68
|
}
|
|
63
69
|
/** Whether Tilt's discovery would find a service.json in `resourceDir` given `patterns`. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2026 Flisk Landscape contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { createServer } from "node:net";
|
|
4
|
+
import { FliskError } from "./errors.js";
|
|
4
5
|
import { hasLocalListener } from "./port-listener-probe.js";
|
|
5
6
|
export const DEFAULT_HOST_PORT_RANGES = {
|
|
6
7
|
ingressHttp: { start: 8080, end: 8180 },
|
|
@@ -43,7 +44,7 @@ export async function isHostPortAvailable(port) {
|
|
|
43
44
|
function parsePort(name, value) {
|
|
44
45
|
const port = Number(value);
|
|
45
46
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
46
|
-
throw new
|
|
47
|
+
throw new FliskError(`${name} must be an integer between 1 and 65535 (received ${JSON.stringify(value)})`, ["Provide a valid port number between 1 and 65535"], 1);
|
|
47
48
|
}
|
|
48
49
|
return port;
|
|
49
50
|
}
|
|
@@ -74,11 +75,15 @@ export async function createHostPortPlan(options = {}) {
|
|
|
74
75
|
break;
|
|
75
76
|
}
|
|
76
77
|
if (explicit) {
|
|
77
|
-
throw new
|
|
78
|
+
throw new FliskError(`${ENV_KEYS[key]}=${port} is already in use. Choose a free host port and retry.`, [
|
|
79
|
+
"Choose a different free host port or unset the environment variable to use fallback ranges",
|
|
80
|
+
], 1);
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
if (chosen === undefined) {
|
|
81
|
-
throw new
|
|
84
|
+
throw new FliskError(`No available host port in ${range.start}-${range.end} for ${key}. Set ${ENV_KEYS[key]} to a free port and retry.`, [
|
|
85
|
+
`Free up port range ${range.start}-${range.end} or set ${ENV_KEYS[key]} to an available port`,
|
|
86
|
+
], 1);
|
|
82
87
|
}
|
|
83
88
|
reserved.add(chosen);
|
|
84
89
|
selected[key] = chosen;
|
|
@@ -1,25 +1,27 @@
|
|
|
1
|
-
// Copyright (c) 2026 Flisk Landscape contributors
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
1
|
/** The env var that moves each host port Flisk publishes. Services publish no host ports, so only these conflict. */
|
|
4
2
|
const PORT_ENV_VARS = {
|
|
5
3
|
"80": "FLISK_HTTP_PORT",
|
|
6
4
|
"443": "FLISK_HTTPS_PORT",
|
|
7
5
|
"5432": "FLISK_POSTGRES_PORT",
|
|
8
6
|
};
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
* the command that finds it is given instead.
|
|
12
|
-
*/
|
|
13
|
-
export function portConflictFix(message) {
|
|
7
|
+
/** The host port a conflict message is about, or null when the message is not a port conflict. */
|
|
8
|
+
export function conflictPort(message) {
|
|
14
9
|
// The raw Docker error can reach the report when the readiness check fails before the summary runs.
|
|
15
10
|
const match = message.match(/^host port (\d+) is already allocated$/) ??
|
|
16
11
|
message.match(/Bind for [^ ]+:(\d+) failed: port is already allocated/i);
|
|
17
|
-
|
|
12
|
+
return match ? Number(match[1]) : null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The next step for a host port another process already holds. When the occupant is known it is named; otherwise the
|
|
16
|
+
* command that finds it is given instead.
|
|
17
|
+
*/
|
|
18
|
+
export function portConflictFix(message, occupant = null) {
|
|
19
|
+
const port = conflictPort(message);
|
|
20
|
+
if (port === null)
|
|
18
21
|
return null;
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return `Fix: stop the process using port ${port} (${find}), or set ${envVar} to a free port and run flisk up`;
|
|
22
|
+
const envVar = PORT_ENV_VARS[String(port)];
|
|
23
|
+
const body = occupant
|
|
24
|
+
? `Fix: port ${port} is held by ${occupant.command} (pid ${occupant.pid}). Stop it`
|
|
25
|
+
: `Fix: stop the process using port ${port} (find it with: lsof -nP -iTCP:${port} -sTCP:LISTEN)`;
|
|
26
|
+
return envVar ? `${body}, or set ${envVar} to a free port and run flisk up` : body;
|
|
25
27
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Copyright (c) 2026 Flisk Landscape contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
const run = (command, args) => execFileSync(command, args, {
|
|
6
|
+
encoding: "utf-8",
|
|
7
|
+
stdio: "pipe",
|
|
8
|
+
timeout: 5000,
|
|
9
|
+
windowsHide: true,
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* The process listening on `port`, or null when it cannot be told (no lsof, Windows, nothing listens, or the process
|
|
13
|
+
* exited before ps could read it). Callers must treat null as "unknown", not as "free".
|
|
14
|
+
*/
|
|
15
|
+
export function findPortOccupant(port, platform = process.platform, commandRunner = run) {
|
|
16
|
+
if (platform === "win32")
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const [first] = commandRunner("lsof", ["-nP", "-t", `-iTCP:${port}`, "-sTCP:LISTEN"])
|
|
20
|
+
.split(/\r?\n/)
|
|
21
|
+
.map((line) => line.trim())
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
if (!first)
|
|
24
|
+
return null;
|
|
25
|
+
const pid = Number(first);
|
|
26
|
+
const name = commandRunner("ps", ["-p", first, "-o", "comm="]).trim();
|
|
27
|
+
if (!Number.isInteger(pid) || !name)
|
|
28
|
+
return null;
|
|
29
|
+
return { pid, command: basename(name) };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// lsof exits 1 when nothing listens, and may be missing.
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -70,22 +70,13 @@ export const PROJECT_FEATURES = {
|
|
|
70
70
|
},
|
|
71
71
|
};
|
|
72
72
|
/**
|
|
73
|
-
* Get all enabled project features
|
|
73
|
+
* Get all enabled project features: those on by default plus those switched
|
|
74
|
+
* on in `optional_infra`. Each feature appears once, in `PROJECT_FEATURES`
|
|
75
|
+
* order. Names that are not features are ignored, and `false` does not turn
|
|
76
|
+
* off a feature that is on by default.
|
|
74
77
|
*/
|
|
75
78
|
export function getEnabledProjectFeatures(optional_infra) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
enabled.push(key);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// Add optional features if enabled
|
|
83
|
-
if (optional_infra) {
|
|
84
|
-
for (const [key, isEnabled] of Object.entries(optional_infra)) {
|
|
85
|
-
if (isEnabled && PROJECT_FEATURES[key]) {
|
|
86
|
-
enabled.push(key);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return enabled;
|
|
79
|
+
return Object.entries(PROJECT_FEATURES)
|
|
80
|
+
.filter(([key, feature]) => feature.enabled_by_default || optional_infra?.[key] === true)
|
|
81
|
+
.map(([key]) => key);
|
|
91
82
|
}
|
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
/** Public starter repositories plus the bundled default product example. */
|
|
2
2
|
export const PROJECT_TEMPLATES = {
|
|
3
3
|
demo: {
|
|
4
|
-
repo: "https://github.com/flisk-sh/
|
|
4
|
+
repo: "https://github.com/flisk-sh/flisk-restaurant-example.git",
|
|
5
5
|
description: "The demo stack: restaurant ops with reservations, kitchen and floor control",
|
|
6
6
|
fixedDir: "flisk-demo",
|
|
7
7
|
},
|
|
8
8
|
restaurant: {
|
|
9
|
-
repo: "https://github.com/flisk-sh/
|
|
9
|
+
repo: "https://github.com/flisk-sh/flisk-restaurant-example.git",
|
|
10
10
|
description: "Restaurant ops: reservations, kitchen, floor control (Hono + Vite + Bun)",
|
|
11
11
|
},
|
|
12
12
|
saas: {
|
|
13
|
-
repo: "https://github.com/flisk-sh/
|
|
13
|
+
repo: "https://github.com/flisk-sh/flisk-saas-starter.git",
|
|
14
14
|
description: "SaaS starter: account dashboard and a working checkout button (Hono + Vite + Bun)",
|
|
15
15
|
},
|
|
16
16
|
erp: {
|
|
17
|
-
repo: "https://github.com/flisk-sh/
|
|
17
|
+
repo: "https://github.com/flisk-sh/flisk-erp-system.git",
|
|
18
18
|
description: "Enterprise ERP: 100 microservices across 7 business domains",
|
|
19
19
|
},
|
|
20
20
|
"user-management": {
|
|
21
|
-
repo: "https://github.com/flisk-sh/
|
|
21
|
+
repo: "https://github.com/flisk-sh/flisk-user-management.git",
|
|
22
22
|
description: "User/auth management services",
|
|
23
23
|
},
|
|
24
24
|
ecommerce: {
|
|
25
|
-
repo: "https://github.com/flisk-sh/
|
|
25
|
+
repo: "https://github.com/flisk-sh/flisk-ecommerce-example.git",
|
|
26
26
|
description: "Vue 3 storefront and Hono catalog API (needs Flisk 1.3.75+ for --framework vue)",
|
|
27
27
|
},
|
|
28
28
|
example: {
|
|
29
|
-
repo: "https://github.com/flisk-sh/
|
|
29
|
+
repo: "https://github.com/flisk-sh/flisk-example.git",
|
|
30
30
|
bundledPath: "examples/tdk-example",
|
|
31
31
|
description: "Hono + Postgres + NATS worker + Vite example with a routed write path",
|
|
32
32
|
},
|
|
33
33
|
"agent-stack": {
|
|
34
|
-
repo: "https://github.com/flisk-sh/
|
|
34
|
+
repo: "https://github.com/flisk-sh/flisk-agent-stack.git",
|
|
35
35
|
description: "AI agent stack: Bun agent + MCP tool server + Postgres + NATS",
|
|
36
36
|
},
|
|
37
37
|
};
|
|
@@ -160,15 +160,15 @@ export const RESOURCE_FEATURES = {
|
|
|
160
160
|
/**
|
|
161
161
|
* Get default features for a resource type
|
|
162
162
|
*/
|
|
163
|
-
export function getDefaultFeaturesForResourceType(
|
|
163
|
+
export function getDefaultFeaturesForResourceType(resourceType) {
|
|
164
164
|
// bring-your-own doesn't generate application code, so no features
|
|
165
|
-
if (
|
|
165
|
+
if (resourceType === BRING_YOUR_OWN_TYPE) {
|
|
166
166
|
return [];
|
|
167
167
|
}
|
|
168
168
|
const defaults = [];
|
|
169
169
|
for (const [key, feature] of Object.entries(RESOURCE_FEATURES)) {
|
|
170
170
|
if (feature.enabled_by_default &&
|
|
171
|
-
feature.applies_to.includes(
|
|
171
|
+
feature.applies_to.includes(resourceType) &&
|
|
172
172
|
// Only include if it's something users control via features array
|
|
173
173
|
["api-client", "env-config", "api-index", "prisma"].includes(key)) {
|
|
174
174
|
defaults.push(key);
|
|
@@ -179,6 +179,6 @@ export function getDefaultFeaturesForResourceType(appType) {
|
|
|
179
179
|
/**
|
|
180
180
|
* Get all generators for a resource type (for documentation)
|
|
181
181
|
*/
|
|
182
|
-
export function getGeneratorsForResourceType(
|
|
183
|
-
return Object.values(RESOURCE_FEATURES).filter((f) => f.applies_to.includes(
|
|
182
|
+
export function getGeneratorsForResourceType(resourceType) {
|
|
183
|
+
return Object.values(RESOURCE_FEATURES).filter((f) => f.applies_to.includes(resourceType));
|
|
184
184
|
}
|
|
@@ -87,6 +87,7 @@ const GENERATED_STRING_FIELDS = [
|
|
|
87
87
|
pattern: /^[A-Za-z0-9._/:@-]+$/,
|
|
88
88
|
expected: "an image reference such as ghcr.io/acme/app:1.0",
|
|
89
89
|
},
|
|
90
|
+
{ path: ["basePath"], pattern: /^\/[a-z0-9/-]*$/, expected: "a path such as /storefront" },
|
|
90
91
|
];
|
|
91
92
|
/**
|
|
92
93
|
* Errors for generated-output fields whose values could change the generated YAML. The value itself is not echoed, because it may
|
|
@@ -64,6 +64,17 @@ export function resolveServicePath(resource) {
|
|
|
64
64
|
}
|
|
65
65
|
return resource.config?.basePath ?? `/${resource.name}`;
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The ingress URL of one service (no /health suffix), or undefined for services with no Traefik route. This is where the service
|
|
69
|
+
* answers, unlike the Tilt UI port, which serves its own page for any path.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveServiceUrl(resource, ingressPort) {
|
|
72
|
+
const appType = resource.config?.appType;
|
|
73
|
+
if (appType !== "frontend" && !isApiServiceType(appType))
|
|
74
|
+
return undefined;
|
|
75
|
+
const { appBase, apiBase } = resolveSubdomainBases(ingressPort);
|
|
76
|
+
return `${appType === "frontend" ? appBase : apiBase}${resolveServicePath(resource)}`;
|
|
77
|
+
}
|
|
67
78
|
/**
|
|
68
79
|
* Health URLs for every routable service, matching what `flisk up` prints.
|
|
69
80
|
* Workers, libraries, SDKs and migrators have no Traefik route, so they are
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2026 Flisk Landscape contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { parseTiltResourceFailures, summarizeTiltBuildError } from "./doctor-runtime.js";
|
|
4
|
-
import { portConflictFix } from "./port-conflict-fix.js";
|
|
4
|
+
import { conflictPort, portConflictFix } from "./port-conflict-fix.js";
|
|
5
5
|
import { effectiveRuntimeStatus, isTiltResourcePending } from "./tilt-resource-state.js";
|
|
6
6
|
import { onlyEnabledResources } from "./up-readiness.js";
|
|
7
7
|
export function transitiveDependencies(name, dependsOn) {
|
|
@@ -52,11 +52,12 @@ export function isStartupStalled(jsonText, dependsOn, deferred = new Set()) {
|
|
|
52
52
|
const report = buildStartupReport(jsonText, dependsOn, deferred);
|
|
53
53
|
return report.failed.length > 0 && report.starting.length === 0;
|
|
54
54
|
}
|
|
55
|
-
export function formatStartupReport(report, timedOut) {
|
|
55
|
+
export function formatStartupReport(report, timedOut, occupantOf) {
|
|
56
56
|
const lines = [];
|
|
57
57
|
for (const { name, message } of report.failed) {
|
|
58
58
|
lines.push(`✗ ${name} failed: ${message}`);
|
|
59
|
-
const
|
|
59
|
+
const port = conflictPort(message);
|
|
60
|
+
const fix = portConflictFix(message, port !== null && occupantOf ? occupantOf(port) : null);
|
|
60
61
|
if (fix)
|
|
61
62
|
lines.push(` ${fix}`);
|
|
62
63
|
}
|
package/cli/dist/utils/tar.js
CHANGED
|
@@ -7,40 +7,45 @@ import { join } from "node:path";
|
|
|
7
7
|
/**
|
|
8
8
|
* Extracts a gzipped tarball (as an in-memory buffer) into destDir.
|
|
9
9
|
* Shells out to the system `tar` binary rather than pulling in a JS tar
|
|
10
|
-
* implementation - available by default on macOS and Linux.
|
|
10
|
+
* implementation - available by default on macOS and Linux. On Windows it
|
|
11
|
+
* runs `tar.exe`, which ships with Windows 10 (1803) and later; see
|
|
12
|
+
* docs/windows-hardware.md.
|
|
11
13
|
*/
|
|
12
14
|
export function extractTarball(data, destDir) {
|
|
13
15
|
const tempDir = mkdtempSync(join(tmpdir(), "flisk-ext-"));
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
windowsHide: true,
|
|
21
|
-
});
|
|
22
|
-
if (result.status !== 0) {
|
|
23
|
-
const fallbackDir = join(tempDir, "fallback");
|
|
24
|
-
mkdirSync(fallbackDir, { recursive: true });
|
|
25
|
-
const fallback = spawnSync(tarBin, ["xzf", tmpFile, "-C", fallbackDir], {
|
|
16
|
+
try {
|
|
17
|
+
const tmpFile = join(tempDir, "bundle.tar.gz");
|
|
18
|
+
writeFileSync(tmpFile, data);
|
|
19
|
+
mkdirSync(destDir, { recursive: true });
|
|
20
|
+
const tarBin = process.platform === "win32" ? "tar.exe" : "tar";
|
|
21
|
+
const result = spawnSync(tarBin, ["xzf", tmpFile, "-C", destDir, "--strip-components=1"], {
|
|
26
22
|
stdio: "pipe",
|
|
27
23
|
windowsHide: true,
|
|
28
24
|
});
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
25
|
+
if (result.status !== 0) {
|
|
26
|
+
const fallbackDir = join(tempDir, "fallback");
|
|
27
|
+
mkdirSync(fallbackDir, { recursive: true });
|
|
28
|
+
const fallback = spawnSync(tarBin, ["xzf", tmpFile, "-C", fallbackDir], {
|
|
29
|
+
stdio: "pipe",
|
|
30
|
+
windowsHide: true,
|
|
31
|
+
});
|
|
32
|
+
if (fallback.status !== 0) {
|
|
33
|
+
throw new Error(`tar extraction failed: ${fallback.stderr?.toString() || result.stderr?.toString() || "unknown error"}`);
|
|
34
|
+
}
|
|
35
|
+
const entries = readdirSync(fallbackDir);
|
|
36
|
+
const [only] = entries;
|
|
37
|
+
const sourceDir = entries.length === 1 &&
|
|
38
|
+
only !== undefined &&
|
|
39
|
+
existsSync(join(fallbackDir, only)) &&
|
|
40
|
+
statSync(join(fallbackDir, only)).isDirectory()
|
|
41
|
+
? join(fallbackDir, only)
|
|
42
|
+
: fallbackDir;
|
|
43
|
+
for (const entry of readdirSync(sourceDir)) {
|
|
44
|
+
renameSync(join(sourceDir, entry), join(destDir, entry));
|
|
45
|
+
}
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
|
-
|
|
48
|
+
finally {
|
|
49
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
46
51
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2026 Flisk Landscape contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { FliskError } from "./errors.js";
|
|
4
5
|
const run = (command, args) => execFileSync(command, args, { encoding: "utf-8", stdio: "pipe", windowsHide: true });
|
|
5
6
|
/** Refuse to target arbitrary listeners: only stop a Tilt process on the requested UI port. */
|
|
6
7
|
export function findTiltProcessIdsOnPort(port, platform = process.platform, commandRunner = run) {
|
|
@@ -15,7 +16,7 @@ export function findTiltProcessIdsOnPort(port, platform = process.platform, comm
|
|
|
15
16
|
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
16
17
|
}
|
|
17
18
|
catch {
|
|
18
|
-
throw new
|
|
19
|
+
throw new FliskError(`Unable to inspect Tilt listeners on port ${port} with PowerShell.`, ["Ensure PowerShell is available and accessible"], 1);
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
let pids;
|
|
@@ -30,7 +31,7 @@ export function findTiltProcessIdsOnPort(port, platform = process.platform, comm
|
|
|
30
31
|
const stderr = commandErr.stderr?.toString().trim();
|
|
31
32
|
if (commandErr?.status === 1 && !stderr)
|
|
32
33
|
return [];
|
|
33
|
-
throw new
|
|
34
|
+
throw new FliskError(`Unable to inspect listeners on port ${port}. Install lsof or resolve the lsof error before using --force.`, ["Install lsof or verify permissions before running with --force"], 1);
|
|
34
35
|
}
|
|
35
36
|
return pids.filter((pid) => {
|
|
36
37
|
try {
|
|
@@ -38,7 +39,7 @@ export function findTiltProcessIdsOnPort(port, platform = process.platform, comm
|
|
|
38
39
|
return name.split(/[\\/]/).at(-1) === "tilt";
|
|
39
40
|
}
|
|
40
41
|
catch (err) {
|
|
41
|
-
throw new
|
|
42
|
+
throw new FliskError(`Unable to identify the process listening on port ${port}.`, ["Check process inspection permissions"], 1);
|
|
42
43
|
}
|
|
43
44
|
});
|
|
44
45
|
}
|