@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
@@ -20,14 +20,18 @@ import { exportHostPortPlan, getHostPortPlan, writeSavedHostPortPlan, } from "..
20
20
  import { formatHostPortPlan } from "../utils/host-port-plan.js";
21
21
  import { createJsonEmitter } from "../utils/json-output.js";
22
22
  import { toMachineError } from "../utils/machine-output.js";
23
+ import { hasNearbyServiceJson } from "../utils/native-project.js";
24
+ import { formatMixedRuntimeRefusal, isNativeProject, isNativeService, resolveRuntimeMode, } from "../utils/native-service.js";
25
+ import { runNativeUp } from "../utils/native-up.js";
23
26
  import { nativeWindowsStackRefusal } from "../utils/native-windows.js";
24
27
  import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
25
28
  import { findAvailablePort } from "../utils/port-assignment.js";
26
29
  import { formatPortFallbackNotice } from "../utils/port-fallback-notice.js";
30
+ import { findPortOccupant } from "../utils/port-occupant.js";
27
31
  import { isApiServiceType } from "../utils/resource-kind.js";
28
32
  import { enforceSchemaVersionGate } from "../utils/schema-version-gate.js";
29
33
  import { enforceServiceConfigGate, warnSchemaVersions } from "../utils/service-config-checks.js";
30
- import { appendHealthPath, resolveSubdomainBases } from "../utils/service-urls.js";
34
+ import { appendHealthPath, resolveServiceUrl, resolveSubdomainBases, } from "../utils/service-urls.js";
31
35
  import { discoverResourcesStrict, discoverResourcesWithProblems, discoverStacks, getResourcesForStack, stackExists, warnAboutSkippedFiles, } from "../utils/services.js";
32
36
  import { buildSmokePlans, formatSmokeFailure, runSmokePlans, safeSmokeDeps, } from "../utils/smoke.js";
33
37
  import { buildStartupReport, formatStartupReport, isStartupStalled, } from "../utils/startup-report.js";
@@ -37,17 +41,54 @@ import { stopTiltOnPort } from "../utils/tilt-process.js";
37
41
  import { parseTiltPort, resolveTiltPort, secondUpAction, stopTiltForUp, } from "../utils/tilt-startup.js";
38
42
  import { findUnknownServices, resolveOnlySelection } from "../utils/up-only.js";
39
43
  import { tiltGetUiResources, waitForTiltResourcesReady } from "../utils/up-readiness.js";
