@flisk-sh/flisk 1.4.2 → 1.4.6

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 (134) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +148 -49
  3. package/cli/dist/backend-languages/bun.js +5 -0
  4. package/cli/dist/backend-languages/go.js +3 -1
  5. package/cli/dist/cli.js +9 -0
  6. package/cli/dist/commands/completion.js +8 -1
  7. package/cli/dist/commands/config.js +130 -55
  8. package/cli/dist/commands/doctor.js +66 -27
  9. package/cli/dist/commands/down.js +33 -1
  10. package/cli/dist/commands/logs.js +39 -1
  11. package/cli/dist/commands/project.js +12 -0
  12. package/cli/dist/commands/resource.js +39 -11
  13. package/cli/dist/commands/status.js +57 -19
  14. package/cli/dist/commands/up.js +221 -18
  15. package/cli/dist/commands/upgrade.js +2 -2
  16. package/cli/dist/frontend-frameworks/next.js +197 -0
  17. package/cli/dist/frontend-frameworks/react.js +81 -12
  18. package/cli/dist/frontend-frameworks/registry.js +5 -4
  19. package/cli/dist/frontend-frameworks/vue.js +82 -12
  20. package/cli/dist/generator/resource-templates.js +2 -4
  21. package/cli/dist/generator/template-engine.js +5 -3
  22. package/cli/dist/utils/bun-requirement.js +7 -4
  23. package/cli/dist/utils/cold-preflight.js +55 -34
  24. package/cli/dist/utils/config-explain.js +197 -0
  25. package/cli/dist/utils/demo-reset.js +22 -0
  26. package/cli/dist/utils/discovery-paths.js +7 -1
  27. package/cli/dist/utils/doctor-native.js +96 -0
  28. package/cli/dist/utils/host-port-plan.js +8 -3
  29. package/cli/dist/utils/native-project.js +41 -0
  30. package/cli/dist/utils/native-runtime.js +513 -0
  31. package/cli/dist/utils/native-service.js +95 -0
  32. package/cli/dist/utils/native-up.js +156 -0
  33. package/cli/dist/utils/platform.js +1 -1
  34. package/cli/dist/utils/port-conflict-fix.js +16 -14
  35. package/cli/dist/utils/port-occupant.js +35 -0
  36. package/cli/dist/utils/project-features.js +7 -16
  37. package/cli/dist/utils/project-templates.js +9 -9
  38. package/cli/dist/utils/resource-features.js +5 -5
  39. package/cli/dist/utils/service-manifest.js +20 -3
  40. package/cli/dist/utils/service-urls.js +11 -0
  41. package/cli/dist/utils/services.js +15 -2
  42. package/cli/dist/utils/startup-report.js +4 -3
  43. package/cli/dist/utils/tar.js +33 -28
  44. package/cli/dist/utils/tilt-logs.js +8 -0
  45. package/cli/dist/utils/tilt-process.js +4 -3
  46. package/cli/dist/utils/windows-doctor.js +18 -15
  47. package/cli/dist-bundle/chunk-BIVAAWBX.js +24 -0
  48. package/cli/dist-bundle/{chunk-YNCBBJ4V.js → chunk-C4UFBXRD.js} +1 -1
  49. package/cli/dist-bundle/{chunk-53L2JN7G.js → chunk-CAEMWNPF.js} +1 -1
  50. package/cli/dist-bundle/chunk-CDKEHFW6.js +35 -0
  51. package/cli/dist-bundle/{chunk-5QBYM33Q.js → chunk-DPQK5TIS.js} +1 -1
  52. package/cli/dist-bundle/chunk-ETXZ5LUG.js +18 -0
  53. package/cli/dist-bundle/chunk-GPUJ55MI.js +3 -0
  54. package/cli/dist-bundle/chunk-GT76VMUL.js +130 -0
  55. package/cli/dist-bundle/{chunk-IGN5UC3P.js → chunk-IDNES5PE.js} +14 -7
  56. package/cli/dist-bundle/{chunk-MWOVYIBB.js → chunk-MUDE4DZD.js} +5 -5
  57. package/cli/dist-bundle/chunk-QEBN6ZVQ.js +2 -0
  58. package/cli/dist-bundle/{chunk-I7TGIK5N.js → chunk-R7DWXISM.js} +1 -1
  59. package/cli/dist-bundle/{chunk-DU2BN46C.js → chunk-U5FCQF42.js} +1 -1
  60. package/cli/dist-bundle/{chunk-3XODDLRJ.js → chunk-WCGBVETR.js} +1 -1
  61. package/cli/dist-bundle/cli.js +626 -389
  62. package/cli/dist-bundle/cold-preflight-KUWOL2Q2.js +2 -0
  63. package/cli/dist-bundle/{demo-stage-OHNTP2UJ.js → demo-stage-QCC4IWXR.js} +1 -1
  64. package/cli/dist-bundle/{extension-fetch-VEFSCG4B.js → extension-fetch-KIPYDIRM.js} +1 -1
  65. package/cli/dist-bundle/{open-XROJ6YEY.js → open-AZNX4AC5.js} +1 -1
  66. package/cli/dist-bundle/project-5SWIGCNP.js +2 -0
  67. package/cli/dist-bundle/{project-templates-EYSQG2EO.js → project-templates-2VQPUBSQ.js} +1 -1
  68. package/cli/dist-bundle/{services-BIMF6MVU.js → services-EY5WKPHQ.js} +1 -1
  69. package/cli/dist-bundle/{tilt-port-owner-ZY5M2JC4.js → tilt-port-owner-UBU6JWOA.js} +1 -1
  70. package/cli/package.json +1 -1
  71. package/examples/{tdk-example → flisk-example}/README.md +2 -2
  72. package/package.json +2 -2
  73. package/runtime/discovery/AGENTS.md +1 -1
  74. package/runtime/discovery/TILT_DISCOVERY.star +1 -1
  75. package/runtime/discovery/deterministic.star +1 -1
  76. package/runtime/engine/AGENTS.md +3 -3
  77. package/runtime/engine/README.md +1 -1
  78. package/runtime/engine/REFACTORING_MANIFEST_SYSTEM.md +1 -1
  79. package/runtime/engine/assets/docker/wake-gateway/decide.ts +1 -1
  80. package/runtime/engine/assets/docker/wake-gateway/server.ts +2 -2
  81. package/runtime/engine/assets/scripts/mac-m1-resource-monitor.sh +6 -6
  82. package/runtime/engine/schemas/service-schema.json +99 -21
  83. package/runtime/engine/scripts/debug-container.sh +1 -1
  84. package/runtime/engine/scripts/generate-manifest.sh +2 -2
  85. package/runtime/engine/topologies/platform/docker/compose/compose.star +40 -4
  86. package/runtime/engine/topologies/platform/docker/compose/traefik_standalone.star +19 -4
  87. package/runtime/engine/topologies/platform/docker/constants.star +2 -2
  88. package/runtime/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +6 -0
  89. package/runtime/engine/topologies/platform/docker/dockerfile/next_dockerfile.star +83 -0
  90. package/runtime/engine/topologies/platform/docker/index.star +4 -0
  91. package/runtime/engine/topologies/platform/docker/layers/prisma/filesystem_setup.star +7 -0
  92. package/runtime/engine/topologies/platform/docker/networking/traefik.star +85 -46
  93. package/runtime/engine/topologies/platform/docker/networking/traefik_static_routes.star +2 -2
  94. package/runtime/engine/topologies/tilt/common/utils_output.star +1 -0
  95. package/runtime/engine/topologies/tilt/generators/validators.star +1 -1
  96. package/runtime/engine/topologies/tilt/generators/vite/frontend.star +2 -2
  97. package/runtime/engine/topologies/tilt/generators/vite/generators.star +3 -0
  98. package/runtime/engine/topologies/tilt/manifest/constants.star +3 -0
  99. package/runtime/engine/topologies/tilt/manifest/mapper.star +1 -1
  100. package/runtime/engine/topologies/tilt/manifest/parser.star +9 -3
  101. package/runtime/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +9 -4
  102. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/dependencies/resolver.star +50 -24
  103. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/env.star +11 -2
  104. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +23 -9
  105. package/runtime/ext/ide-components/README.md +1 -1
  106. package/runtime/ext/ui-enhancements/extension.yaml +2 -2
  107. package/runtime/specs/specs/TILT_SERVICE_DEFAULTS.star +1 -1
  108. package/runtime/specs/specs/TILT_TECH_STACK.star +1 -1
  109. package/cli/dist-bundle/chunk-43XWL2PB.js +0 -61
  110. package/cli/dist-bundle/chunk-5F7YP5IR.js +0 -17
  111. package/cli/dist-bundle/chunk-ETKUVKJ2.js +0 -35
  112. package/cli/dist-bundle/chunk-K6SOCO6V.js +0 -3
  113. package/cli/dist-bundle/chunk-YHYRGWUH.js +0 -24
  114. package/cli/dist-bundle/chunk-YL56J2WX.js +0 -2
  115. package/cli/dist-bundle/cold-preflight-ZOPR7PWJ.js +0 -2
  116. package/cli/dist-bundle/project-7PTIOHGF.js +0 -2
  117. /package/examples/{tdk-example → flisk-example}/LICENSE +0 -0
  118. /package/examples/{tdk-example → flisk-example}/TILT_SERVICE_DEFAULTS.star +0 -0
  119. /package/examples/{tdk-example → flisk-example}/TILT_TECH_STACK.star +0 -0
  120. /package/examples/{tdk-example → flisk-example}/Tiltfile +0 -0
  121. /package/examples/{tdk-example → flisk-example}/services/platform/messaging/docker-compose.yml +0 -0
  122. /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/package.json +0 -0
  123. /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/service.json +0 -0
  124. /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/src/index.ts +0 -0
  125. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/index.html +0 -0
  126. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/package.json +0 -0
  127. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/public/.gitkeep +0 -0
  128. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/service.json +0 -0
  129. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/main.tsx +0 -0
  130. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/orders.tsx +0 -0
  131. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/style.css +0 -0
  132. /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/package.json +0 -0
  133. /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/service.json +0 -0
  134. /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/src/index.ts +0 -0
