@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
@@ -15,6 +15,7 @@ import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.j
15
15
  import { checkCrashLoopingContainers } from "../utils/doctor-crashloop.js";
16
16
  import { applyDoctorFixes, checkDuplicateFliskInstalls, hasFailedActions, planDoctorFixes, projectTiltPids, } from "../utils/doctor-fix.js";
17
17
  import { checkDiskSpace, dockerDesktopStuck } from "../utils/doctor-host.js";
18
+ import { checkNativeCommands, checkNativeDependsOn, checkNativeRuntime, checkNativeServiceHealth, } from "../utils/doctor-native.js";
18
19
  import { collectDoctorChecks, createDoctorReport, getDoctorExitCode, } from "../utils/doctor-report.js";
19
20
  import { checkHealthRoutes, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeAdvertisedProbes, summarizeServiceProbes, summarizeSmokeResults, } from "../utils/doctor-runtime.js";
20
21
  import { checkStarlarkLoadExports } from "../utils/doctor-starlark.js";
@@ -25,6 +26,7 @@ import { evaluateFliskVersionFloor } from "../utils/flisk-version.js";
25
26
  import { formatCount } from "../utils/formatting.js";
26
27
  import { getHostPortPlan, resolveRuntimeIngressPort } from "../utils/host-port-config.js";
27
28
  import { createHostPortPlan } from "../utils/host-port-plan.js";
29
+ import { findNativeProject } from "../utils/native-project.js";
28
30
  import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
29
31
  import { promptConfirm } from "../utils/prompt.js";
30
32
  import { EnvUnreadableError, envSecretValues, redactSecrets, redactValue, } from "../utils/secret-redaction.js";
@@ -291,7 +293,8 @@ export async function checkColima() {
291
293
  name: "Colima",
292
294
  didPass: false,
293
295
  message: status.problem ?? "The Colima VM is not running",
294
- fix: status.fix,
296
+ fix: status.fix ??
297
+ "Start Colima: colima start\n Verify: colima status\n Docs: https://github.com/abiosoft/colima",
295
298
  };
296
299
  }
297
300
  export async function checkDockerRuntime() {
@@ -309,7 +312,7 @@ export async function checkDockerRuntime() {
309
312
  name: CONTAINER_RUNTIME_CHECK,
310
313
  didPass: false,
311
314
  message: "Docker is not running",
312
- fix: DOCTOR_FIXES.dockerDaemonDown,
315
+ fix: "Start Docker Desktop, enable WSL2 integration, then retry: flisk doctor\n Verify: docker ps\n Docs: https://docs.docker.com/desktop/wsl/",
313
316
  };
314
317
  }
315
318
  }
@@ -327,8 +330,8 @@ export async function checkDockerRuntime() {
327
330
  return {
328
331
  name: CONTAINER_RUNTIME_CHECK,
329
332
  didPass: false,
330
- message: "Docker daemon is not responding",
331
- fix: DOCTOR_FIXES.dockerDaemonDown,
333
+ message: "Docker daemon is not responding (timeout)",
334
+ fix: "Restart Docker daemon: systemctl restart docker (Linux) or restart Docker Desktop/Colima\n Verify: docker ps\n Docs: https://docs.docker.com/config/daemon/",
332
335
  };
333
336
  }
334
337
  }
@@ -349,7 +352,7 @@ export async function checkDockerRuntime() {
349
352
  name: CONTAINER_RUNTIME_CHECK,
350
353
  didPass: false,
351
354
  message: "Colima is installed but not running",
352
- fix: DOCTOR_FIXES.dockerDaemonDown,
355
+ fix: "Start Colima: colima start\n Verify: colima status\n Docs: https://github.com/abiosoft/colima",
353
356
  };
354
357
  }
355
358
  if (await succeeds("podman ps")) {
@@ -364,14 +367,14 @@ export async function checkDockerRuntime() {
364
367
  name: CONTAINER_RUNTIME_CHECK,
365
368
  didPass: false,
366
369
  message: "Docker daemon is not running",
367
- fix: DOCTOR_FIXES.dockerDaemonDown,
370
+ fix: "Start Docker daemon: systemctl start docker (Linux) or start Docker Desktop\n Verify: docker ps\n Docs: https://docs.docker.com/engine/daemon/",
368
371
  };
369
372
  }
370
373
  return {
371
374
  name: CONTAINER_RUNTIME_CHECK,
372
375
  didPass: false,
373
- message: "Docker is not running",
374
- fix: DOCTOR_FIXES.dockerMissing,
376
+ message: "Docker is not installed",
377
+ fix: "Install Docker: https://docs.docker.com/get-docker/\n Verify: docker --version\n After install, start Docker and retry: flisk doctor",
375
378
  };
376
379
  }
377
380
  export const checkDockerCompose = createExecCheck("Docker Compose", "docker compose version", "Docker Compose plugin available", "Docker Compose plugin not found", "Install Docker Compose: https://docs.docker.com/compose/install/");