40
- import { enableDiscoveredStacks } from "./project.js";
41
- export function formatUpSuccess(port, appUrls = []) {
42
- return [
43
- "Flisk is up.",
44
- `Tilt UI: http://localhost:${port}`,
45
- "App URLs:",
46
- ...(appUrls.length > 0
47
- ? appUrls.slice(0, 5).map((url) => ` ${url}`)
48
- : [" run: flisk networks"]),
49
- "Stop: flisk down",
50
- ];
44
+ import { enableDiscoveredStacks, writeDefaultProjectConfig } from "./project.js";
45
+ export function formatUpSuccess(port, options = {}) {
46
+ const { appUrls = [], serviceHealth = [], showLogsHint = serviceHealth.length > 0 } = options;
47
+ const lines = [];
48
+ lines.push("Flisk is up.");
49
+ lines.push(`Tilt UI: http://localhost:${port}`);
50
+ if (serviceHealth.length > 0) {
51
+ lines.push("Services:");
52
+ for (const svc of serviceHealth.slice(0, 10)) {
53
+ const icon = svc.status === "ready" ? "✓" : svc.status === "failed" ? "✗" : "⟳";
54
+ const urlPart = svc.url ? ` → ${svc.url}` : "";
55
+ lines.push(` ${icon} ${svc.name}${urlPart}`);
56
+ }
57
+ if (serviceHealth.length > 10) {
58
+ lines.push(` … and ${serviceHealth.length - 10} more`);
59
+ }
60
+ // Health check URLs for test infrastructure and monitoring
61
+ lines.push("Health checks:");
62
+ for (const svc of serviceHealth.slice(0, 10)) {
63
+ const icon = svc.status === "ready" ? "✓" : svc.status === "failed" ? "✗" : "⟳";
64
+ if (svc.url) {
65
+ const healthUrl = svc.url.endsWith("/health") ? svc.url : `${svc.url}/health`;
66
+ lines.push(` ${icon} ${svc.name}: ${healthUrl}`);
67
+ }
68
+ }
69
+ if (serviceHealth.length > 10) {
70
+ lines.push(` … and ${serviceHealth.length - 10} more`);
71
+ }
72
+ }
73
+ else if (appUrls.length > 0) {
74
+ lines.push("App URLs:");
75
+ for (const url of appUrls.slice(0, 5)) {
76
+ lines.push(` ${url}`);
77
+ }
78
+ if (appUrls.length > 5) {
79
+ lines.push(` … and ${appUrls.length - 5} more`);
80
+ }
81
+ }
82
+ else {
83
+ lines.push("App URLs:");
84
+ lines.push(" run: flisk networks");
85
+ }
86
+ lines.push("Stop: flisk down");
87
+ if (showLogsHint) {
88
+ lines.push("Logs: flisk logs <service>");
89
+ }
90
+ lines.push("Next: flisk help # or visit https://flisk.sh/docs");
91
+ return lines;
51
92
  }
52
93
  /**
53
94
  * The native Windows boundary for a command that starts a stack.
@@ -197,6 +238,67 @@ function envFileFailure(error) {
197
238
  exitCode: error instanceof FliskError ? error.exitCode : 2,
198
239
  };
199
240
  }
241
+ /**
242
+ * Decides whether `flisk up` runs the selection as local processes (#2229). Returns null for today's Docker + Tilt path, which
243
+ * a project without any `command` service always takes. Outside a project, a directory whose services all declare `command`
244
+ * becomes the project. Refuses (through `fail`) an unknown stack or service, an invalid service.json, and a run that would mix
245
+ * native and container services.
246
+ */
247
+ export function selectNativeRun(stackName, only, fail, cwd = process.cwd()) {
248
+ const foundRoot = findProjectRoot(cwd);
249
+ if (!foundRoot && !hasNearbyServiceJson(cwd))
250
+ return null;
251
+ const projectRoot = foundRoot ?? cwd;
252
+ let discovered;
253
+ try {
254
+ discovered = discoverResourcesWithProblems(projectRoot);
255
+ }
256
+ catch {
257
+ return null;
258
+ }
259
+ const all = discovered.resources;
260
+ if (!all.some(isNativeService))
261
+ return null;
262
+ if (!foundRoot && !isNativeProject(all))
263
+ return null;
264
+ if (discovered.problems.length > 0) {
265
+ fail("INVALID_SERVICE_CONFIG", discovered.problems
266
+ .map((problem) => `Invalid service.json at ${problem.path}: ${problem.reason}`)
267
+ .join("\n"));
268
+ }
269
+ let requested = all;
270
+ if (stackName) {
271
+ if (!stackExists(stackName, all)) {
272
+ const error = errorFactories.stackNotFound(stackName, discoverStacks(all).map((s) => s.name));
273
+ fail("STACK_NOT_FOUND", error.message, error.suggestions);
274
+ }
275
+ requested = getResourcesForStack(stackName, all);
276
+ }
277
+ if (only) {
278
+ const unknown = findUnknownServices(only, requested);
279
+ if (unknown.length > 0) {
280
+ const error = errorFactories.unknownServices(unknown, requested.map((service) => service.name));
281
+ fail("UNKNOWN_SERVICE", error.message, error.suggestions);
282
+ }
283
+ }
284
+ // A process cannot start before what it depends on, so a stack or --only selection always brings its dependsOn closure.
285
+ const names = only ?? requested.map((service) => service.name);
286
+ const selection = resolveOnlySelection(names, all);
287
+ const { mode, mixed } = resolveRuntimeMode(selection.selected, all);
288
+ if (mode === "container") {
289
+ const containerNames = (selection.selected.some(isNativeService) ? selection.selected : all)
290
+ .filter((service) => !isNativeService(service))
291
+ .map((service) => service.name);
292
+ fail("MIXED_RUNTIME", formatMixedRuntimeRefusal(mixed, containerNames));
293
+ }
294
+ return {
295
+ projectRoot,
296
+ isNewProject: !foundRoot,
297
+ all,
298
+ selected: selection.selected,
299
+ dependencies: only ? selection.dependencies : [],
300
+ };
301
+ }
200
302
  export const upCommand = new Command("up")
201
303
  .description("Start all services (optionally filtered by stack)")
202
304
  .alias("deploy")
@@ -228,6 +330,60 @@ export const upCommand = new Command("up")
228
330
  onFail: (message) => emit?.({ ok: false }, [{ code: "FLISK_VERSION_TOO_OLD", message }]),
229
331
  });
