@flisk-sh/flisk 1.4.5 → 1.4.7

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 (117) hide show
  1. package/README.md +37 -14
  2. package/cli/dist/agent-runtimes/custom.js +94 -13
  3. package/cli/dist/commands/agent.js +144 -123
  4. package/cli/dist/commands/doctor.js +62 -12
  5. package/cli/dist/commands/down.js +33 -1
  6. package/cli/dist/commands/logs.js +107 -3
  7. package/cli/dist/commands/project.js +27 -0
  8. package/cli/dist/commands/resource.js +3 -1
  9. package/cli/dist/commands/status.js +82 -34
  10. package/cli/dist/commands/ui.js +41 -2
  11. package/cli/dist/commands/up.js +173 -12
  12. package/cli/dist/commands/upgrade.js +2 -2
  13. package/cli/dist/components/DetailPanel.js +23 -3
  14. package/cli/dist/mcp/tools.js +9 -6
  15. package/cli/dist/types/index.js +0 -2
  16. package/cli/dist/utils/agent-format.js +18 -0
  17. package/cli/dist/utils/agent-sessions.js +122 -0
  18. package/cli/dist/utils/agent-skills.js +64 -0
  19. package/cli/dist/utils/bun-requirement.js +7 -4
  20. package/cli/dist/utils/cold-preflight.js +55 -32
  21. package/cli/dist/utils/doctor-fix-first.js +93 -0
  22. package/cli/dist/utils/doctor-native.js +96 -0
  23. package/cli/dist/utils/doctor-report.js +3 -0
  24. package/cli/dist/utils/native-project.js +41 -0
  25. package/cli/dist/utils/native-runtime.js +516 -0
  26. package/cli/dist/utils/native-service.js +95 -0
  27. package/cli/dist/utils/native-up.js +156 -0
  28. package/cli/dist/utils/platform.js +1 -1
  29. package/cli/dist/utils/project-templates.js +1 -1
  30. package/cli/dist/utils/service-manifest.js +10 -1
  31. package/cli/dist/utils/services.js +11 -1
  32. package/cli/dist/utils/startup-report.js +45 -3
  33. package/cli/dist/utils/status-groups.js +23 -0
  34. package/cli/dist/utils/tilt-logs.js +8 -0
  35. package/cli/dist/utils/up-hints.js +42 -0
  36. package/cli/dist/utils/up-progress.js +117 -0
  37. package/cli/dist/utils/up-readiness.js +5 -0
  38. package/cli/dist-bundle/{chunk-Z5QEE4SW.js → chunk-6OW2GVQV.js} +1 -1
  39. package/cli/dist-bundle/chunk-B2JOM3HN.js +35 -0
  40. package/cli/dist-bundle/{chunk-VMVTECCK.js → chunk-F2DLIM75.js} +1 -1
  41. package/cli/dist-bundle/chunk-L6O7TT3K.js +18 -0
  42. package/cli/dist-bundle/chunk-MFY7RZ76.js +130 -0
  43. package/cli/dist-bundle/{chunk-SFOFTVYM.js → chunk-MYAYJF4Q.js} +1 -1
  44. package/cli/dist-bundle/{chunk-LUUJ4JB7.js → chunk-P7GZ6NGM.js} +1 -1
  45. package/cli/dist-bundle/{chunk-BP2M4OJ4.js → chunk-QEBN6ZVQ.js} +1 -1
  46. package/cli/dist-bundle/{chunk-4J2FLFUA.js → chunk-T44DEEL6.js} +1 -1
  47. package/cli/dist-bundle/{chunk-U42XZWXB.js → chunk-TIPSERWO.js} +1 -1
  48. package/cli/dist-bundle/{chunk-V36PT2HF.js → chunk-VYPB7MY5.js} +1 -1
  49. package/cli/dist-bundle/{chunk-TIYTGN5R.js → chunk-XHENT4KO.js} +1 -1
  50. package/cli/dist-bundle/{chunk-CGTWXWBU.js → chunk-YTBTCAWY.js} +1 -1
  51. package/cli/dist-bundle/cli.js +486 -396
  52. package/cli/dist-bundle/cold-preflight-DQVJUGBB.js +2 -0
  53. package/cli/dist-bundle/{demo-stage-G4W23BXW.js → demo-stage-F4YA7UL2.js} +1 -1
  54. package/cli/dist-bundle/{open-HQFT6WDC.js → open-ZUSSVPD3.js} +1 -1
  55. package/cli/dist-bundle/project-JFCTPRP5.js +2 -0
  56. package/cli/dist-bundle/{project-templates-IMJ3OX5I.js → project-templates-2VQPUBSQ.js} +1 -1
  57. package/cli/dist-bundle/{services-GLFKZBBZ.js → services-DOHDHOCT.js} +1 -1
  58. package/cli/dist-bundle/{tilt-port-owner-PJN4MPYE.js → tilt-port-owner-XODQHXHX.js} +1 -1
  59. package/cli/package.json +1 -1
  60. package/cli/templates/agent-skills/flisk-agent-ops/SKILL.md +39 -0
  61. package/cli/templates/agent-skills/flisk-build/SKILL.md +85 -0
  62. package/cli/templates/agent-skills/flisk-doctor/SKILL.md +28 -0
  63. package/cli/templates/agent-skills/flisk-recipe-ai-agent/SKILL.md +45 -0
  64. package/cli/templates/agent-skills/flisk-recipe-blog/SKILL.md +36 -0
  65. package/cli/templates/agent-skills/flisk-recipe-saas/SKILL.md +39 -0
  66. package/cli/templates/agent-skills/flisk-recipe-shop/SKILL.md +41 -0
  67. package/cli/templates/agent-skills/flisk-recipe-starters/SKILL.md +46 -0
  68. package/cli/templates/agent-skills/flisk-resource/SKILL.md +27 -0
  69. package/cli/templates/agent-skills/flisk-status/SKILL.md +25 -0
  70. package/cli/templates/agent-skills/flisk-troubleshoot/SKILL.md +227 -0
  71. package/cli/templates/agent-skills/flisk-up/SKILL.md +37 -0
  72. package/examples/{tdk-example → flisk-example}/README.md +2 -2
  73. package/package.json +2 -2
  74. package/runtime/discovery/AGENTS.md +1 -1
  75. package/runtime/discovery/TILT_DISCOVERY.star +1 -1
  76. package/runtime/discovery/deterministic.star +1 -1
  77. package/runtime/engine/AGENTS.md +3 -3
  78. package/runtime/engine/README.md +1 -1
  79. package/runtime/engine/REFACTORING_MANIFEST_SYSTEM.md +1 -1
  80. package/runtime/engine/assets/docker/wake-gateway/decide.ts +1 -1
  81. package/runtime/engine/assets/docker/wake-gateway/server.ts +2 -2
  82. package/runtime/engine/assets/scripts/mac-m1-resource-monitor.sh +6 -6
  83. package/runtime/engine/schemas/service-schema.json +87 -21
  84. package/runtime/engine/scripts/debug-container.sh +1 -1
  85. package/runtime/engine/scripts/generate-manifest.sh +2 -2
  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/networking/traefik_static_routes.star +2 -2
  89. package/runtime/engine/topologies/tilt/manifest/mapper.star +1 -1
  90. package/runtime/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +8 -2
  91. package/runtime/ext/ide-components/README.md +1 -1
  92. package/runtime/ext/ui-enhancements/extension.yaml +2 -2
  93. package/runtime/specs/specs/TILT_SERVICE_DEFAULTS.star +1 -1
  94. package/runtime/specs/specs/TILT_TECH_STACK.star +1 -1
  95. package/cli/dist-bundle/chunk-BMHPPX2Q.js +0 -35
  96. package/cli/dist-bundle/chunk-PEZQEZVV.js +0 -17
  97. package/cli/dist-bundle/chunk-ULMQVD7J.js +0 -122
  98. package/cli/dist-bundle/cold-preflight-3BNDOLGS.js +0 -2
  99. package/cli/dist-bundle/project-JZO2CPZZ.js +0 -2
  100. /package/examples/{tdk-example → flisk-example}/LICENSE +0 -0
  101. /package/examples/{tdk-example → flisk-example}/TILT_SERVICE_DEFAULTS.star +0 -0
  102. /package/examples/{tdk-example → flisk-example}/TILT_TECH_STACK.star +0 -0
  103. /package/examples/{tdk-example → flisk-example}/Tiltfile +0 -0
  104. /package/examples/{tdk-example → flisk-example}/services/platform/messaging/docker-compose.yml +0 -0
  105. /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/package.json +0 -0
  106. /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/service.json +0 -0
  107. /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/src/index.ts +0 -0
  108. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/index.html +0 -0
  109. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/package.json +0 -0
  110. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/public/.gitkeep +0 -0
  111. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/service.json +0 -0
  112. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/main.tsx +0 -0
  113. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/orders.tsx +0 -0
  114. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/style.css +0 -0
  115. /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/package.json +0 -0
  116. /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/service.json +0 -0
  117. /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/src/index.ts +0 -0