@@ -0,0 +1,156 @@
1
+ // Copyright (c) 2026 Flisk Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ import { existsSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import chalk from "chalk";
6
+ import { readEnvFileValues } from "./env-selection.js";
7
+ import { showError } from "./errors.js";
8
+ import { formatCount } from "./formatting.js";
9
+ import { dependencyWaves, findMissingPrograms, formatPrefix, isPortListening, isProcessAlive, NATIVE_DIR, NativeSupervisor, nativeRunStatePath, planNativeRun, readNativeRunState, stopNativeRun, } from "./native-runtime.js";
10
+ import { findPortOccupant } from "./port-occupant.js";
11
+ /** Variables `${VAR}` in a service's env may reference: the env file of the run, overridden by the shell. */
12
+ function runVariables(projectRoot, envFilePath) {
13
+ const path = envFilePath ?? join(projectRoot, ".env");
14
+ const fromFile = existsSync(path) ? Object.fromEntries(readEnvFileValues(path)) : {};
15
+ const fromShell = Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
16
+ return { ...fromFile, ...fromShell };
17
+ }
18
+ /**
19
+ * `flisk up` for a selection where every service declares `command`: no Docker, no Compose, no Tilt and no generated files.
20
+ * Resolves only when the run ends (Ctrl+C, `flisk down`, or a failed start, which exits 1).
21
+ */
22
+ export async function runNativeUp(input) {
23
+ const { projectRoot, selected, emit } = input;
24
+ const exit = input.exit ?? ((code) => process.exit(code));
25
+ const quiet = Boolean(input.quiet);
26
+ const fail = (code, message, data = {}) => {
27
+ emit?.({ ok: false, runtime: "native", ...data }, [{ code, message }]);
28
+ showError(message);
29
+ return exit(1);
30
+ };
31
+ const vars = runVariables(projectRoot, input.envFilePath);
32
+ const plan = planNativeRun(selected, vars);
33
+ if (plan.errors.length > 0)
34
+ fail("INVALID_SERVICE_CONFIG", plan.errors.join("\n"));
35
+ if (plan.missingVars.length > 0 && !quiet) {
36
+ console.warn(chalk.yellow(`Unset variables expanded to an empty value: ${plan.missingVars.join(", ")}. Set them in .env or the shell.`));
37
+ }
38
+ const missing = findMissingPrograms(plan.services);
39
+ if (missing.length > 0) {
40
+ fail("NATIVE_COMMAND_NOT_FOUND", missing
41
+ .map(({ name, program }) => `service "${name}" runs ${program}, which is not installed (not found on PATH). Install it, or change "command" in its service.json.`)
42
+ .join("\n"));
43
+ }
44
+ const serviceNames = plan.services.map((service) => service.name);
45
+ const startOrder = dependencyWaves(plan.services);
46
+ const description = input.only
47
+ ? `${formatCount(input.only.length, "requested service")}${input.dependencies.length > 0 ? ` plus ${input.dependencies.join(", ")}` : ""}`
48
+ : input.stackName
49
+ ? `stack "${input.stackName}"`
50
+ : formatCount(serviceNames.length, "service");
51
+ if (!quiet) {
52
+ console.log(chalk.blue(`${input.dryRun ? "Would start" : "Starting"} ${description} as local processes (no Docker, no Tilt)...`));
53
+ for (const service of plan.services) {
54
+ const after = service.dependsOn.length > 0 ? ` (after ${service.dependsOn.join(", ")})` : "";
55
+ console.log(chalk.gray(` - ${service.name}: ${service.command}${after}`));
56
+ }
57
+ }
58
+ if (input.dryRun) {
59
+ emit?.({
60
+ ok: true,
61
+ dryRun: true,
62
+ runtime: "native",
63
+ stack: input.stackName ?? null,
64
+ services: serviceNames,
65
+ ...(input.only ? { requested: input.only, declaredDependencies: input.dependencies } : {}),
66
+ startOrder,
67
+ });
68
+ if (!quiet)
69
+ console.log(chalk.yellow("Dry run: not starting services"));
70
+ return;
71
+ }
72
+ const running = readNativeRunState(projectRoot);
73
+ if (running && running.pid !== process.pid && isProcessAlive(running.pid)) {
74
+ if (!input.force) {
75
+ const message = `This project is already running as local processes (flisk up, pid ${running.pid}).`;
76
+ emit?.({ ok: true, alreadyRunning: true, runtime: "native", pid: running.pid });
77
+ if (!quiet) {
78
+ console.log(chalk.green(message));
79
+ console.log(chalk.gray("Stop it with flisk down, or pass --force to restart it."));
80
+ }
81
+ return;
82
+ }
83
+ }
84
+ if (running) {
85
+ // A previous run that was replaced (--force) or killed without cleaning up: stop whatever it left behind.
86
+ await stopNativeRun(projectRoot);
87
+ }
88
+ for (const service of plan.services) {
89
+ if (service.port !== undefined && (await isPortListening(service.port))) {
90
+ const occupant = findPortOccupant(service.port);
91
+ fail("PORT_IN_USE", `Port ${service.port} for service "${service.name}" is already in use${occupant ? ` by ${occupant.command} (pid ${occupant.pid})` : ""}. Stop it, or change "port" in the service.json.`);
92
+ }
93
+ }
94
+ const width = Math.max(...serviceNames.map((name) => name.length));
95
+ const prefix = (name) => formatPrefix(name, serviceNames.indexOf(name), width);
96
+ const supervisor = new NativeSupervisor(projectRoot, plan.services, {
97
+ onLine: quiet ? undefined : (name, line) => console.log(`${prefix(name)}${line}`),
98
+ onNote: quiet
99
+ ? undefined
100
+ : (name, message) => console.log(`${prefix(name)}${chalk.gray(`[flisk] ${message}`)}`),
101
+ }, { readyTimeoutMs: input.readyTimeoutMs, baseEnv: vars });
102
+ let stopping;
103
+ const stopAndExit = (code) => {
104
+ stopping ??= (async () => {
105
+ if (!quiet)
106
+ console.log(chalk.gray("\nStopping local processes..."));
107
+ await supervisor.stop();
108
+ exit(code);
109
+ })();
110
+ };
111
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
112
+ process.on(signal, () => stopAndExit(0));
113
+ }
114
+ // If every service exits for good, nothing keeps `flisk up` alive; it ends and must not leave a run.json behind.
115
+ process.once("exit", () => {
116
+ if (readNativeRunState(projectRoot)?.pid === process.pid) {
117
+ rmSync(nativeRunStatePath(projectRoot), { force: true });
118
+ }
119
+ });
120
+ const failures = await supervisor.start();
121
+ if (stopping)
122
+ return;
123
+ if (failures.length > 0) {
124
+ const message = [
125
+ "Local processes did not start:",
126
+ ...failures.map((failure) => ` ✗ ${failure.name}: ${failure.reason}`),
127
+ `Output of every service: ${join(NATIVE_DIR, "logs")}/<service>.log`,
128
+ ].join("\n");
129
+ emit?.({ ok: false, runtime: "native", failures }, [{ code: "UP_RESOURCE_FAILED", message }]);
130
+ showError(message);
131
+ await supervisor.stop();
132
+ exit(1);
133
+ return;
134
+ }
135
+ emit?.({
136
+ ok: true,
137
+ runtime: "native",
138
+ stack: input.stackName ?? null,
139
+ services: serviceNames,
140
+ ...(input.only ? { requested: input.only, dependencies: input.dependencies } : {}),
141
+ urls: Object.fromEntries(plan.services.flatMap((service) => (service.url ? [[service.name, service.url]] : []))),
142
+ });
143
+ if (!quiet) {
144
+ const lines = ["Flisk is up (local processes, no Docker).", "Services:"];
145
+ for (const service of plan.services) {
146
+ const health = service.healthUrl ? ` (health: ${service.healthUrl})` : "";
147
+ lines.push(` ✓ ${service.name}${service.url ? ` → ${service.url}` : ""}${health}`);
148
+ }
149
+ lines.push("Stop: Ctrl+C here, or flisk down from another terminal");
150
+ lines.push(`Logs: flisk logs --service <name>, or ${join(NATIVE_DIR, "logs")}/<service>.log`);
151
+ for (const line of lines)
152
+ console.log(chalk.blue(line));
153
+ }
154
+ // The services keep the process alive; `flisk up` ends when it is stopped.
155
+ await new Promise(() => { });
156
+ }
@@ -33,7 +33,7 @@ export function binaryAssetNameFor(platformName, architecture) {
33
33
  return null;
34
34
  if (osName === "windows" && archName !== "amd64")
35
35
  return null;
36
- const base = `tdk-${osName}-${archName}`;
36
+ const base = `flisk-${osName}-${archName}`;
37
37
  return osName === "windows" ? `${base}.exe` : base;
38
38
  }