230
332
  }
333
+ // Every selected service declares `command`: run them as local processes, with no Docker, Compose, Tilt or generated
334
+ // files (#2229). Decided before any Docker or Tilt check; a project without `command` services never enters this branch.
335
+ const nativeRun = selectNativeRun(stackName, options.only, (code, message, suggestions) => {
336
+ emit?.({ ok: false }, [
337
+ { code, message, ...(suggestions && suggestions.length > 0 ? { suggestions } : {}) },
338
+ ]);
339
+ return showErrorAndExit(message, 1, suggestions);
340
+ });
341
+ if (nativeRun) {
342
+ const platformRefusal = nativeWindowsUpRefusal(process.platform, process.env.FLISK_ALLOW_NATIVE_WINDOWS);
343
+ if (platformRefusal)
344
+ showErrorAndExit(platformRefusal);
345
+ const { projectRoot } = nativeRun;
346
+ let envFilePath;
347
+ if (options.envFile !== undefined) {
348
+ try {
349
+ envFilePath = selectEnvFile(projectRoot, options.envFile).path;
350
+ }
351
+ catch (error) {
352
+ const { failure, exitCode } = envFileFailure(error);
353
+ emit?.({ ok: false }, [failure]);
354
+ showErrorAndExit(failure.message, exitCode, failure.suggestions);
355
+ }
356
+ }
357
+ if (!nativeRun.isNewProject) {
358
+ enforceSchemaVersionGate(projectRoot, {
359
+ resources: nativeRun.all,
360
+ onInvalid: (message) => emit?.({ ok: false }, [{ code: "INVALID_SCHEMA_VERSION", message }]),
361
+ });
362
+ }
363
+ enforceServiceConfigGate(projectRoot, {
364
+ resources: nativeRun.all,
365
+ onInvalid: (message) => emit?.({ ok: false }, [{ code: "INVALID_SERVICE_CONFIG", message }]),
366
+ });
367
+ if (nativeRun.isNewProject && !options.dryRun) {
368
+ writeDefaultProjectConfig(projectRoot);
369
+ if (!options.quiet) {
370
+ console.log(chalk.gray("Created .flisk/project.json (this directory is now a Flisk project)"));
371
+ }
372
+ }
373
+ await runNativeUp({
374
+ projectRoot,
375
+ selected: nativeRun.selected,
376
+ dependencies: nativeRun.dependencies,
377
+ stackName,
378
+ only: options.only,
379
+ envFilePath,
380
+ dryRun: options.dryRun,
381
+ quiet: options.quiet,
382
+ force: options.force,
383
+ emit,
384
+ });
385
+ return;
386
+ }
231
387
  // The env file of the run. `--env-file` replaces the project .env for this run, so the file is
232
388
  // resolved and read before any prerequisite check, any write and any service starts.
233
389
  const envFileRoot = versionRoot;
@@ -366,8 +522,13 @@ export const upCommand = new Command("up")
366
522
  console.log(chalk.gray(`Refreshed runtime assets: ${copiedAssets.join(", ")}`));
367
523
  }
368
524
  const newlyEnabledStacks = enableDiscoveredStacks(projectRoot, discoveredResources);