@@ -14,7 +14,9 @@ import { AUTOGENERATED_DIR, CONTAINER_RUNTIME_CHECK, MASTER_CONFIG_FILES, PACKAG
14
14
  import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
15
15
  import { checkCrashLoopingContainers } from "../utils/doctor-crashloop.js";
16
16
  import { applyDoctorFixes, checkDuplicateFliskInstalls, hasFailedActions, planDoctorFixes, projectTiltPids, } from "../utils/doctor-fix.js";
17
+ import { formatFixFirst, rankDoctorFixes } from "../utils/doctor-fix-first.js";
17
18
  import { checkDiskSpace, dockerDesktopStuck } from "../utils/doctor-host.js";
19
+ import { checkNativeCommands, checkNativeDependsOn, checkNativeRuntime, checkNativeServiceHealth, } from "../utils/doctor-native.js";
18
20
  import { collectDoctorChecks, createDoctorReport, getDoctorExitCode, } from "../utils/doctor-report.js";
19
21
  import { checkHealthRoutes, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeAdvertisedProbes, summarizeServiceProbes, summarizeSmokeResults, } from "../utils/doctor-runtime.js";
20
22
  import { checkStarlarkLoadExports } from "../utils/doctor-starlark.js";
@@ -25,6 +27,7 @@ import { evaluateFliskVersionFloor } from "../utils/flisk-version.js";
25
27
  import { formatCount } from "../utils/formatting.js";
26
28
  import { getHostPortPlan, resolveRuntimeIngressPort } from "../utils/host-port-config.js";
27
29
  import { createHostPortPlan } from "../utils/host-port-plan.js";
30
+ import { findNativeProject } from "../utils/native-project.js";
28
31
  import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
29
32
  import { promptConfirm } from "../utils/prompt.js";
30
33
  import { EnvUnreadableError, envSecretValues, redactSecrets, redactValue, } from "../utils/secret-redaction.js";
@@ -1214,6 +1217,15 @@ function printDoctorResults(list) {
1214
1217
  }
1215
1218
  export function renderDoctorOutput(machineResults, projectResults, inProject, options = {}, allResults) {
1216
1219
  const results = allResults ?? [...machineResults, ...projectResults];
1220
+ const printFixFirst = () => {
1221
+ const lines = formatFixFirst(rankDoctorFixes(results));
1222
+ if (lines.length === 0)
1223
+ return;
1224
+ console.log(chalk.bold("Fix first"));
1225
+ for (const line of lines)
1226
+ console.log(line);
1227
+ console.log("");
1228
+ };
1217
1229
  if (options.all) {
1218
1230
  console.log(chalk.bold("Machine"));
1219
1231
  printDoctorResults(machineResults);
@@ -1228,10 +1240,12 @@ export function renderDoctorOutput(machineResults, projectResults, inProject, op
1228
1240
  }
1229
1241
  console.log("");
1230
1242
  console.log(formatDoctorSummary(results));
1243
+ printFixFirst();
1231
1244
  }
1232
1245
  else {
1233
1246
  printDoctorResults(results);
1234
1247
  console.log("");
1248
+ printFixFirst();
1235
1249
  const allPassed = results.every((check) => check.didPass || check.isSkipped || check.isWarning);
1236
1250
  if (allPassed && !inProject) {
1237
1251
  console.log(`${chalk.gray("○")} ${chalk.gray("Not in a Flisk project, so project checks were skipped")}`);
@@ -1285,8 +1299,13 @@ export const doctorCommand = new Command("doctor")
1285
1299
  process.exit(2);
1286
1300
  return;
1287
1301
  }
1302
+ // A project whose services all declare `command` runs as local processes: doctor checks only what they need (#2231).
1303
+ const nativeProject = findNativeProject();
1304
+ const nativeResources = nativeProject?.resources ?? null;
1288
1305
  if (!options.json) {
1289
- console.log("Flisk doctor checks your local Docker + Tilt development environment; it does not check cluster deployments.");
1306
+ console.log(nativeResources
1307
+ ? "Flisk doctor checks what this project's local processes need; its services declare commands, so Docker and Tilt are not checked."
1308
+ : "Flisk doctor checks your local Docker + Tilt development environment; it does not check cluster deployments.");
1290
1309
  }
1291
1310
  const host = detectHost();
1292
1311
  if (process.platform === "win32") {
@@ -1343,10 +1362,15 @@ export const doctorCommand = new Command("doctor")
1343
1362
  console.log(`\n${chalk.bold("🔍 Flisk Doctor")}\n`);
1344
1363
  console.log("Checking environment...\n");
1345
1364
  }
1346
- const projectRoot = findProjectRoot();
1365
+ // A directory of command services is a project for doctor even before `flisk up` writes its .flisk/project.json.
1366
+ const projectRoot = findProjectRoot() ?? nativeProject?.projectRoot ?? null;
1347
1367
  // Repairs run before the checks so the checks report the state after the fix.
1348
1368
  let fixActions;
1349
- if (options.fix) {
1369
+ if (options.fix && nativeResources) {
1370
+ // The repairs are for Docker networks and Tilt probes, which a project of local processes does not have.
1371
+ fixActions = [];
1372
+ }
1373
+ else if (options.fix) {
1350
1374
  try {
1351
1375
  let projectName;
1352
1376
  if (projectRoot) {
@@ -1388,16 +1412,19 @@ export const doctorCommand = new Command("doctor")
1388
1412
  if (!options.json)
1389
1413
  printFixActions(fixActions, options.dryRun);
1390
1414
  }
1415
+ // The host port plan places the gateway and Postgres containers; a project of local processes has neither.
1391
1416
  let hostPortPlan;
1392
1417
  let portPlanError;
1393
- try {
1394
- hostPortPlan = await getHostPortPlan(projectRoot ?? process.cwd());
1395
- }
1396
- catch (error) {
1397
- hostPortPlan = null;
1398
- portPlanError = error instanceof Error ? error.message : String(error);
1418
+ if (!nativeResources) {
1419
+ try {
1420
+ hostPortPlan = await getHostPortPlan(projectRoot ?? process.cwd());
1421
+ }
1422
+ catch (error) {
1423
+ hostPortPlan = null;
1424
+ portPlanError = error instanceof Error ? error.message : String(error);
1425
+ }
1399
1426
  }
1400
- const machineChecks = [
1427
+ const containerMachineChecks = [
1401
1428
  () => hostPortPlan
1402
1429
  ? {
1403
1430
  name: "Host Ports",
@@ -1434,7 +1461,7 @@ export const doctorCommand = new Command("doctor")
1434
1461
  requiresContainerRuntime("Docker Network Pool", () => checkDockerNetworkCapacity()),
1435
1462
  () => checkDuplicateFliskInstalls(),
1436
1463
  ];
1437
- const projectChecks = [
1464
+ const containerProjectChecks = [
1438
1465
  () => checkFliskVersion(),
1439
1466
  checkMasterConfigs,
1440
1467
  checkGeneratedProjectRuntimeAssets,
@@ -1495,8 +1522,31 @@ export const doctorCommand = new Command("doctor")
1495
1522
  // When Tilt is up, surface red resources (Traefik/apps never started).
1496
1523
  requiresContainerRuntime("Tilt Resources", checkTiltResourceHealth),
1497
1524
  ];
1525
+ const machineChecks = nativeResources
1526
+ ? [
1527
+ () => checkNativeRuntime(nativeResources),
1528
+ () => checkNativeCommands(nativeResources),
1529
+ () => checkWslProjectLocation(findProjectRoot() ?? process.cwd(), options.strict),
1530
+ () => checkDuplicateFliskInstalls(),
1531
+ ]
1532
+ : containerMachineChecks;
1533
+ const projectChecks = nativeResources
1534
+ ? [
1535
+ () => checkFliskVersion(),
1536
+ () => checkDuplicateResourceNames(),
1537
+ () => checkDependsOnShape(),
1538
+ () => checkNativeDependsOn(nativeResources),
1539
+ () => checkCircularDependencies(),
1540
+ () => checkServicePorts(),
1541
+ () => checkSchemaVersions(undefined, options.strict),
1542
+ () => checkDuplicateResourcePorts(),
1543
+ ]
1544
+ : containerProjectChecks;
1545
+ if (options.ping && nativeResources && projectRoot) {
1546
+ projectChecks.push(() => checkNativeServiceHealth(projectRoot, nativeResources, pingTimeout));
1547
+ }
1498
1548
  // Runs last: needs routable services (and working Traefik) to mean anything.
1499
- if (options.ping) {
1549
+ if (options.ping && !nativeResources) {
1500
1550
  // Probe the port a running stack is actually on, not the one the next `flisk up` would pick.
1501
1551
  const pingPort = projectRoot
1502
1552
  ? 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 });
@@ -2,12 +2,18 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { Command } from "commander";
4
4
  import { STANDARD_PORTS } from "../utils/constants.js";
5
+ import { getDeferredResourceNames } from "../utils/doctor-runtime.js";
5
6
  import { errorFactories, FliskError, runCommand, showErrorAndExit } from "../utils/errors.js";
6
7
  import { createMachineEnvelope } from "../utils/machine-output.js";
8
+ import { nativeServiceStates, readNativeLogLines } from "../utils/native-runtime.js";
9
+ import { isNativeProject } from "../utils/native-service.js";
7
10
  import { findProjectRoot } from "../utils/paths.js";
8
11
  import { EnvUnreadableError, envSecretValues, redactSecrets, redactValue, } from "../utils/secret-redaction.js";
12
+ import { deriveServiceStates } from "../utils/service-runtime-state.js";
13
+ import { discoverResourcesFromRoot } from "../utils/services.js";
14
+ import { unhealthyServiceNames } from "../utils/status-groups.js";
9
15
  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";
16
+ import { DEFAULT_LOG_TAIL, isTiltConnectionFailure, isValidPort, isValidSince, isValidTail, MAX_LOG_TAIL, parseTiltLogLines, sinceMilliseconds, } from "../utils/tilt-logs.js";
11
17
  import { tiltUnreachableMessage } from "../utils/tilt-unreachable-message.js";
12
18
  /** Resource names Tilt knows about, or null when they cannot be listed (the logs call then reports the real error). */
13
19
  async function tiltResourceNames(port) {
@@ -25,12 +31,56 @@ async function tiltResourceNames(port) {
25
31
  return null;
26
32
  }
27
33
  }
34
+ /** Raw `tilt get uiresources` JSON, or null when Tilt cannot be read. */
35
+ async function tiltResourcesJson(port) {
36
+ try {
37
+ const result = await runTilt("get", ["uiresources", "-o", "json", "--port", port], {
38
+ inheritStdio: false,
39
+ });
40
+ return result.exitCode === 0 ? result.stdout : null;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ /** Failing or pending services among the project's, from the running Tilt; null when Tilt cannot be read. */
47
+ async function tiltUnhealthyNames(projectRoot, port) {
48
+ const text = await tiltResourcesJson(port);
49
+ if (!text)
50
+ return null;
51
+ let resources = [];
52
+ try {
53
+ resources = discoverResourcesFromRoot(projectRoot);
54
+ }
55
+ catch {
56
+ // Without discovery there are no dependsOn edges; Tilt's own states still decide.
57
+ }
58
+ try {
59
+ const states = deriveServiceStates(text, Object.fromEntries(resources.map((r) => [r.name, r.config?.dependsOn ?? []])), getDeferredResourceNames(resources));
60
+ return unhealthyServiceNames(Object.keys(states).map((name) => ({ name })), states);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ /** The project's services when every one declares `command` (they run as local processes), else null. */
67
+ function nativeResources(projectRoot) {
68
+ try {
69
+ const resources = discoverResourcesFromRoot(projectRoot);
70
+ return isNativeProject(resources) ? resources : null;
71
+ }
72
+ catch {
73
+ // Discovery problems are reported by the commands that start the stack; logs still asks Tilt.
74
+ return null;
75
+ }
76
+ }
28
77
  export const logsCommand = new Command("logs")
29
78
  .description("Print a bounded snapshot of recent logs from the running stack")
30
79
  .showHelpAfterError("\nService names are not positional. Use: flisk logs --service <name>")
31
80
  .option("-s, --service <names...>", "Only these services (Tilt resource names)")
32
81
  .option("--tail <n>", "Number of most recent lines to return", String(DEFAULT_LOG_TAIL))
33
82
  .option("--since <duration>", "Only logs newer than this duration, for example 30s, 5m, 1h")
83
+ .option("--unhealthy", "Only services that are failing or still starting", false)
34
84
  .option("--port <n>", "Tilt UI port (default: TILT_PORT or 10350)")
35
85
  .option("--json", "Output one JSON object and exit", false)
36
86
  .action(async (options) => {
@@ -64,6 +114,19 @@ export const logsCommand = new Command("logs")
64
114
  fail("USAGE", "--port must be a valid port number", 2);
65
115
  const tail = Number(options.tail);
66
116
  const services = options.service ?? [];
117
+ const reportAllHealthy = () => {
118
+ if (options.json) {
119
+ console.log(JSON.stringify(createMachineEnvelope({
120
+ services,
121
+ tail,
122
+ since: options.since ?? null,
123
+ lines: [],
124
+ unhealthy: [],
125
+ })));
126
+ return;
127
+ }
128
+ console.log("All services are healthy; nothing to show with --unhealthy.");
129
+ };
67
130
  const action = async () => {
68
131
  const projectRoot = findProjectRoot();
69
132
  if (!projectRoot) {
@@ -81,6 +144,37 @@ export const logsCommand = new Command("logs")
81
144
  "Fix the read permission on .env, or move it out of the project",
82
145
  ]);
83
146
  }
147
+ // A project of `command` services keeps its output in .flisk/native/logs, so no Tilt is involved.
148
+ const resources = nativeResources(projectRoot);
149
+ if (resources) {
150
+ const known = resources.map((resource) => resource.name);
151
+ const unknown = services.filter((name) => !known.includes(name));
152
+ if (unknown.length > 0) {
153
+ const error = errorFactories.unknownServices(unknown, known);
154
+ fail("UNKNOWN_SERVICE", error.message, error.exitCode, error.suggestions);
155
+ }
156
+ let selected = services.length > 0 ? services : known;
157
+ if (options.unhealthy) {
158
+ const states = nativeServiceStates(projectRoot, known);
159
+ const bad = new Set(unhealthyServiceNames(resources, states));
160
+ selected = selected.filter((name) => bad.has(name));
161
+ if (selected.length === 0)
162
+ return reportAllHealthy();
163
+ }
164
+ const lines = readNativeLogLines(projectRoot, selected, {
165
+ tail,
166
+ sinceMs: options.since === undefined ? undefined : sinceMilliseconds(options.since),
167
+ });
168
+ if (options.json) {
169
+ console.log(JSON.stringify(redactValue(createMachineEnvelope({ services, tail, since: options.since ?? null, lines }), secrets)));
170
+ return;
171
+ }
172
+ const width = Math.max(0, ...lines.map((line) => line.resource.length));
173
+ for (const line of lines) {
174
+ console.log(redactSecrets(`${line.resource.padEnd(width)} | ${line.text}`, secrets));
175
+ }
176
+ return;
177
+ }
84
178
  if (!(await isTiltAvailable())) {
85
179
  fail("TILT_MISSING", "Tilt is not installed. Run: flisk doctor");
86
180
  }
@@ -93,14 +187,24 @@ export const logsCommand = new Command("logs")
93
187
  fail("UNKNOWN_SERVICE", error.message, error.exitCode, suggestions);
94
188
  }
95
189
  }
190
+ let selectedServices = services;
191
+ if (options.unhealthy) {
192
+ const bad = await tiltUnhealthyNames(projectRoot, portText);
193
+ if (bad === null)
194
+ fail("TILT_NOT_RUNNING", tiltUnreachableMessage(portText));
195
+ selectedServices =
196
+ services.length > 0 ? services.filter((n) => bad?.includes(n)) : (bad ?? []);
197
+ if (selectedServices.length === 0)
198
+ return reportAllHealthy();
199
+ }
96
200
  const args = ["--port", portText, "--tail", String(tail)];
97
201
  if (options.since !== undefined)
98
202
  args.push("--since", options.since);
99
203
  if (options.json)
100
204
  args.push("--json");
101
205
  // `--` so a service name that starts with "-" is never read as a Tilt flag.
102
- if (services.length > 0)
103
- args.push("--", ...services);
206
+ if (selectedServices.length > 0)
207
+ args.push("--", ...selectedServices);
104
208
  const result = await runTilt("logs", args, { inheritStdio: false });
105
209
  if (result.exitCode !== 0) {
106
210
  const detail = result.stderr.trim();
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  import chalk from "chalk";
9
9
  import { Command } from "commander";
10
10
  import { generateMasterConfigs, readProjectConfig } from "../generator/template-engine.js";
11
+ import { installAgentSkills } from "../utils/agent-skills.js";
11
12
  import { addYesOption } from "../utils/command-helpers.js";
12
13
  import { MASTER_CONFIG_FILES, PROJECT_JSON } from "../utils/constants.js";
13
14
  import { clearDiscoveryCache } from "../utils/discovery-context.js";
@@ -213,6 +214,17 @@ const DEFAULT_PROJECT_JSON = {
213
214
  },
214
215
  overrides: {},
215
216
  };
217
+ /**
218
+ * Writes .flisk/project.json with the defaults `flisk project --yes` uses, and nothing else: no generated Tiltfile, Compose file,
219
+ * .env or engine copy. `flisk up` uses it to adopt a directory of `command` services as a project, so `flisk down` and
220
+ * `flisk doctor` find it afterwards.
221
+ */
222
+ export function writeDefaultProjectConfig(projectRoot) {
223
+ const projectConfig = JSON.parse(JSON.stringify(DEFAULT_PROJECT_JSON));
224
+ projectConfig.project.name = basename(projectRoot) || "my-project";
225
+ ensureDirectory(join(projectRoot, ".flisk"));
226
+ writeJsonFile(join(projectRoot, ".flisk", PROJECT_JSON), projectConfig);
227
+ }
216
228
  export const projectCommand = addYesOption(new Command("project"))
217
229
  .alias("init")
218
230
  .description("Initialize or validate project-level master configuration")
@@ -222,6 +234,7 @@ export const projectCommand = addYesOption(new Command("project"))
222
234
  .option("--force", "Overwrite existing configuration (dangerous)")
223
235
  .option("--config-file <path>", "Load project config from existing JSON file")
224
236
  .option("-q, --quiet", "Only print what fails; used by `flisk demo`", false)
237
+ .option("--no-agent-skills", "Do not write the Flisk agent skills (.claude/skills, .agents/skills) and AGENTS.md")
225
238
  .action(async (template, options) => {
226
239
  const { assertMachineReadyOrExit, detectRuntimePrerequisites, formatRuntimeNextSteps } = await import("../utils/cold-preflight.js");
227
240
  // Project setup only writes files: it must not need a running Docker daemon
@@ -482,6 +495,20 @@ export const projectCommand = addYesOption(new Command("project"))
482
495
  if (ignored.length > 0 && !quiet) {
483
496
  showSuccess(`Added to .gitignore: ${ignored.join(", ")}`);
484
497
  }
498
+ // A coding agent opened in the project then knows to build and run the app with Flisk.
499
+ // The project works without them, so a failure here is a warning, not an exit.
500
+ if (options.agentSkills !== false) {
501
+ try {
502
+ const skillFiles = installAgentSkills(projectRoot);
503
+ if (skillFiles.length > 0 && !quiet) {
504
+ showSuccess("Added agent skills: .claude/skills, .agents/skills and AGENTS.md (start with flisk-build)");
505
+ }
506
+ }
507
+ catch (error) {
508
+ const reason = error instanceof Error ? error.message : String(error);
509
+ console.log(chalk.yellow(`⚠️ Could not add the agent skills: ${reason}`));
510
+ }
511
+ }
485
512
  if (!quiet) {
486
513
  console.log(chalk.green("\n✅ Project configuration complete!"));
487
514
  showDetail("\nGenerated files in .flisk/.flisk-out/:", 0);
@@ -231,7 +231,9 @@ export function createPackageJson(name, type, frameworkId, prismaEnabled = false
231
231
  // vitest 5 peers on vite ^6.4.0 || ^7 || ^8; vite 5 makes `vitest run` fail at startup with
232
232
  // ERR_PACKAGE_PATH_NOT_EXPORTED ('./module-runner'), in every resource once it is hoisted.
233
233
  ...(isFrontend && !framework?.scripts ? { vite: "^6.4.1" } : {}),
234
- ...(drizzleEnabled ? { "drizzle-kit": DRIZZLE_KIT_RANGE, "@types/pg": "^8.11.0" } : {}),
234
+ ...(drizzleEnabled ? { "drizzle-kit": DRIZZLE_KIT_RANGE } : {}),
235
+ // Both import 'pg' (src/db.ts, the agent's checkpoint store); without the types the image's `tsc` fails with TS7016.
236
+ ...(drizzleEnabled || isPostgresAgent ? { "@types/pg": "^8.11.0" } : {}),
235
237
  ...(framework?.devDependencies ?? {}),
236
238
  ...(backendFramework?.devDependencies ?? {}),
237
239
  },
@@ -8,9 +8,12 @@ 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";
16
+ import { groupServicesByStack, unhealthyServiceNames } from "../utils/status-groups.js";
14
17
  import { buildServicePorts, buildStackPorts } from "../utils/status-ports.js";
15
18
  import { isTiltAvailable, runTilt } from "../utils/tilt.js";
16
19
  import { getTiltPollingPort } from "../utils/tilt-startup.js";
@@ -22,6 +25,7 @@ export const statusCommand = new Command("status")
22
25
  .option("--stacks", "Show stack information (default)", true)
23
26
  .option("--resources", "Show all discovered resources", false)
24
27
  .option("--tilt", "Show tilt resource status", false)
28
+ .option("--unhealthy", "Show only services that are failing or still starting", false)
25
29
  .action(async (options) => {
26
30
  const action = async () => {
27
31
  // Discover first: outside a project this fails before any status line is printed.
@@ -31,19 +35,30 @@ export const statusCommand = new Command("status")
31
35
  const sharedInfraGroup = projectName
32
36
  ? groupSharedPostgres(projectName, discovery.stacks)
33
37
  : null;
38
+ // A project of `command` services runs as local processes: its state comes from the run on record, not from Tilt.
39
+ const nativeStates = projectRoot && isNativeProject(discovery.resources)
40
+ ? nativeServiceStates(projectRoot, discovery.resources.map((resource) => resource.name))
41
+ : null;
34
42
  let tiltAvailable = false;
35
- try {
36
- tiltAvailable = await isTiltAvailable();
37
- }
38
- catch {
39
- // Tilt not available (spawn error)
40
- tiltAvailable = false;
43
+ if (!nativeStates) {
44
+ try {
45
+ tiltAvailable = await isTiltAvailable();
46
+ }
47
+ catch {
48
+ // Tilt not available (spawn error)
49
+ tiltAvailable = false;
50
+ }
41
51
  }
42
52
  if (!options.json) {
43
53
  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");
54
+ if (nativeStates) {
55
+ console.log(chalk.bold("Runtime:"), "local processes (no Docker, no Tilt)");
56
+ }
57
+ else {
58
+ console.log(chalk.bold("Tilt:"), tiltAvailable ? chalk.green("available") : chalk.red("not found"));
59
+ if (!tiltAvailable) {
60
+ showDetail("Install Tilt: https://docs.tilt.dev/install.html");
61
+ }
47
62
  }
48
63
  console.log();
49
64
  }
@@ -53,7 +68,7 @@ export const statusCommand = new Command("status")
53
68
  // Tilt answers or its output cannot be read.
54
69
  let readiness = null;
55
70
  // The same look at Tilt feeds readiness and each service's state, so `flisk status` and `flisk ui` cannot disagree.
56
- let serviceStates = null;
71
+ let serviceStates = nativeStates;
57
72
  const tiltPort = getTiltPollingPort(process.env.TILT_PORT);
58
73
  // Port plan is best effort; service URLs are shown in both JSON and terminal output.
59
74
  let portPlan = null;
@@ -99,7 +114,23 @@ export const statusCommand = new Command("status")
99
114
  }
100
115
  if (options.json) {
101
116
  const errors = queryError ? [{ code: "TILT_STATUS_UNAVAILABLE", message: queryError }] : [];
117
+ // A native run answers the readiness question agents poll (data.tilt.readiness.ready) from its own run state.
118
+ if (nativeStates) {
119
+ const states = Object.entries(nativeStates);
120
+ const isRunning = states.some(([, state]) => state.status !== "unknown");
121
+ readiness = isRunning
122
+ ? {
123
+ ready: states.every(([, state]) => state.status === "ready"),
124
+ failures: [],
125
+ pending: states.filter(([, state]) => state.status !== "ready").length,
126
+ timedOut: false,
127
+ enabled: states.map(([name]) => name),
128
+ }
129
+ : null;
130
+ }
102
131
  const data = {
132
+ // Only a project of local processes says so; a container project's report keeps its shape.
133
+ ...(nativeStates ? { runtime: "native" } : {}),
103
134
  tilt: {
104
135
  available: tiltAvailable,
105
136
  resourcesQueried: options.tilt && tiltAvailable,
@@ -126,9 +157,12 @@ export const statusCommand = new Command("status")
126
157
  name: stack.name,
127
158
  resourceCount: stack.resourceCount,
128
159
  resources: stack.resources.map((resource) => resource.name),
160
+ healthyCount: stack.resources.filter((resource) => serviceStates?.[resource.name]?.status === "ready").length,
129
161
  })),
130
162
  unassignedResources: discovery.unassignedResources.map((resource) => resource.name),
131
163
  sharedInfra: sharedInfraGroup,
164
+ // Names of the failing or pending services, so a caller need not filter `resources` itself.
165
+ unhealthy: unhealthyServiceNames(discovery.resources, serviceStates),
132
166
  };
133
167
  console.log(JSON.stringify(createMachineEnvelope(data, errors)));
134
168
  if (errors.length > 0) {
@@ -139,27 +173,29 @@ export const statusCommand = new Command("status")
139
173
  }
140
174
  console.log(chalk.bold("Resources:"), `${discovery.resources.length} discovered`);
141
175
  console.log(chalk.bold("Stacks:"), `${discovery.stacks.length} defined`);
142
- if (discovery.stacks.length > 0) {
176
+ const hasStates = serviceStates !== null && Object.keys(serviceStates).length > 0;
177
+ const unhealthyOnly = Boolean(options.unhealthy);
178
+ // With service states the grouped "Services:" section below replaces the plain per-stack counts.
179
+ if (!hasStates && !unhealthyOnly) {
143
180
  for (const stack of discovery.stacks) {
144
- const resourcesInStack = stack.resources.length;
145
- showDetail(`${stack.name}: ${formatCount(resourcesInStack, "resource")}`);
181
+ showDetail(`${stack.name}: ${formatCount(stack.resources.length, "resource")}`);
146
182
  if (options.verbose) {
147
183
  for (const resource of stack.resources) {
148
184
  showDetail(`${resource.name}`, 6);
149
185
  }
150
186
  }
151
187
  }
152
- }
153
- if (discovery.unassignedResources.length > 0) {
154
- console.log();
155
- console.log(chalk.yellow(`${formatCount(discovery.unassignedResources.length, "resource")} not in any stack:`));
156
- if (options.verbose) {
157
- for (const resource of discovery.unassignedResources) {
158
- showDetail(`${resource.name}`);
188
+ if (discovery.unassignedResources.length > 0) {
189
+ console.log();
190
+ console.log(chalk.yellow(`${formatCount(discovery.unassignedResources.length, "resource")} not in any stack:`));
191
+ if (options.verbose) {
192
+ for (const resource of discovery.unassignedResources) {
193
+ showDetail(`${resource.name}`);
194
+ }
159
195
  }
160
196
  }
161
197
  }
162
- if (sharedInfraGroup) {
198
+ if (sharedInfraGroup && !unhealthyOnly) {
163
199
  console.log();
164
200
  console.log(chalk.bold("Shared infrastructure:"));
165
201
  showDetail(`${sharedInfraGroup.name} (one instance, ${formatCount(sharedInfraGroup.databases.length, "database")})`);
@@ -167,21 +203,33 @@ export const statusCommand = new Command("status")
167
203
  showDetail(` ${database.name} (stack ${database.stack}): ${database.services.join(", ")}`);
168
204
  }
169
205
  }
170
- if (serviceStates && Object.keys(serviceStates).length > 0) {
206
+ if (hasStates && serviceStates) {
207
+ const states = serviceStates;
171
208
  console.log();
172
- console.log(chalk.bold("Services:"));
173
- for (const resource of discovery.resources) {
174
- const state = serviceStates[resource.name];
175
- if (!state)
209
+ console.log(chalk.bold(unhealthyOnly ? "Unhealthy services:" : "Services:"));
210
+ const unhealthy = new Set(unhealthyServiceNames(discovery.resources, states));
211
+ if (unhealthyOnly && unhealthy.size === 0) {
212
+ showDetail(chalk.green("All services are healthy."));
213
+ }
214
+ for (const group of groupServicesByStack(discovery.stacks, discovery.unassignedResources, states)) {
215
+ const shown = group.resources.filter((resource) => states[resource.name] && (!unhealthyOnly || unhealthy.has(resource.name)));
216
+ if (unhealthyOnly && shown.length === 0)
176
217
  continue;
177
- const mark = state.status === "ready"
178
- ? chalk.green("✓")
179
- : state.status === "error"
180
- ? chalk.red("✗")
181
- : chalk.yellow("…");
182
- const ports = buildServicePorts(resource, portPlan?.ingressHttp);
183
- const urlPart = ports.url ? ` → ${ports.url}` : "";
184
- showDetail(`${mark} ${resource.name}${urlPart} ${state.status}${state.reason ? ` (${state.reason})` : ""}`);
218
+ const summary = `${group.healthy}/${group.total} healthy`;
219
+ showDetail(`${chalk.bold(group.name)}: ${group.healthy === group.total ? chalk.green(summary) : chalk.yellow(summary)}`);
220
+ for (const resource of shown) {
221
+ const state = states[resource.name];
222
+ if (!state)
223
+ continue;
224
+ const mark = state.status === "ready"
225
+ ? chalk.green("✓")
226
+ : state.status === "error"
227
+ ? chalk.red("✗")
228
+ : chalk.yellow("…");
229
+ const ports = buildServicePorts(resource, portPlan?.ingressHttp);
230
+ const urlPart = ports.url ? ` → ${ports.url}` : "";
231
+ showDetail(`${mark} ${resource.name}${urlPart} ${state.status}${state.reason ? ` (${state.reason})` : ""}`, 6);
232
+ }
185
233
  }
186
234
  }
187
235
  else if (discovery.resources.length > 0) {