@@ -442,7 +445,7 @@ export async function checkDockerVersions(exec = execAsync) {
442
445
  name: "Docker Versions",
443
446
  didPass: false,
444
447
  message: `Docker is too old for generated healthchecks (they use start_interval):\n ${problems.join("\n ")}`,
445
- fix: "Update Docker Desktop, or on Linux update docker-ce and the docker-compose-plugin package",
448
+ fix: "Update Docker: Docker Desktop (auto-updates), or Linux: apt/yum/dnf update docker-ce docker-compose-plugin\n Verify: docker version --format '{{.Server.Version}}' && docker compose version --short\n Docs: https://docs.docker.com/engine/install/",
446
449
  };
447
450
  }
448
451
  checkDockerVersions.requiresContainerRuntime = true;
@@ -457,7 +460,7 @@ export async function checkTilt(exec = execAsync) {
457
460
  name: "Tilt CLI",
458
461
  didPass: false,
459
462
  message: "Tilt CLI not found",
460
- fix: DOCTOR_FIXES.tiltMissing,
463
+ fix: "Install Tilt: curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash\n Verify: tilt version\n Docs: https://docs.tilt.dev/install.html",
461
464
  };
462
465
  }
463
466
  if (!versionMeetsMinimum(raw, MIN_TILT_VERSION)) {
@@ -465,7 +468,7 @@ export async function checkTilt(exec = execAsync) {
465
468
  name: "Tilt CLI",
466
469
  didPass: false,
467
470
  message: `Tilt ${raw || "version unknown"} is below the required v${MIN_TILT_VERSION.join(".")} floor`,
468
- fix: `Install Tilt v${MIN_TILT_VERSION.join(".")} or newer: https://docs.tilt.dev/install.html`,
471
+ fix: `Upgrade Tilt to v${MIN_TILT_VERSION.join(".")} or newer: https://docs.tilt.dev/install.html\n Verify: tilt version\n If using Homebrew: brew upgrade tilt`,
469
472
  };
470
473
  }
471
474
  return { name: "Tilt CLI", didPass: true, message: `Tilt ${raw} is available` };
@@ -490,7 +493,7 @@ export async function checkDockerOperatingSystem(exec = execAsync) {
490
493
  name: "Docker OS",
491
494
  didPass: false,
492
495
  message: `Docker is using the ${osType || "unknown"} engine; Flisk requires Linux containers`,
493
- fix: "Switch Docker Desktop to Linux containers, enable WSL2 integration, then retry flisk doctor.",
496
+ fix: "Switch Docker Desktop to Linux containers: Settings > General > Use the WSL 2 based engine\n Or on Windows: docker context use default (if using WSL2 backend)\n Verify: docker info --format '{{.OSType}}'\n Docs: https://docs.docker.com/desktop/wsl/",
494
497
  };
495
498
  }
496
499
  return { name: "Docker OS", didPass: true, message: "Docker is using Linux containers" };
@@ -522,7 +525,7 @@ export async function checkBun(exec = execAsync) {
522
525
  name: "Bun",
523
526
  didPass: false,
524
527
  message: `Bun ${rawVersion || "version unknown"} is below the required ${MIN_BUN_VERSION.join(".")} floor`,
525
- fix: `Install Bun ${MIN_BUN_VERSION.join(".")} or newer: https://bun.sh/install`,
528
+ fix: `Upgrade Bun to ${MIN_BUN_VERSION.join(".")} or newer: https://bun.sh/install\n Verify: bun --version\n If using npm: npm install -g bun@latest`,
526
529
  };
527
530
  }
528
531
  return {
@@ -535,8 +538,8 @@ export async function checkBun(exec = execAsync) {
535
538
  return {
536
539
  name: "Bun",
537
540
  didPass: false,
538
- message: "Bun is not available",
539
- fix: DOCTOR_FIXES.bunMissing,
541
+ message: "Bun is not installed",
542
+ fix: "Install Bun: curl -fsSL https://bun.sh/install | bash\n Verify: bun --version\n Docs: https://bun.sh/docs/installation",
540
543
  };
541
544
  }
542
545
  }
@@ -1284,8 +1287,13 @@ export const doctorCommand = new Command("doctor")
1284
1287
  process.exit(2);
1285
1288
  return;
1286
1289
  }
1290
+ // A project whose services all declare `command` runs as local processes: doctor checks only what they need (#2231).
1291
+ const nativeProject = findNativeProject();
1292
+ const nativeResources = nativeProject?.resources ?? null;
1287
1293
  if (!options.json) {
1288
- console.log("Flisk doctor checks your local Docker + Tilt development environment; it does not check cluster deployments.");
1294
+ console.log(nativeResources
1295
+ ? "Flisk doctor checks what this project's local processes need; its services declare commands, so Docker and Tilt are not checked."
1296
+ : "Flisk doctor checks your local Docker + Tilt development environment; it does not check cluster deployments.");
1289
1297
  }