369
- if (newlyEnabledStacks.length > 0 && !options.quiet) {
370
- console.log(chalk.gray(`Added ${newlyEnabledStacks.join(", ")} to the pre_alpha phase in .flisk/project.json`));
525
+ if (newlyEnabledStacks.length > 0) {
526
+ // project.json just changed, so the generated spec.master is now stale. Regenerate it here, or the next
527
+ // `flisk config verify` reports it out of sync.
528
+ await generateMasterConfigs(projectRoot, { quiet: options.quiet });
529
+ if (!options.quiet) {
530
+ console.log(chalk.gray(`Added ${newlyEnabledStacks.join(", ")} to the pre_alpha phase in .flisk/project.json`));
531
+ }
371
532
  }
372
533
  }
373
534
  let servicesToStart;
@@ -413,7 +574,7 @@ export const upCommand = new Command("up")
413
574
  console.log(chalk.blue("\n🌍 Frontend URLs:"));
414
575
  for (const svc of frontends) {
415
576
  const basePath = svc.config?.basePath ?? `/${svc.name}`;
416
- console.log(chalk.gray(` - ${svc.name}: ${appBase}${chalk.cyan(appendHealthPath(basePath))}`));
577
+ console.log(chalk.gray(` - ${svc.name}: ${appBase}${chalk.cyan(basePath)}`));
417
578
  }
418
579
  }
419
580
  if (backends.length > 0) {
@@ -605,14 +766,35 @@ export const upCommand = new Command("up")
605
766
  blocked: [],
606
767
  starting: [],
607
768
  };
608
- const reportLines = formatStartupReport(report, readiness.timedOut);
769
+ const reportLines = formatStartupReport(report, readiness.timedOut, (port) => findPortOccupant(port));
609
770
  if (reportLines.length > 0) {
610
771
  showError(reportLines.join("\n"));
611
772
  }
612
773
  }
613
774
  }
614
775
  if (uiReady && !options.quiet && !startupFailed) {
615
- for (const line of formatUpSuccess(port))
776
+ const deferred = getDeferredResourceNames(discoveredResources);
777
+ const dependsOn = Object.fromEntries(discoveredResources.map((r) => [r.name, r.config?.dependsOn ?? []]));
778
+ const readiness = await waitForTiltResourcesReady(port, {
779
+ deferred,
780
+ isStalled: (text) => isStartupStalled(text, dependsOn, deferred),
781
+ });
782
+ const serviceHealth = readiness.enabled.map((name) => {
783
+ const failed = readiness.failures.find((f) => f.name === name);
784
+ const svc = discoveredResources.find((r) => r.name === name);
785
+ const isReady = !failed && !readiness.failures.some((f) => f.name === name);
786
+ const isStarting = !isReady && !failed;
787
+ const url = svc ? resolveServiceUrl(svc, hostPortPlan.ingressHttp) : undefined;
788
+ return {
789
+ name,
790
+ status: failed ? "failed" : isStarting ? "starting" : "ready",
791
+ url,
792
+ };
793
+ });
794
+ for (const line of formatUpSuccess(port, {
795
+ serviceHealth,
796
+ showLogsHint: true,
797
+ }))
616
798
  console.log(chalk.blue(line));
617
799
  printedSuccess = true;
618
800
  }
@@ -673,7 +855,28 @@ export const upCommand = new Command("up")
673
855
  process.exit(1);
674
856
  }
675
857
  if (!options.quiet && result.exitCode === 0 && !printedSuccess) {
676
- for (const line of formatUpSuccess(port))
858
+ const deferred = getDeferredResourceNames(discoveredResources);
859
+ const dependsOn = Object.fromEntries(discoveredResources.map((r) => [r.name, r.config?.dependsOn ?? []]));
860
+ const readiness = await waitForTiltResourcesReady(port, {
861
+ deferred,
862
+ isStalled: (text) => isStartupStalled(text, dependsOn, deferred),
863
+ });
864
+ const serviceHealth = readiness.enabled.map((name) => {
865
+ const failed = readiness.failures.find((f) => f.name === name);
866
+ const svc = discoveredResources.find((r) => r.name === name);
867
+ const isReady = !failed && !readiness.failures.some((f) => f.name === name);
868
+ const isStarting = !isReady && !failed;
869
+ const url = svc ? resolveServiceUrl(svc, hostPortPlan.ingressHttp) : undefined;
870
+ return {
871
+ name,
872
+ status: failed ? "failed" : isStarting ? "starting" : "ready",
873
+ url,
874
+ };
875
+ });
876
+ for (const line of formatUpSuccess(port, {
877
+ serviceHealth,
878
+ showLogsHint: true,
879
+ }))
677
880
  console.log(chalk.blue(line));
678
881
  }