39
39
  export function binaryAssetName() {
@@ -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
- * The next step for a host port another process already holds. The occupant is not named: finding it needs the OS, so
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
- if (!match)
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 port = match[1];
20
- const find = `find it with: lsof -nP -iTCP:${port} -sTCP:LISTEN`;
21
- const envVar = PORT_ENV_VARS[port];
22
- if (!envVar)
23
- return `Fix: stop the process using port ${port} (${find})`;
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
- const enabled = [];
77
- for (const [key, feature] of Object.entries(PROJECT_FEATURES)) {
78
- if (feature.enabled_by_default) {
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/tdk-restaurant-example.git",
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/tdk-restaurant-example.git",
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/tdk-saas-starter.git",
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/tdk-erp-system.git",
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/tdk-user-management.git",
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/tdk-ecommerce-example.git",
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/tdk-example.git",
30
- bundledPath: "examples/tdk-example",
29
+ repo: "https://github.com/flisk-sh/flisk-example.git",
30
+ bundledPath: "examples/flisk-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/tdk-agent-stack.git",
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(appType) {
163
+ export function getDefaultFeaturesForResourceType(resourceType) {
164
164
  // bring-your-own doesn't generate application code, so no features
165
- if (appType === BRING_YOUR_OWN_TYPE) {
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(appType) &&
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(appType) {
183
- return Object.values(RESOURCE_FEATURES).filter((f) => f.applies_to.includes(appType));
182
+ export function getGeneratorsForResourceType(resourceType) {
183
+ return Object.values(RESOURCE_FEATURES).filter((f) => f.applies_to.includes(resourceType));
184
184
  }
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) 2026 Flisk Landscape contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { existsSync, readFileSync } from "node:fs";
4
+ import { declaresCommand, validateNativeServiceFields } from "./native-service.js";
4
5
  import { validateSmoke } from "./smoke.js";
5
6
  export const SERVICE_MANIFEST_SCHEMA_VERSION = 1;
6
7
  export const SERVICE_MANIFEST_SCHEMA_URL = "https://flisk.sh/schema.service.json";
@@ -24,12 +25,14 @@ const KNOWN_SERVICE_FIELDS = new Set([
24
25
  "basePath",
25
26
  "build",
26
27
  "buildContext",
28
+ "command",
27
29
  "databaseName",
28
30
  "dependencies",
29
31
  "dev",
30
32
  "dependsOn",
31
33
  "dockerfile",
32
34
  "enabled",
35
+ "env",
33
36
  "exposeViaProxy",
34
37
  "featuresEnabled",
35
38
  "framework",
@@ -87,6 +90,7 @@ const GENERATED_STRING_FIELDS = [
87
90
  pattern: /^[A-Za-z0-9._/:@-]+$/,
88
91
  expected: "an image reference such as ghcr.io/acme/app:1.0",
89
92
  },
93
+ { path: ["basePath"], pattern: /^\/[a-z0-9/-]*$/, expected: "a path such as /storefront" },
90
94
  ];
91
95
  /**
92
96
  * Errors for generated-output fields whose values could change the generated YAML. The value itself is not echoed, because it may
@@ -182,9 +186,21 @@ export function validateServiceManifest(value, displayPath) {
182
186
  }
183
187
  const manifest = value;
184
188
  const errors = [];
185
- for (const field of ["appName", "appType", "stack", "schemaVersion"]) {
186
- if (!(field in manifest))
187
- errors.push(`${displayPath}.${field}: required field is missing`);
189
+ // A `command` service needs only a name and schemaVersion: it runs as a local process, so it has no app type, stack or
190
+ // generated output to describe (the minimal contract in utils/native-service.ts). Every other service keeps the full set.
191
+ if (declaresCommand(manifest)) {
192
+ if (!("appName" in manifest) && typeof manifest.name !== "string") {
193
+ errors.push(`${displayPath}.name: required field is missing`);
194
+ }
195
+ if (!("schemaVersion" in manifest)) {
196
+ errors.push(`${displayPath}.schemaVersion: required field is missing`);
197
+ }
198
+ }
199
+ else {
200
+ for (const field of ["appName", "appType", "stack", "schemaVersion"]) {
201
+ if (!(field in manifest))
202
+ errors.push(`${displayPath}.${field}: required field is missing`);
203
+ }
188
204
  }
189
205
  for (const field of ["appName", "appType", "stack"]) {
190
206
  if (field in manifest && typeof manifest[field] !== "string") {
@@ -205,6 +221,7 @@ export function validateServiceManifest(value, displayPath) {
205
221
  errors.push(`${displayPath}.dev.liveReload: expected true or false`);
206
222
  }
207
223
  errors.push(...validateGeneratedStringFields(manifest, displayPath));
224
+ errors.push(...validateNativeServiceFields(manifest).map((error) => `${displayPath}: ${error}`));
208
225
  if ("smoke" in manifest) {
209
226
  for (const message of validateSmoke(manifest.smoke))
210
227
  errors.push(`${displayPath}.${message}`);
@@ -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
@@ -3,9 +3,10 @@
3
3
  import fs, { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { Cache, createCacheValidator } from "./cache.js";
6
- import { AUTOGENERATED_FILE_PATTERNS, DOCKER_COMPOSE_FILE, PACKAGE_JSON, SERVICE_JSON, SKIP_DIRECTORIES, } from "./constants.js";
6
+ import { AUTOGENERATED_FILE_PATTERNS, BRING_YOUR_OWN_TYPE, DOCKER_COMPOSE_FILE, PACKAGE_JSON, SERVICE_JSON, SKIP_DIRECTORIES, } from "./constants.js";
7
7
  import { errorFactories } from "./errors.js";
8
8
  import { formatCount } from "./formatting.js";
9
+ import { declaresCommand, validateNativeServiceFields } from "./native-service.js";
9
10
  import { findProjectRoot } from "./paths.js";
10
11
  import { getInvalidServiceRoutePathFields, validateGeneratedStringFields, validateServiceManifest, } from "./service-manifest.js";
11
12
  import { deriveStackStatus } from "./stack-status.js";
@@ -40,6 +41,10 @@ function isValidResourceConfig(value) {
40
41
  if (!value || typeof value !== "object")
41
42
  return false;
42
43
  const config = value;
44
+ // A `command` service (the minimal contract) needs only a name, as appName or name.
45
+ if (declaresCommand(config)) {
46
+ return typeof config.appName === "string" || typeof config.name === "string";
47
+ }
43
48
  // Accept runtime, appType, or type (for backward compatibility with existing service.json files)
44
49
  const hasRuntimeOrAppType = typeof config.runtime === "string" ||
45
50
  typeof config.appType === "string" ||
@@ -81,6 +86,9 @@ function parseResource(serviceJsonPath) {
81
86
  if (!isValidResourceConfig(parsed)) {
82
87
  throw new InvalidServiceJsonError("missing required fields (appName, runtime or appType)");
83
88
  }
89
+ const nativeErrors = validateNativeServiceFields(parsed);
90
+ if (nativeErrors.length > 0)
91
+ throw new InvalidServiceJsonError(nativeErrors.join("; "));
84
92
  const invalidRoutePathFields = getInvalidServiceRoutePathFields(parsed);
85
93
  if (invalidRoutePathFields.length > 0) {
86
94
  throw new InvalidServiceJsonError(invalidRoutePathFields.map((field) => `${field}: expected a string`).join("; "));
@@ -98,8 +106,13 @@ function parseResource(serviceJsonPath) {
98
106
  }
99
107
  const config = {
100
108
  ...parsedConfig,
109
+ appName: parsedConfig.appName ?? String(parsedConfig.name),
110
+ // A `command` service without a type is the user's own program, like a bring-your-own container.
101
111
  appType: (parsedConfig.appType ||
102
- parsedConfig.type),
112
+ parsedConfig.type ||
113
+ (declaresCommand(parsedConfig)
114
+ ? BRING_YOUR_OWN_TYPE
115
+ : undefined)),
103
116
  };
104
117
  const resourceDir = dirname(serviceJsonPath);
105
118
  return {
@@ -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 fix = portConflictFix(message);
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
  }
@@ -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
- const tmpFile = join(tempDir, "bundle.tar.gz");
15
- writeFileSync(tmpFile, data);
16
- mkdirSync(destDir, { recursive: true });
17
- const tarBin = process.platform === "win32" ? "tar.exe" : "tar";
18
- const result = spawnSync(tarBin, ["xzf", tmpFile, "-C", destDir, "--strip-components=1"], {
19
- stdio: "pipe",
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 (fallback.status !== 0) {
30
- rmSync(tempDir, { recursive: true, force: true });
31
- throw new Error(`tar extraction failed: ${fallback.stderr?.toString() || result.stderr?.toString() || "unknown error"}`);
32
- }
33
- const entries = readdirSync(fallbackDir);
34
- const [only] = entries;
35
- const sourceDir = entries.length === 1 &&
36
- only !== undefined &&
37
- existsSync(join(fallbackDir, only)) &&
38
- statSync(join(fallbackDir, only)).isDirectory()
39
- ? join(fallbackDir, only)
40
- : fallbackDir;
41
- for (const entry of readdirSync(sourceDir)) {
42
- renameSync(join(sourceDir, entry), join(destDir, entry));
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
- rmSync(tempDir, { recursive: true, force: true });
48
+ finally {
49
+ rmSync(tempDir, { recursive: true, force: true });
50
+ }
46
51
  }
@@ -26,6 +26,14 @@ export function isValidSince(value) {
26
26
  }
27
27
  return total <= MAX_DURATION_NANOSECONDS;
28
28
  }
29
+ /** A valid --since duration in milliseconds, for logs Flisk reads itself (local processes) rather than through Tilt. */
30
+ export function sinceMilliseconds(value) {
31
+ let total = 0;
32
+ for (const [, amount, unit] of value.matchAll(/(\d+(?:\.\d+)?)(ns|us|µs|ms|s|m|h)/g)) {
33
+ total += Number(amount) * (NANOSECONDS[unit] ?? 0);
34
+ }
35
+ return total / 1e6;
36
+ }
29
37
  export function isValidPort(value) {
30
38
  return /^[1-9]\d{0,4}$/.test(value) && Number(value) <= 65535;
31
39
  }
@@ -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 Error(`Unable to inspect Tilt listeners on port ${port} with PowerShell.`);
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 Error(`Unable to inspect listeners on port ${port}. Install lsof or resolve the lsof error before using --force.`, { cause: commandErr });
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 Error(`Unable to identify the process listening on port ${port}.`, { cause: err });
42
+ throw new FliskError(`Unable to identify the process listening on port ${port}.`, ["Check process inspection permissions"], 1);
42
43
  }
43
44
  });
44
45
  }