1290
1298
  const host = detectHost();
1291
1299
  if (process.platform === "win32") {
@@ -1342,10 +1350,15 @@ export const doctorCommand = new Command("doctor")
1342
1350
  console.log(`\n${chalk.bold("🔍 Flisk Doctor")}\n`);
1343
1351
  console.log("Checking environment...\n");
1344
1352
  }
1345
- const projectRoot = findProjectRoot();
1353
+ // A directory of command services is a project for doctor even before `flisk up` writes its .flisk/project.json.
1354
+ const projectRoot = findProjectRoot() ?? nativeProject?.projectRoot ?? null;
1346
1355
  // Repairs run before the checks so the checks report the state after the fix.
1347
1356
  let fixActions;
1348
- if (options.fix) {
1357
+ if (options.fix && nativeResources) {
1358
+ // The repairs are for Docker networks and Tilt probes, which a project of local processes does not have.
1359
+ fixActions = [];
1360
+ }
1361
+ else if (options.fix) {
1349
1362
  try {
1350
1363
  let projectName;
1351
1364
  if (projectRoot) {
@@ -1387,16 +1400,19 @@ export const doctorCommand = new Command("doctor")
1387
1400
  if (!options.json)
1388
1401
  printFixActions(fixActions, options.dryRun);
1389
1402
  }
1403
+ // The host port plan places the gateway and Postgres containers; a project of local processes has neither.
1390
1404
  let hostPortPlan;
1391
1405
  let portPlanError;
1392
- try {
1393
- hostPortPlan = await getHostPortPlan(projectRoot ?? process.cwd());
1394
- }
1395
- catch (error) {
1396
- hostPortPlan = null;
1397
- portPlanError = error instanceof Error ? error.message : String(error);
1406
+ if (!nativeResources) {
1407
+ try {
1408
+ hostPortPlan = await getHostPortPlan(projectRoot ?? process.cwd());
1409
+ }
1410
+ catch (error) {
1411
+ hostPortPlan = null;
1412
+ portPlanError = error instanceof Error ? error.message : String(error);
1413
+ }
1398
1414
  }
1399
- const machineChecks = [
1415
+ const containerMachineChecks = [
1400
1416
  () => hostPortPlan
1401
1417
  ? {
1402
1418
  name: "Host Ports",
@@ -1433,7 +1449,7 @@ export const doctorCommand = new Command("doctor")
1433
1449
  requiresContainerRuntime("Docker Network Pool", () => checkDockerNetworkCapacity()),
1434
1450
  () => checkDuplicateFliskInstalls(),
1435
1451
  ];
1436
- const projectChecks = [
1452
+ const containerProjectChecks = [
1437
1453
  () => checkFliskVersion(),
1438
1454
  checkMasterConfigs,
1439
1455
  checkGeneratedProjectRuntimeAssets,
@@ -1494,8 +1510,31 @@ export const doctorCommand = new Command("doctor")
1494
1510
  // When Tilt is up, surface red resources (Traefik/apps never started).
1495
1511
  requiresContainerRuntime("Tilt Resources", checkTiltResourceHealth),
1496
1512
  ];
1513
+ const machineChecks = nativeResources
1514
+ ? [
1515
+ () => checkNativeRuntime(nativeResources),
1516
+ () => checkNativeCommands(nativeResources),
1517
+ () => checkWslProjectLocation(findProjectRoot() ?? process.cwd(), options.strict),
1518
+ () => checkDuplicateFliskInstalls(),
1519
+ ]
1520
+ : containerMachineChecks;
1521
+ const projectChecks = nativeResources
1522
+ ? [
1523
+ () => checkFliskVersion(),
1524
+ () => checkDuplicateResourceNames(),
1525
+ () => checkDependsOnShape(),
1526
+ () => checkNativeDependsOn(nativeResources),
1527
+ () => checkCircularDependencies(),
1528
+ () => checkServicePorts(),
1529
+ () => checkSchemaVersions(undefined, options.strict),
1530
+ () => checkDuplicateResourcePorts(),
1531
+ ]
1532
+ : containerProjectChecks;
1533
+ if (options.ping && nativeResources && projectRoot) {
1534
+ projectChecks.push(() => checkNativeServiceHealth(projectRoot, nativeResources, pingTimeout));
1535
+ }
1497
1536
  // Runs last: needs routable services (and working Traefik) to mean anything.
1498
- if (options.ping) {
1537
+ if (options.ping && !nativeResources) {
1499
1538
  // Probe the port a running stack is actually on, not the one the next `flisk up` would pick.
1500
1539
  const pingPort = projectRoot
1501
1540
  ? resolveRuntimeIngressPort(projectRoot, hostPortPlan?.ingressHttp)
@@ -8,8 +8,10 @@ import { pruneProjectNetworks, stopProjectTiltUp, waitForTiltUpExit, } from "../
8
8
  import { errorFactories, handleTiltFailure, withTiltCheck } from "../utils/errors.js";
9
9
  import { createJsonEmitter } from "../utils/json-output.js";
10
10
  import { writeMachineError } from "../utils/machine-output.js";
11
+ import { readNativeRunState, stopNativeRun } from "../utils/native-runtime.js";
12
+ import { isNativeProject } from "../utils/native-service.js";
11
13
  import { findProjectRoot } from "../utils/paths.js";
12
- import { discoverResources, discoverStacks } from "../utils/services.js";
14
+ import { discoverResources, discoverResourcesFromRoot, discoverStacks } from "../utils/services.js";
13
15
  import { buildTiltDownArgs, getTiltfilePath, runTilt } from "../utils/tilt.js";
14
16
  function allStackNames() {
15
17
  try {
@@ -20,6 +22,17 @@ function allStackNames() {
20
22
  return undefined;
21
23
  }
22
24
  }
25
+ /** True when `flisk down` stops local processes instead of Tilt: a native run is on record, or every service declares `command`. */
26
+ function isNativeRun(projectRoot) {
27
+ if (readNativeRunState(projectRoot))
28
+ return true;
29
+ try {
30
+ return isNativeProject(discoverResourcesFromRoot(projectRoot));
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
23
36
  function pruneNetworksOrWarn(json) {
24
37
  const root = findProjectRoot();
25
38
  if (!root)
@@ -64,6 +77,25 @@ export const downCommand = addYesOption(new Command("down"))
64
77
  }
65
78
  }
66
79
  const emit = options.json ? createJsonEmitter("DOWN_FAILED", "flisk down") : undefined;
80
+ // A project of `command` services (or one with a native run on record) stops without Docker or Tilt.
81
+ const projectRoot = findProjectRoot();
82
+ if (isNativeRun(projectRoot)) {
83
+ if (emit && options.dryRun) {
84
+ emit({ ok: true, dryRun: true, runtime: "native" });
85
+ return;
86
+ }
87
+ if (handleDryRun(options, "not stopping local processes", "stop the local processes of this project")) {
88
+ return;
89
+ }
90
+ const stopped = await stopNativeRun(projectRoot);
91
+ if (!options.json) {
92
+ console.log(stopped.wasRunning
93
+ ? chalk.blue(`Stopped local processes: ${stopped.services.join(", ")}`)
94
+ : chalk.gray("Nothing is running for this project."));
95
+ }
96
+ emit?.({ ok: true, runtime: "native", stopped: stopped.services });
97
+ return;
98
+ }
67
99
  await withTiltCheck(async () => {
68
100
  if (emit && options.dryRun) {
69
101
  emit({ ok: true, dryRun: true });
@@ -4,10 +4,13 @@ import { Command } from "commander";
4
4
  import { STANDARD_PORTS } from "../utils/constants.js";
5
5
  import { errorFactories, FliskError, runCommand, showErrorAndExit } from "../utils/errors.js";
6
6
  import { createMachineEnvelope } from "../utils/machine-output.js";
7
+ import { readNativeLogLines } from "../utils/native-runtime.js";
8
+ import { isNativeProject } from "../utils/native-service.js";
7
9
  import { findProjectRoot } from "../utils/paths.js";
8
10
  import { EnvUnreadableError, envSecretValues, redactSecrets, redactValue, } from "../utils/secret-redaction.js";
11
+ import { discoverResourcesFromRoot } from "../utils/services.js";
9
12
  import { isTiltAvailable, runTilt } from "../utils/tilt.js";
10
- import { DEFAULT_LOG_TAIL, isTiltConnectionFailure, isValidPort, isValidSince, isValidTail, MAX_LOG_TAIL, parseTiltLogLines, } from "../utils/tilt-logs.js";
13
+ import { DEFAULT_LOG_TAIL, isTiltConnectionFailure, isValidPort, isValidSince, isValidTail, MAX_LOG_TAIL, parseTiltLogLines, sinceMilliseconds, } from "../utils/tilt-logs.js";
11
14
  import { tiltUnreachableMessage } from "../utils/tilt-unreachable-message.js";
12
15
  /** Resource names Tilt knows about, or null when they cannot be listed (the logs call then reports the real error). */
13
16
  async function tiltResourceNames(port) {
@@ -25,8 +28,20 @@ async function tiltResourceNames(port) {
25
28
  return null;
26
29
  }
27
30
  }
31
+ /** The project's services when every one declares `command` (they run as local processes), else null. */
32
+ function nativeResources(projectRoot) {
33
+ try {
34
+ const resources = discoverResourcesFromRoot(projectRoot);
35
+ return isNativeProject(resources) ? resources : null;
36
+ }
37
+ catch {
38
+ // Discovery problems are reported by the commands that start the stack; logs still asks Tilt.
39
+ return null;
40
+ }
41
+ }
28
42
  export const logsCommand = new Command("logs")
29
43
  .description("Print a bounded snapshot of recent logs from the running stack")
44
+ .showHelpAfterError("\nService names are not positional. Use: flisk logs --service <name>")
30
45
  .option("-s, --service <names...>", "Only these services (Tilt resource names)")
31
46
  .option("--tail <n>", "Number of most recent lines to return", String(DEFAULT_LOG_TAIL))
32
47
  .option("--since <duration>", "Only logs newer than this duration, for example 30s, 5m, 1h")
@@ -80,6 +95,29 @@ export const logsCommand = new Command("logs")
80
95
  "Fix the read permission on .env, or move it out of the project",
81
96
  ]);
82
97
  }
98
+ // A project of `command` services keeps its output in .flisk/native/logs, so no Tilt is involved.
99
+ const resources = nativeResources(projectRoot);
100
+ if (resources) {
101
+ const known = resources.map((resource) => resource.name);
102
+ const unknown = services.filter((name) => !known.includes(name));
103
+ if (unknown.length > 0) {
104
+ const error = errorFactories.unknownServices(unknown, known);
105
+ fail("UNKNOWN_SERVICE", error.message, error.exitCode, error.suggestions);
106
+ }
107
+ const lines = readNativeLogLines(projectRoot, services.length > 0 ? services : known, {
108
+ tail,
109
+ sinceMs: options.since === undefined ? undefined : sinceMilliseconds(options.since),
110
+ });
111
+ if (options.json) {
112
+ console.log(JSON.stringify(redactValue(createMachineEnvelope({ services, tail, since: options.since ?? null, lines }), secrets)));
113
+ return;
114
+ }
115
+ const width = Math.max(0, ...lines.map((line) => line.resource.length));
116
+ for (const line of lines) {
117
+ console.log(redactSecrets(`${line.resource.padEnd(width)} | ${line.text}`, secrets));
118
+ }
119
+ return;
120
+ }
83
121
  if (!(await isTiltAvailable())) {
84
122
  fail("TILT_MISSING", "Tilt is not installed. Run: flisk doctor");
85
123
  }
@@ -213,7 +213,19 @@ const DEFAULT_PROJECT_JSON = {
213
213
  },
214
214
  overrides: {},
215
215
  };
216
+ /**
217
+ * Writes .flisk/project.json with the defaults `flisk project --yes` uses, and nothing else: no generated Tiltfile, Compose file,
218
+ * .env or engine copy. `flisk up` uses it to adopt a directory of `command` services as a project, so `flisk down` and
219
+ * `flisk doctor` find it afterwards.
220
+ */
221
+ export function writeDefaultProjectConfig(projectRoot) {
222
+ const projectConfig = JSON.parse(JSON.stringify(DEFAULT_PROJECT_JSON));
223
+ projectConfig.project.name = basename(projectRoot) || "my-project";
224
+ ensureDirectory(join(projectRoot, ".flisk"));
225
+ writeJsonFile(join(projectRoot, ".flisk", PROJECT_JSON), projectConfig);
226
+ }
216
227
  export const projectCommand = addYesOption(new Command("project"))
228
+ .alias("init")
217
229
  .description("Initialize or validate project-level master configuration")
218
230
  .argument("[template]", `Clone a starter example instead of a blank project (${Object.keys(PROJECT_TEMPLATES).join(", ")})`)
219
231
  .option("--path <dir>", "Directory to clone the template into (default: the template's repo name)")
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) 2026 Flisk Landscape contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { existsSync, mkdirSync } from "node:fs";
4
- import { isAbsolute, relative, resolve } from "node:path";
4
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
5
5
  import chalk from "chalk";
6
6
  import { Command } from "commander";
7
7
  import { DEFAULT_AGENT_RUNTIME, resolveAgentDurability, resolveAgentRuntime, } from "../agent-runtimes/registry.js";
@@ -90,7 +90,7 @@ export function resolveResourcePort(resourceType, value, assignedPort, resources
90
90
  }
91
91
  return port;
92
92
  }
93
- export function createServiceJson(name, type, stack, port, extraFeatures = [], frameworkId, languageId, agent) {
93
+ export function createServiceJson(name, type, stack, port, extraFeatures = [], frameworkId, languageId, agent, basePath) {
94
94
  const framework = resolveFrontendFramework(type, frameworkId);
95
95
  // Only an explicit selection is persisted: a missing `language` has always meant Bun.
96
96
  const language = languageId === undefined ? undefined : resolveBackendLanguage(type, languageId);
@@ -129,11 +129,15 @@ export function createServiceJson(name, type, stack, port, extraFeatures = [], f
129
129
  stack,
130
130
  port,
131
131
  ...(framework ? { framework: framework.id } : {}),
132
+ // A server framework (Next.js) is served under /<name>/ with a base path, so the engine's default /health probe finds
133
+ // nothing there. Probe the framework's own health route under the same prefix.
134
+ ...(framework?.kind === "server" ? { healthCheckPath: `/${name}/health` } : {}),
132
135
  ...(backendFramework && type !== "mcp" && type !== "agent"
133
136
  ? { framework: backendFramework.id }
134
137
  : {}),
135
138
  ...(language ? { language: language.id } : {}),
136
139
  ...(type === "agent" && agent ? { agent } : {}),
140
+ ...(type === "frontend" && basePath ? { basePath } : {}),
137
141
  };
138
142
  }
139
143
  export function createByoServiceJson(name, stack, port, options) {
@@ -173,20 +177,26 @@ export function createPackageJson(name, type, frameworkId, prismaEnabled = false
173
177
  version: "0.0.1",
174
178
  type: "module",
175
179
  scripts: {
176
- ...(isFrontend
180
+ ...(isFrontend && !framework?.scripts
177
181
  ? {
178
182
  predev: requireGeneratedViteConfig(`${AUTOGENERATED_DIR}/vite.config.frontend.autogenerated.ts`),
179
183
  prebuild: requireGeneratedViteConfig(`${AUTOGENERATED_DIR}/vite.config.build.autogenerated.ts`),
180
184
  }
181
185
  : {}),
182
186
  dev: isFrontend
183
- ? `vite --config ${AUTOGENERATED_DIR}/vite.config.frontend.autogenerated.ts`
187
+ ? (framework?.scripts?.dev ??
188
+ `vite --config ${AUTOGENERATED_DIR}/vite.config.frontend.autogenerated.ts`)
184
189
  : "bun run --watch src/index.ts",
185
190
  build: isFrontend
186
- ? `tsc && vite build --config ${AUTOGENERATED_DIR}/vite.config.build.autogenerated.ts`
191
+ ? (framework?.scripts?.build ??
192
+ `tsc && vite build --config ${AUTOGENERATED_DIR}/vite.config.build.autogenerated.ts`)
187
193
  : "tsc",
188
- // Backends and workers run `start` in their production image; doctor checks for it.
189
- ...(isFrontend ? {} : { start: "bun run dist/index.js" }),
194
+ // Backends and workers run `start` in their production image; doctor checks for it. A server framework brings its own.
195
+ ...(isFrontend
196
+ ? framework?.scripts?.start
197
+ ? { start: framework.scripts.start }
198
+ : {}
199
+ : { start: "bun run dist/index.js" }),
190
200
  test: "vitest",
191
201
  lint: "biome check .",
192
202
  "lint:fix": "biome check . --write",
@@ -220,7 +230,7 @@ export function createPackageJson(name, type, frameworkId, prismaEnabled = false
220
230
  "@biomejs/biome": "^2.5.13",
221
231
  // vitest 5 peers on vite ^6.4.0 || ^7 || ^8; vite 5 makes `vitest run` fail at startup with
222
232
  // ERR_PACKAGE_PATH_NOT_EXPORTED ('./module-runner'), in every resource once it is hoisted.
223
- ...(isFrontend ? { vite: "^6.4.1" } : {}),
233
+ ...(isFrontend && !framework?.scripts ? { vite: "^6.4.1" } : {}),
224
234
  ...(drizzleEnabled ? { "drizzle-kit": DRIZZLE_KIT_RANGE, "@types/pg": "^8.11.0" } : {}),
225
235
  ...(framework?.devDependencies ?? {}),
226
236
  ...(backendFramework?.devDependencies ?? {}),
@@ -269,6 +279,9 @@ export default defineConfig({
269
279
  `;
270
280
  export function createResourceTsconfig(resourceType, frameworkId) {
271
281
  const framework = resolveFrontendFramework(resourceType, frameworkId);
282
+ // A server framework (Next.js) owns its compiler settings: the SPA template's `rootDir`, `outDir` and declaration output do not apply.
283
+ if (framework?.tsconfig)
284
+ return framework.tsconfig;
272
285
  const backendFramework = resolveBackendFramework(resourceType, frameworkId);
273
286
  return {
274
287
  ...TSCONFIG_TEMPLATE,
@@ -380,7 +393,7 @@ export const resourceCommand = addYesOption(new Command("resource"))
380
393
  .description("Create a new resource (service) from scratch, or register an existing one")
381
394
  .argument("[name]", "Resource name (kebab-case)")
382
395
  .option("-t, --type <type>", "Resource type: backend, frontend, worker, mcp, agent, bring-your-own, sdk", "backend")
383
- .option("--framework <id>", "Framework: frontend react (default), vue, svelte, preact, lit, solid, qwik, vanilla, tanstack-router; backend hono (default), express, elysia, fastify, nestjs, koa, h3 (list them with --frameworks)")
396
+ .option("--framework <id>", "Framework: frontend react (default), vue, svelte, preact, lit, solid, qwik, vanilla, tanstack-router, next; backend hono (default), express, elysia, fastify, nestjs, koa, h3 (list them with --frameworks)")
384
397
  .option("--frameworks", "List registered frameworks for the resource type and exit (--type frontend, --type backend)")
385
398
  .option("--language <id>", "Backend language: bun (default), python, go, rust, node")
386
399
  .option("-s, --stack <stack>", "Stack to assign resource to", "default")
@@ -394,6 +407,7 @@ export const resourceCommand = addYesOption(new Command("resource"))
394
407
  .option("--restart <policy>", "Compose restart policy: no, on-failure, unless-stopped, always (bring-your-own only; use no for one-shot jobs)")
395
408
  .option("--image <name>", "Docker image name instead of building from Dockerfile (for bring-your-own)")
396
409
  .option("--port <port>", "Port for the resource, within its type's range (default: next free port in that range)")
410
+ .option("--base-path <path>", "Base path for frontend routing (e.g., /storefront). Default is /{stack}s. Frontend only.")
397
411
  .option("--feature <feature...>", "Enable a resource feature (for example: prisma, drizzle)")
398
412
  .option("--runtime <id>", "Agent runtime (for agent only): custom (default)")
399
413
  .option("--durability <kind>", "Where agent sessions are checkpointed (for agent only): files (default, named volume) or postgres")
@@ -757,6 +771,10 @@ This file contains the resource configuration for Flisk.
757
771
  }
758
772
  if (resourceType === "frontend") {
759
773
  mkdirSync(resolve(fullPath, "public"), { recursive: true });
774
+ // A framework can keep its files in folders of its own (Next.js: src/app).
775
+ for (const file of frontendFramework?.createFiles(resourceName) ?? []) {
776
+ mkdirSync(dirname(resolve(fullPath, file.filename)), { recursive: true });
777
+ }
760
778
  }
761
779
  if (dddEnabled) {
762
780
  for (const layer of ["domain", "application", "infrastructure", "presentation"]) {
@@ -766,11 +784,19 @@ This file contains the resource configuration for Flisk.
766
784
  // Prepare file generation tasks
767
785
  const prismaEnabled = requestedFeatures.includes("prisma");
768
786
  const drizzleEnabled = requestedFeatures.includes("drizzle");
769
- const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, [...(dddEnabled ? ["ddd"] : []), ...requestedFeatures], scaffoldFrameworkId, options.language === undefined ? undefined : backendLanguage?.id, agentConfig);
787
+ const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, [...(dddEnabled ? ["ddd"] : []), ...requestedFeatures], scaffoldFrameworkId, options.language === undefined ? undefined : backendLanguage?.id, agentConfig, resourceType === "frontend" ? options.basePath : undefined);
770
788
  const packageJson = createPackageJson(resourceName, resourceType, scaffoldFrameworkId, prismaEnabled, agentConfig, drizzleEnabled);
771
789
  if (prismaEnabled && resourceType === "backend") {
772
790
  serviceJson.dependsOn = [`${resourceName}-migrator`];
773
791
  }
792
+ // A frontend is wired to the first backend already in its stack, so its starter can call that API out of the box.
793
+ const frontendBackendName = resourceType === "frontend"
794
+ ? allResources.find((resource) => resource.stack === stackName && resource.type === "backend")?.name
795
+ : undefined;
796
+ if (frontendBackendName) {
797
+ serviceJson.backendName = frontendBackendName;
798
+ serviceJson.dependsOn = [frontendBackendName];
799
+ }
774
800
  const tasks = [
775
801
  {
776
802
  type: "json",
@@ -829,7 +855,9 @@ This file contains the resource configuration for Flisk.
829
855
  });
830
856
  }
831
857
  else if (resourceType === "frontend") {
832
- tasks.push(...(frontendFramework?.createFiles(resourceName).map((file) => ({
858
+ tasks.push(...(frontendFramework
859
+ ?.createFiles(resourceName, { backendName: frontendBackendName })
860
+ .map((file) => ({
833
861
  ...file,
834
862
  type: "text",
835
863
  })) ?? []));
@@ -8,6 +8,8 @@ import { runCommand } from "../utils/errors.js";
8
8
  import { formatCount, showDetail, showStep } from "../utils/formatting.js";
9
9
  import { getHostPortPlan } from "../utils/host-port-config.js";
10
10
  import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
11
+ import { nativeServiceStates } from "../utils/native-runtime.js";
12
+ import { isNativeProject } from "../utils/native-service.js";
11
13
  import { findProjectRoot } from "../utils/paths.js";
12
14
  import { deriveServiceStates } from "../utils/service-runtime-state.js";
13
15
  import { groupSharedPostgres, readProjectName } from "../utils/shared-infra.js";
@@ -31,19 +33,30 @@ export const statusCommand = new Command("status")
31
33
  const sharedInfraGroup = projectName
32
34
  ? groupSharedPostgres(projectName, discovery.stacks)
33
35
  : null;
36
+ // A project of `command` services runs as local processes: its state comes from the run on record, not from Tilt.
37
+ const nativeStates = projectRoot && isNativeProject(discovery.resources)
38
+ ? nativeServiceStates(projectRoot, discovery.resources.map((resource) => resource.name))
39
+ : null;
34
40
  let tiltAvailable = false;
35
- try {
36
- tiltAvailable = await isTiltAvailable();
37
- }
38
- catch {
39
- // Tilt not available (spawn error)
40
- tiltAvailable = false;
41
+ if (!nativeStates) {
42
+ try {
43
+ tiltAvailable = await isTiltAvailable();
44
+ }
45
+ catch {
46
+ // Tilt not available (spawn error)
47
+ tiltAvailable = false;
48
+ }
41
49
  }
42
50
  if (!options.json) {
43
51
  showStep("Flisk Status\n");
44
- console.log(chalk.bold("Tilt:"), tiltAvailable ? chalk.green("available") : chalk.red("not found"));
45
- if (!tiltAvailable) {
46
- showDetail("Install Tilt: https://docs.tilt.dev/install.html");
52
+ if (nativeStates) {
53
+ console.log(chalk.bold("Runtime:"), "local processes (no Docker, no Tilt)");
54
+ }
55
+ else {
56
+ console.log(chalk.bold("Tilt:"), tiltAvailable ? chalk.green("available") : chalk.red("not found"));
57
+ if (!tiltAvailable) {
58
+ showDetail("Install Tilt: https://docs.tilt.dev/install.html");
59
+ }
47
60
  }
48
61
  console.log();
49
62
  }
@@ -53,8 +66,17 @@ export const statusCommand = new Command("status")
53
66
  // Tilt answers or its output cannot be read.
54
67
  let readiness = null;
55
68
  // The same look at Tilt feeds readiness and each service's state, so `flisk status` and `flisk ui` cannot disagree.
56
- let serviceStates = null;
69
+ let serviceStates = nativeStates;
57
70
  const tiltPort = getTiltPollingPort(process.env.TILT_PORT);
71
+ // Port plan is best effort; service URLs are shown in both JSON and terminal output.
72
+ let portPlan = null;
73
+ try {
74
+ const root = findProjectRoot();
75
+ portPlan = root ? await getHostPortPlan(root, { inspectDocker: false }) : null;
76
+ }
77
+ catch {
78
+ // Port planning failed; service URLs will be omitted from terminal output.
79
+ }
58
80
  if (tiltAvailable) {
59
81
  const text = await tiltGetUiResources(tiltPort);
60
82
  // `tilt get` has no -f flag; resources come from the running Tilt's API on its port, the same source as readiness.
@@ -89,16 +111,24 @@ export const statusCommand = new Command("status")
89
111
  }
90
112
  }
91
113
  if (options.json) {
92
- let portPlan = null;
93
- try {
94
- const root = findProjectRoot();
95
- portPlan = root ? await getHostPortPlan(root, { inspectDocker: false }) : null;
96
- }
97
- catch {
98
- // Port planning is best effort; stack ports then list only the Tilt UI.
99
- }
100
114
  const errors = queryError ? [{ code: "TILT_STATUS_UNAVAILABLE", message: queryError }] : [];
115
+ // A native run answers the readiness question agents poll (data.tilt.readiness.ready) from its own run state.
116
+ if (nativeStates) {
117
+ const states = Object.entries(nativeStates);
118
+ const isRunning = states.some(([, state]) => state.status !== "unknown");
119
+ readiness = isRunning
120
+ ? {
121
+ ready: states.every(([, state]) => state.status === "ready"),
122
+ failures: [],
123
+ pending: states.filter(([, state]) => state.status !== "ready").length,
124
+ timedOut: false,
125
+ enabled: states.map(([name]) => name),
126
+ }
127
+ : null;
128
+ }
101
129
  const data = {
130
+ // Only a project of local processes says so; a container project's report keeps its shape.
131
+ ...(nativeStates ? { runtime: "native" } : {}),
102
132
  tilt: {
103
133
  available: tiltAvailable,
104
134
  resourcesQueried: options.tilt && tiltAvailable,
@@ -178,9 +208,17 @@ export const statusCommand = new Command("status")
178
208
  : state.status === "error"
179
209
  ? chalk.red("✗")
180
210
  : chalk.yellow("…");
181
- showDetail(`${mark} ${resource.name} ${state.status}${state.reason ? ` (${state.reason})` : ""}`);
211
+ const ports = buildServicePorts(resource, portPlan?.ingressHttp);
212
+ const urlPart = ports.url ? ` → ${ports.url}` : "";
213
+ showDetail(`${mark} ${resource.name}${urlPart} ${state.status}${state.reason ? ` (${state.reason})` : ""}`);
182
214
  }
183
215
  }
216
+ else if (discovery.resources.length > 0) {
217
+ // Without a running Tilt, no service state can be read. Say so instead of printing nothing.
218
+ console.log();
219
+ console.log(chalk.bold("Services:"));
220
+ showDetail(`unknown: no Tilt answers on port ${tiltPort}, so nothing is confirmed running. Start with: flisk up`);
221
+ }
184
222
  if (options.tilt && tiltAvailable) {
185
223
  console.log();
186
224
  showStep("Tilt Resources:");