679
882
  };
@@ -140,7 +140,7 @@ export function detectAllInstallations() {
140
140
  return [...unique.values()];
141
141
  }
142
142
  const BINARY_RELEASE_REPO = "flisk-sh/releases";
143
- const ENGINE_ASSET_NAME = "tdk-cli-engine.tar.gz";
143
+ const ENGINE_ASSET_NAME = "flisk-cli-engine.tar.gz";
144
144
  const CHECKSUMS_ASSET_NAME = "checksums.txt";
145
145
  /** Parses `shasum -a 256` / `sha256sum` output ("<hash> <name>" or "<hash> *<name>"). */
146
146
  /** @internal Exported for unit tests only; not part of the package API. */
@@ -656,7 +656,7 @@ export function createUpgradeCommand() {
656
656
  showErrorAndExit("Flisk Windows v1 supports AMD64 only. Use WSL2 Ubuntu or a 64-bit Intel/AMD PC.");
657
657
  }
658
658
  if (process.platform === "win32") {
659
- showErrorAndExit("No Windows AMD64 binary in this Flisk release. Need asset tdk-windows-amd64.exe.");
659
+ showErrorAndExit("No Windows AMD64 binary in this Flisk release. Need asset flisk-windows-amd64.exe.");
660
660
  }
661
661
  showErrorAndExit(`Unsupported OS: Flisk does not support this OS: ${process.platform}. Supported: linux, darwin, win32.`);
662
662
  }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Next.js (App Router) is a `server` provider: it owns its build and dev server, so there is no Vite config and no nginx.
3
+ * Under `flisk up` the engine runs `next dev` in a Node image (live reload through Tilt), served under `/<name>/` by Traefik.
4
+ * The engine writes `.autogenerated/.env.frontend.autogenerated`; the generated Dockerfile copies it to `.env`, so
5
+ * `next.config.mjs` and the page read `FLISK_BASE_PATH` and `NEXT_PUBLIC_API_URL` without any wiring in the app.
6
+ */
7
+ const NEXT_CONFIG = `// FLISK_BASE_PATH is set by flisk up (the route this app is served under, for example /web). On the host it is unset,
8
+ // so \`next dev\` serves from the root.
9
+ const basePath = process.env.FLISK_BASE_PATH ?? '';
10
+
11
+ /** @type {import('next').NextConfig} */
12
+ const nextConfig = {
13
+ basePath,
14
+ // Flisk's route is /<name>/; with trailingSlash the page answers there instead of redirecting.
15
+ trailingSlash: true,
16
+ output: 'standalone',
17
+ reactStrictMode: true,
18
+ };
19
+
20
+ export default nextConfig;
21
+ `;
22
+ const STARTER_STYLES = `const page = { maxWidth: 640, margin: '0 auto', padding: '3rem 1.5rem', fontFamily: 'system-ui, sans-serif', color: '#1f2933' };
23
+ const muted = { color: '#616e7c', marginTop: 0 };
24
+ `;
25
+ function renderPage(name, backendName) {
26
+ return `${backendName ? "import HealthCard from './health-card';\n\n" : ""}${STARTER_STYLES}
27
+ export default function Home() {
28
+ return (
29
+ <main style={page}>
30
+ <h1>${name}</h1>
31
+ <p style={muted}>Flisk frontend (Next.js)</p>
32
+ ${backendName ? "\n <HealthCard />\n" : ""}
33
+ <p>Edit <code>src/app/page.tsx</code> and save. It live-reloads.</p>
34
+ <ul>
35
+ <li><a href="http://localhost:10350">Tilt UI</a>: logs and resource status</li>
36
+ <li><code>flisk status</code>: every resource in this project</li>
37
+ </ul>
38
+ </main>
39
+ );
40
+ }
41
+ `;
42
+ }
43
+ function renderHealthCard(backendName) {
44
+ return `'use client';
45
+
46
+ import { useEffect, useState } from 'react';
47
+
48
+ // Set by flisk up from the backend's route; empty on the host, where the browser calls the same origin.
49
+ const API_URL = process.env.NEXT_PUBLIC_API_URL ?? '';
50
+ const BACKEND_NAME = '${backendName}';
51
+
52
+ const card = { border: '1px solid #d9e2ec', borderRadius: 8, padding: '1rem 1.25rem', margin: '1.5rem 0' };
53
+ const cardHeader = { display: 'flex', justifyContent: 'space-between', alignItems: 'center' };
54
+ const pre = { background: '#f5f7fa', borderRadius: 6, padding: '0.75rem', overflowX: 'auto' as const };
55
+
56
+ interface Health {
57
+ ok: boolean;
58
+ body: unknown;
59
+ }
60
+
61
+ export default function HealthCard() {
62
+ const [health, setHealth] = useState<Health | null>(null);
63
+
64
+ useEffect(() => {
65
+ const controller = new AbortController();
66
+ fetch(API_URL + '/health', { signal: controller.signal })
67
+ .then(async (response) => {
68
+ // Read the body as text first: a non-JSON error page must show its real status, not an unreachable backend.
69
+ const text = await response.text();
70
+ let body: unknown = text;
71
+ try {
72
+ body = JSON.parse(text);
73
+ } catch {
74
+ // keep the raw text
75
+ }
76
+ setHealth({ ok: response.ok, body: { status: response.status, body } });
77
+ })
78
+ .catch((error: unknown) => {
79
+ if (!controller.signal.aborted) setHealth({ ok: false, body: { error: String(error) } });
80
+ });
81
+ return () => controller.abort();
82
+ }, []);
83
+
84
+ return (
85
+ <section style={card}>
86
+ <div style={cardHeader}>
87
+ <strong>Backend: {BACKEND_NAME}</strong>
88
+ {health === null && <span>checking…</span>}
89
+ {health?.ok === true && <span style={{ color: '#0a7f3f' }}>ok</span>}
90
+ {health?.ok === false && <span style={{ color: '#b42318' }}>unreachable</span>}
91
+ </div>
92
+ {health && <pre style={pre}>{JSON.stringify(health.body, null, 2)}</pre>}
93
+ </section>
94
+ );
95
+ }
96
+ `;
97
+ }
98
+ export const nextFrontendProvider = {
99
+ id: "next",
100
+ label: "Next.js (App Router)",
101
+ kind: "server",
102
+ dependencies: {
103
+ next: "^15.5.0",
104
+ react: "^19.0.0",
105
+ "react-dom": "^19.0.0",
106
+ },
107
+ devDependencies: {
108
+ "@types/react": "^19.0.0",
109
+ "@types/react-dom": "^19.0.0",
110
+ // `next build` type-checks through the TypeScript JavaScript API.
111
+ typescript: "^5.9.0",
112
+ // The image installs with npm, which enforces peer ranges: vitest 5 peers on @types/node ^22 || >=24, and the
113
+ // scaffold's ^20 otherwise fails `npm install` with ERESOLVE.
114
+ "@types/node": "^22.0.0",
115
+ },
116
+ compilerOptions: {},
117
+ scripts: {
118
+ dev: "next dev",
119
+ build: "next build",
120
+ start: "next start",
121
+ },
122
+ tsconfig: {
123
+ compilerOptions: {
124
+ target: "ES2022",
125
+ lib: ["dom", "dom.iterable", "esnext"],
126
+ allowJs: true,
127
+ skipLibCheck: true,
128
+ strict: true,
129
+ noEmit: true,
130
+ esModuleInterop: true,
131
+ module: "esnext",
132
+ moduleResolution: "bundler",
133
+ resolveJsonModule: true,
134
+ isolatedModules: true,
135
+ jsx: "preserve",
136
+ incremental: true,
137
+ plugins: [{ name: "next" }],
138
+ },
139
+ include: ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx", ".next/types/**/*.ts"],
140
+ exclude: ["node_modules"],
141
+ },
142
+ createFiles(name, options = {}) {
143
+ return [
144
+ {
145
+ filename: "next.config.mjs",
146
+ content: NEXT_CONFIG,
147
+ description: "Generating Next.js config",
148
+ emoji: "⚙️",
149
+ },
150
+ {
151
+ filename: "src/app/layout.tsx",
152
+ content: `import type { ReactNode } from 'react';
153
+
154
+ export const metadata = { title: '${name}' };
155
+
156
+ export default function RootLayout({ children }: { children: ReactNode }) {
157
+ return (
158
+ <html lang="en">
159
+ <body>{children}</body>
160
+ </html>
161
+ );
162
+ }
163
+ `,
164
+ description: "Generating Next.js root layout",
165
+ emoji: "💻",
166
+ },
167
+ {
168
+ filename: "src/app/page.tsx",
169
+ content: renderPage(name, options.backendName),
170
+ description: "Generating Next.js home page",
171
+ emoji: "💻",
172
+ },
173
+ {
174
+ filename: "src/app/health/route.ts",
175
+ content: `// The engine probes /<name>/health; the base path puts this route under the same prefix.
176
+ export const dynamic = 'force-static';
177
+
178
+ export function GET() {
179
+ return new Response('ok');
180
+ }
181
+ `,
182
+ description: "Generating health route",
183
+ emoji: "🩺",
184
+ },
185
+ ...(options.backendName
186
+ ? [
187
+ {
188
+ filename: "src/app/health-card.tsx",
189
+ content: renderHealthCard(options.backendName),
190
+ description: "Generating backend health card",
191
+ emoji: "💻",
192
+ },
193
+ ]
194
+ : []),
195
+ ];
196
+ },
197
+ };
@@ -1,3 +1,80 @@
1
+ const STARTER_STYLES = `const page = { maxWidth: 640, margin: '0 auto', padding: '3rem 1.5rem', fontFamily: 'system-ui, sans-serif', color: '#1f2933' };
2
+ const muted = { color: '#616e7c', marginTop: 0 };
3
+ const card = { border: '1px solid #d9e2ec', borderRadius: 8, padding: '1rem 1.25rem', margin: '1.5rem 0' };
4
+ const cardHeader = { display: 'flex', justifyContent: 'space-between', alignItems: 'center' };
5
+ const pre = { background: '#f5f7fa', borderRadius: 6, padding: '0.75rem', overflowX: 'auto' };
6
+ `;
7
+ /** Hints and links shared by both starters. They need no backend, so they appear in every frontend. */
8
+ const STARTER_HINTS = ` <p>Edit <code>src/App.tsx</code> and save. It live-reloads.</p>
9
+ <ul>
10
+ <li><a href="http://localhost:10350">Tilt UI</a>: logs and resource status</li>
11
+ <li><code>flisk status</code>: every resource in this project</li>
12
+ </ul>`;
13
+ function renderAppWithBackend(name, backendName) {
14
+ return `/// <reference types="vite/client" />
15
+ import { useEffect, useState } from 'react';
16
+
17
+ const API_URL = import.meta.env.VITE_API_URL ?? '';
18
+ const BACKEND_NAME = '${backendName}';
19
+
20
+ ${STARTER_STYLES}
21
+ interface Health {
22
+ ok: boolean;
23
+ body: unknown;
24
+ }
25
+
26
+ function App() {
27
+ const [health, setHealth] = useState<Health | null>(null);
28
+
29
+ useEffect(() => {
30
+ const controller = new AbortController();
31
+ fetch(API_URL + '/health', { signal: controller.signal })
32
+ .then(async (response) => setHealth({ ok: response.ok, body: await response.json() }))
33
+ .catch((error: unknown) => {
34
+ if (!controller.signal.aborted) setHealth({ ok: false, body: { error: String(error) } });
35
+ });
36
+ return () => controller.abort();
37
+ }, []);
38
+
39
+ return (
40
+ <main style={page}>
41
+ <h1>${name}</h1>
42
+ <p style={muted}>Flisk frontend</p>
43
+
44
+ <section style={card}>
45
+ <div style={cardHeader}>
46
+ <strong>Backend: {BACKEND_NAME}</strong>
47
+ {health === null && <span>checking…</span>}
48
+ {health?.ok === true && <span style={{ color: '#0a7f3f' }}>ok</span>}
49
+ {health?.ok === false && <span style={{ color: '#b42318' }}>unreachable</span>}
50
+ </div>
51
+ {health && <pre>{JSON.stringify(health.body, null, 2)}</pre>}
52
+ </section>
53
+
54
+ ${STARTER_HINTS}
55
+ </main>
56
+ );
57
+ }
58
+
59
+ export default App;
60
+ `;
61
+ }
62
+ function renderAppWithoutBackend(name) {
63
+ return `${STARTER_STYLES}
64
+ function App() {
65
+ return (
66
+ <main style={page}>
67
+ <h1>${name}</h1>
68
+ <p style={muted}>Flisk frontend</p>
69
+
70
+ ${STARTER_HINTS}
71
+ </main>
72
+ );
73
+ }
74
+
75
+ export default App;
76
+ `;
77
+ }
1
78
  export const reactFrontendProvider = {
2
79
  id: "react",
3
80
  label: "React + Vite",
@@ -13,7 +90,7 @@ export const reactFrontendProvider = {
13
90
  compilerOptions: {
14
91
  jsx: "react-jsx",
15
92
  },
16
- createFiles(name) {
93
+ createFiles(name, options = {}) {
17
94
  return [
18
95
  {
19
96
  filename: "index.html",
@@ -50,17 +127,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
50
127
  },
51
128
  {
52
129
  filename: "src/App.tsx",
53
- content: `function App() {
54
- return (
55
- <div style={{ padding: '2rem', fontFamily: 'system-ui' }}>
56
- <h1>${name}</h1>
57
- <p>Frontend resource created with Flisk</p>
58
- </div>
59
- );
60
- }
61
-
62
- export default App;
63
- `,
130
+ content: options.backendName
131
+ ? renderAppWithBackend(name, options.backendName)
132
+ : renderAppWithoutBackend(name),
64
133
  description: "Generating React app",
65
134
  emoji: "💻",
66
135
  },
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { FliskError } from "../utils/errors.js";
4
4
  import { litFrontendProvider } from "./lit.js";
5
+ import { nextFrontendProvider } from "./next.js";
5
6
  import { preactFrontendProvider } from "./preact.js";
6
7
  import { qwikFrontendProvider } from "./qwik.js";
7
8
  import { reactFrontendProvider } from "./react.js";
@@ -21,11 +22,11 @@ export const FRONTEND_FRAMEWORKS = {
21
22
  qwik: qwikFrontendProvider,
22
23
  vanilla: vanillaFrontendProvider,
23
24
  "tanstack-router": tanstackRouterFrontendProvider,
25
+ next: nextFrontendProvider,
24
26
  };
25
- // Frameworks that own their own server or build config. They are never scaffolded as Vite SPA providers; they go through
26
- // `flisk resource <name> --type bring-your-own` instead.
27
+ // Meta-frameworks that are not registered providers. They are never scaffolded as Vite SPA providers; they go through
28
+ // `flisk resource <name> --type bring-your-own` instead. Next.js is a registered `server` provider, so it is not listed here.
27
29
  export const META_FRAMEWORK_IDS = [
28
- "next",
29
30
  "nuxt",
30
31
  "sveltekit",
31
32
  "astro",
@@ -49,7 +50,7 @@ export function listFrontendFrameworks() {
49
50
  return Object.entries(FRONTEND_FRAMEWORKS).map(([id, provider]) => ({
50
51
  id,
51
52
  label: provider.label,
52
- kind: "vite-spa",
53
+ kind: provider.kind ?? "vite-spa",
53
54
  verified: VERIFIED_FRONTEND_FRAMEWORKS.includes(id),
54
55
  command: `flisk resource <name> --type frontend --framework ${id}`,
55
56
  }));