@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
@@ -0,0 +1,64 @@
1
+ // Copyright (c) 2026 Flisk Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { loadTemplate } from "../generator/template-engine.js";
6
+ // The agent skills `flisk project` writes into a new project, so a coding agent opened there knows how to build and run
7
+ // the app with Flisk. Copies of .claude/skills/<name>/SKILL.md, shipped in cli/templates/agent-skills (a test keeps
8
+ // them identical).
9
+ export const PROJECT_AGENT_SKILLS = [
10
+ "flisk-build",
11
+ "flisk-recipe-starters",
12
+ "flisk-recipe-shop",
13
+ "flisk-recipe-saas",
14
+ "flisk-recipe-ai-agent",
15
+ "flisk-recipe-blog",
16
+ "flisk-doctor",
17
+ "flisk-up",
18
+ "flisk-resource",
19
+ "flisk-status",
20
+ "flisk-troubleshoot",
21
+ "flisk-agent-ops",
22
+ ];
23
+ // Claude Code reads .claude/skills; Codex, Cursor and other agents read .agents/skills.
24
+ export const AGENT_SKILL_DIRS = [".claude/skills", ".agents/skills"];
25
+ const DOCS_URL = "https://github.com/flisk-sh/flisk/blob/main/docs/";
26
+ export const PROJECT_AGENTS_MD = `# AGENTS.md
27
+
28
+ This project runs on [Flisk](https://github.com/flisk-sh/flisk). To build or run something here, follow the Flisk skills in \`.agents/skills\` (Claude Code: \`.claude/skills\`), starting with \`flisk-build\`.
29
+
30
+ - Add a service: \`flisk resource <name> --type <frontend|backend|worker|agent|mcp> --stack <stack> --yes\`
31
+ - Start it: \`flisk up <stack>\`, then check every URL from \`flisk networks\`
32
+ - When something fails: \`flisk doctor\`, then \`flisk logs -s <service>\`
33
+ - Do not hand-write Dockerfiles, Compose files or Tiltfiles, and do not edit \`.autogenerated/\`: change \`service.json\` instead.
34
+ `;
35
+ // In the repository the skills link to docs/ with relative paths; in a user's project those point nowhere.
36
+ export function rewriteSkillLinks(markdown) {
37
+ return markdown.replace(/\]\((?:\.\.\/)+docs\//g, `](${DOCS_URL}`);
38
+ }
39
+ /**
40
+ * Write the Flisk agent skills and an AGENTS.md into a project. Never overwrites: a file that already exists is left
41
+ * as it is, so a user's own edits survive `flisk project` running again. Returns the paths written, relative to the
42
+ * project root.
43
+ */
44
+ export function installAgentSkills(projectRoot) {
45
+ const written = [];
46
+ for (const name of PROJECT_AGENT_SKILLS) {
47
+ const contents = rewriteSkillLinks(loadTemplate(`agent-skills/${name}/SKILL.md`));
48
+ for (const dir of AGENT_SKILL_DIRS) {
49
+ const rel = path.join(dir, name, "SKILL.md");
50
+ const target = path.join(projectRoot, rel);
51
+ if (existsSync(target))
52
+ continue;
53
+ mkdirSync(path.dirname(target), { recursive: true });
54
+ writeFileSync(target, contents);
55
+ written.push(rel);
56
+ }
57
+ }
58
+ const agentsMd = path.join(projectRoot, "AGENTS.md");
59
+ if (!existsSync(agentsMd)) {
60
+ writeFileSync(agentsMd, PROJECT_AGENTS_MD);
61
+ written.push("AGENTS.md");
62
+ }
63
+ return written;
64
+ }
@@ -1,10 +1,13 @@
1
+ import { isNativeService } from "./native-service.js";
1
2
  import { isApiServiceType } from "./resource-kind.js";
2
3
  /**
3
4
  * Bun runs the generated JS services. A project without any has nothing for Bun to run, so neither `flisk doctor` nor
4
- * `flisk up` needs it. Doctor and the start-up preflight share this rule so they cannot disagree.
5
+ * `flisk up` needs it. A `command` service runs whatever its command names, so it never needs Bun on Flisk's behalf.
6
+ * Doctor and the start-up preflight share this rule so they cannot disagree.
5
7
  */
6
8
  export function projectNeedsBun(resources) {
7
- return resources.some((resource) => isApiServiceType(resource.config?.appType) ||
8
- resource.config?.appType === "frontend" ||
9
- resource.config?.appType === "worker");
9
+ return resources.some((resource) => !isNativeService(resource) &&
10
+ (isApiServiceType(resource.config?.appType) ||
11
+ resource.config?.appType === "frontend" ||
12
+ resource.config?.appType === "worker"));
10
13
  }
@@ -11,9 +11,19 @@ import { checkColima } from "./colima.js";
11
11
  import { PROJECT_JSON } from "./constants.js";
12
12
  import { checkNatsBroker } from "./doctor-wiring.js";
13
13
  import { getHostPortPlan } from "./host-port-config.js";
14
+ import { isNativeProject } from "./native-service.js";
14
15
  import { findProjectRoot } from "./paths.js";
15
16
  import { discoverResourcesFromRoot } from "./services.js";
16
17
  import { findOnPath } from "./which.js";
18
+ /** Checks a project of `command` services needs: no container runtime, no Tilt, no gateway or database ports. */
19
+ export const NATIVE_CHECK_IDS = ["node", "bun"];
20
+ function blockingCheckIds(scope) {
21
+ if (scope === "scaffold")
22
+ return SCAFFOLD_CHECK_IDS;
23
+ if (scope === "native")
24
+ return NATIVE_CHECK_IDS;
25
+ return MACHINE_CHECK_IDS;
26
+ }
17
27
  /** Checks that only the local runtime (starting a stack) needs. */
18
28
  export const RUNTIME_CHECK_IDS = ["docker", "compose", "tilt"];
19
29
  /** Checks that writing project configuration needs. */
@@ -71,11 +81,35 @@ export function usesPrisma(projectRoot) {
71
81
  return false;
72
82
  }
73
83
  }
84
+ async function hostPortsItem(root, scope) {
85
+ try {
86
+ // Scaffolding must not talk to Docker at all, so the port plan is planned
87
+ // from the host ports alone.
88
+ const plan = await getHostPortPlan(root, scope === "scaffold" ? { inspectDocker: false } : {});
89
+ return {
90
+ id: "ports",
91
+ ok: true,
92
+ message: `HTTP ${plan.ingressHttp}, HTTPS ${plan.ingressHttps}, Postgres ${plan.postgres}`,
93
+ };
94
+ }
95
+ catch (error) {
96
+ return {
97
+ id: "ports",
98
+ ok: false,
99
+ message: error instanceof Error ? error.message : String(error),
100
+ fix: "Set FLISK_HTTP_PORT, FLISK_HTTPS_PORT, or FLISK_POSTGRES_PORT to available host ports.",
101
+ };
102
+ }
103
+ }
74
104
  export async function runColdPreflight(opts = {}) {
75
105
  const cwd = opts.cwd ?? process.cwd();
76
- const scope = opts.scope ?? "runtime";
77
106
  const projectRoot = findProjectRoot(cwd);
78
107
  const inProject = Boolean(projectRoot);
108
+ const resources = projectRoot ? discoverResourcesFromRoot(projectRoot) : [];
109
+ // A project whose services all declare `command` runs them as local processes (#2231): Docker, Compose, Tilt, the
110
+ // gateway/database host ports and the container wiring checks do not apply, so they are not run or reported.
111
+ const isNative = isNativeProject(resources);
112
+ const scope = isNative ? "native" : (opts.scope ?? "runtime");
79
113
  const items = [];
80
114
  const nodeVersion = process.versions.node;
81
115
  items.push({
@@ -115,7 +149,7 @@ export async function runColdPreflight(opts = {}) {
115
149
  items.push(await safeCheck("tilt", checkTilt, "Tilt CLI not found"));
116
150
  }
117
151
  // Outside a project Bun stays required: `flisk project` is about to create services that need it.
118
- const bunNeeded = projectRoot ? projectNeedsBun(discoverResourcesFromRoot(projectRoot)) : true;
152
+ const bunNeeded = projectRoot ? projectNeedsBun(resources) : true;
119
153
  let bun = false;
120
154
  const bunPath = bunNeeded ? findOnPath("bun") : undefined;
121
155
  if (bunPath) {
@@ -141,24 +175,9 @@ export async function runColdPreflight(opts = {}) {
141
175
  fix: bun ? undefined : "curl -fsSL https://bun.sh/install | bash",
142
176
  }
143
177
  : { id: "bun", ok: true, message: "Bun not needed: no generated JS services" });
144
- try {
145
- // Scaffolding must not talk to Docker at all, so the port plan is planned
146
- // from the host ports alone.
147
- const plan = await getHostPortPlan(projectRoot ?? cwd, scope === "scaffold" ? { inspectDocker: false } : {});
148
- items.push({
149
- id: "ports",
150
- ok: true,
151
- message: `HTTP ${plan.ingressHttp}, HTTPS ${plan.ingressHttps}, Postgres ${plan.postgres}`,
152
- });
153
- }
154
- catch (error) {
155
- items.push({
156
- id: "ports",
157
- ok: false,
158
- message: error instanceof Error ? error.message : String(error),
159
- fix: "Set FLISK_HTTP_PORT, FLISK_HTTPS_PORT, or FLISK_POSTGRES_PORT to available host ports.",
160
- });
161
- }
178
+ // The host port plan places the gateway and Postgres, which only the container runtime starts.
179
+ if (scope !== "native")
180
+ items.push(await hostPortsItem(projectRoot ?? cwd, scope));
162
181
  // Project wiring (NATS, Prisma database) describes a running stack, so it is
163
182
  // only probed for the runtime scope.
164
183
  if (projectRoot && scope === "runtime") {
@@ -198,18 +217,20 @@ export async function runColdPreflight(opts = {}) {
198
217
  }
199
218
  }
200
219
  const failures = items.filter((check) => !check.ok).slice(0, 8);
201
- const blockingIds = scope === "scaffold" ? SCAFFOLD_CHECK_IDS : MACHINE_CHECK_IDS;
220
+ const blockingIds = blockingCheckIds(scope);
202
221
  const machineFailed = failures.some((check) => blockingIds.includes(check.id));
203
222
  const bunFailed = failures.some((check) => check.id === "bun");
204
223
  const header = failures.length
205
224
  ? machineFailed
206
- ? scope === "scaffold"
207
- ? bunFailed
208
- ? `Project setup blocked. Bun ${BUN_FLOOR_LABEL} must be installed on PATH.\nFix the machine checks below.`
209
- : "Project setup blocked. Fix the machine checks below."
210
- : bunFailed
211
- ? `Cold start blocked. Bun ${BUN_FLOOR_LABEL} must be installed on PATH.\nPrisma/NATS configuration is project-specific and generated after \`flisk project\`. Fix the machine checks below.`
212
- : "Cold start blocked. Bun/Prisma/NATS are not the first failure.\nThey are generated after `flisk project`. Fix the machine checks below."
225
+ ? scope === "native"
226
+ ? "Cannot start the local processes. Fix the machine checks below."
227
+ : scope === "scaffold"
228
+ ? bunFailed
229
+ ? `Project setup blocked. Bun ${BUN_FLOOR_LABEL} must be installed on PATH.\nFix the machine checks below.`
230
+ : "Project setup blocked. Fix the machine checks below."
231
+ : bunFailed
232
+ ? `Cold start blocked. Bun ${BUN_FLOOR_LABEL} must be installed on PATH.\nPrisma/NATS configuration is project-specific and generated after \`flisk project\`. Fix the machine checks below.`
233
+ : "Cold start blocked. Bun/Prisma/NATS are not the first failure.\nThey are generated after `flisk project`. Fix the machine checks below."
213
234
  : "Machine is ready. Project wiring is not."
214
235
  : "";
215
236
  const footer = failures.length
@@ -217,7 +238,7 @@ export async function runColdPreflight(opts = {}) {
217
238
  : inProject
218
239
  ? "Environment ready for Flisk"
219
240
  : `This machine is ready for Flisk.\n${readyNextStep()}`;
220
- return { ok: failures.length === 0, inProject, items: failures, header, footer };
241
+ return { ok: failures.length === 0, inProject, scope, items: failures, header, footer };
221
242
  }
222
243
  /** Formats only failed preflight checks and the applicable readiness message. */
223
244
  export function formatColdPreflight(result, options = {}) {
@@ -233,12 +254,14 @@ export function formatColdPreflight(result, options = {}) {
233
254
  }
234
255
  /** Exits before machine-dependent command work when any machine check fails. */
235
256
  export async function assertMachineReadyOrExit(opts = {}) {
236
- const scope = opts.scope ?? "runtime";
237
- const result = await runColdPreflight({ scope });
238
- const blockingIds = scope === "scaffold" ? SCAFFOLD_CHECK_IDS : MACHINE_CHECK_IDS;
257
+ const result = await runColdPreflight({ scope: opts.scope });
258
+ const blockingIds = blockingCheckIds(result.scope);
239
259
  const machineFailure = result.items.some((check) => blockingIds.includes(check.id));
240
260
  if (machineFailure) {
241
261
  console.error(formatColdPreflight(result));
262
+ // What to run once the machine checks pass; the caller knows its own next step.
263
+ for (const hint of opts.hints ?? [])
264
+ console.error(`→ ${hint}`);
242
265
  process.exit(1);
243
266
  }
244
267
  }
@@ -0,0 +1,93 @@
1
+ import { CONTAINER_RUNTIME_CHECK } from "./constants.js";
2
+ import { DISK_SPACE_CHECK } from "./doctor-host.js";
3
+ const POSIX = (platform) => platform === "darwin" || platform === "linux";
4
+ function conflictingPort(result) {
5
+ const match = /^\s*(\d{2,5}) \(/m.exec(result.message);
6
+ return match ? Number(match[1]) : null;
7
+ }
8
+ function inspectPort(platform, port) {
9
+ if (port === null)
10
+ return null;
11
+ if (platform === "darwin")
12
+ return `lsof -nP -iTCP:${port} -sTCP:LISTEN`;
13
+ if (platform === "linux")
14
+ return `ss -ltnp 'sport = :${port}'`;
15
+ return null;
16
+ }
17
+ const RULES = {
18
+ [CONTAINER_RUNTIME_CHECK]: {
19
+ tier: 0,
20
+ command: (platform) => platform === "darwin"
21
+ ? "open -a Docker"
22
+ : platform === "linux" && !process.env.WSL_DISTRO_NAME
23
+ ? "sudo systemctl start docker"
24
+ : null,
25
+ },
26
+ Colima: { tier: 1, command: (platform) => (POSIX(platform) ? "colima start" : null) },
27
+ "Host Ports": {
28
+ tier: 2,
29
+ command: (platform, result) => inspectPort(platform, conflictingPort(result)),
30
+ },
31
+ "Ingress Ports": { tier: 2, command: () => null },
32
+ "Tilt CLI": {
33
+ tier: 3,
34
+ command: (platform) => POSIX(platform)
35
+ ? "curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash"
36
+ : null,
37
+ },
38
+ Bun: {
39
+ tier: 3,
40
+ command: (platform) => (POSIX(platform) ? "curl -fsSL https://bun.sh/install | bash" : null),
41
+ },
42
+ "Docker Network Pool": { tier: 4, command: () => "flisk down --prune-networks" },
43
+ "Docker networks": { tier: 4, command: () => "flisk down --prune-networks" },
44
+ [DISK_SPACE_CHECK]: { tier: 5, command: () => "docker system prune -af" },
45
+ "Flisk installs": { tier: 6, command: () => "flisk upgrade" },
46
+ "Flisk version": { tier: 6, command: () => "flisk upgrade" },
47
+ "Master Configs": { tier: 7, command: () => "flisk project" },
48
+ "Generated Runtime Assets": { tier: 7, command: () => "flisk config regenerate" },
49
+ "Starlark Load Exports": { tier: 7, command: () => "flisk config regenerate" },
50
+ };
51
+ /** Project-level findings come after every machine-level one that has no rule. */
52
+ const UNRANKED_TIER = 99;
53
+ export function checkId(name) {
54
+ return name
55
+ .toLowerCase()
56
+ .replace(/[^a-z0-9]+/g, "-")
57
+ .replace(/^-+|-+$/g, "");
58
+ }
59
+ /**
60
+ * Failing checks ordered by how much they block, each with one runnable command. Hard failures come before
61
+ * warnings (a low-disk warning never outranks a stopped daemon), and a port 5432 conflict leads the port tier.
62
+ * Checks without a command that is valid on this platform are left out; their prose fix is still printed.
63
+ */
64
+ export function rankDoctorFixes(results, platform = process.platform) {
65
+ const candidates = results
66
+ .filter((result) => !result.didPass && !result.isSkipped)
67
+ .map((result, index) => {
68
+ const rule = RULES[result.name];
69
+ return {
70
+ result,
71
+ index,
72
+ tier: rule?.tier ?? UNRANKED_TIER,
73
+ command: rule?.command(platform, result) ?? null,
74
+ };
75
+ })
76
+ .filter((entry) => entry.command !== null);
77
+ const weight = (entry) => (entry.result.isWarning ? 1000 : 0) +
78
+ entry.tier * 10 +
79
+ (entry.result.name === "Host Ports" && conflictingPort(entry.result) === 5432 ? 0 : 1);
80
+ candidates.sort((left, right) => weight(left) - weight(right) || left.index - right.index);
81
+ return candidates.map((entry, position) => ({
82
+ rank: position + 1,
83
+ checkId: checkId(entry.result.name),
84
+ check: entry.result.name,
85
+ command: entry.command,
86
+ }));
87
+ }
88
+ /** The human "Fix first" section, or an empty list when there is nothing to run. */
89
+ export function formatFixFirst(fixes) {
90
+ if (fixes.length === 0)
91
+ return [];
92
+ return fixes.flatMap((fix) => [` ${fix.rank}. ${fix.check}`, ` ${fix.command}`]);
93
+ }
@@ -0,0 +1,96 @@
1
+ import { formatCount } from "./formatting.js";
2
+ import { findMissingPrograms, isPortListening, isProcessAlive, localUrl, planNativeRun, readNativeRunState, } from "./native-runtime.js";
3
+ import { isNativeService } from "./native-service.js";
4
+ import { findOnPath } from "./which.js";
5
+ /**
6
+ * Doctor checks for a project whose services all declare `command` (#2231). Such a project runs as local processes, so doctor
7
+ * checks what those processes need (their programs, their dependsOn, their ports) instead of Docker, Compose and Tilt.
8
+ */
9
+ /** Says which runtime doctor checked, so a missing Docker check reads as intended and not as forgotten. */
10
+ export function checkNativeRuntime(resources) {
11
+ return {
12
+ name: "Runtime",
13
+ didPass: true,
14
+ message: `Local processes: ${formatCount(resources.length, "service")} declare a command, so Docker, Compose and Tilt are not needed`,
15
+ };
16
+ }
17
+ /** Each service's command starts a program that is installed. */
18
+ export function checkNativeCommands(resources, find = findOnPath) {
19
+ const missing = findMissingPrograms(planNativeRun(resources.filter(isNativeService)).services, find);
20
+ if (missing.length === 0) {
21
+ return {
22
+ name: "Service commands",
23
+ didPass: true,
24
+ message: "Every service command starts a program that is installed",
25
+ };
26
+ }
27
+ return {
28
+ name: "Service commands",
29
+ didPass: false,
30
+ message: `${formatCount(missing.length, "service")} run a program that is not on PATH:\n ${missing.map(({ name, program }) => `${name}: ${program}`).join("\n ")}`,
31
+ fix: 'Install the program, or change "command" in the service.json',
32
+ };
33
+ }
34
+ /** A dependsOn name must be another service of the project: local processes cannot start a database or gateway for it. */
35
+ export function checkNativeDependsOn(resources) {
36
+ const names = new Set(resources.map((resource) => resource.name));
37
+ const problems = resources.flatMap((resource) => {
38
+ const deps = resource.config?.dependsOn;
39
+ return (Array.isArray(deps) ? deps : [])
40
+ .filter((dep) => !names.has(dep))
41
+ .map((dep) => `${resource.name} dependsOn "${dep}", which is not a service of this project`);
42
+ });
43
+ if (problems.length === 0) {
44
+ return {
45
+ name: "Service dependencies",
46
+ didPass: true,
47
+ message: "Every dependsOn names a service of this project",
48
+ };
49
+ }
50
+ return {
51
+ name: "Service dependencies",
52
+ didPass: false,
53
+ message: problems.join("\n "),
54
+ fix: "Add the missing service with a command, or remove it from dependsOn. A database or the gateway still needs the Docker path.",
55
+ };
56
+ }
57
+ /**
58
+ * When a native run is up, each service answers on its health URL (or accepts connections on its port). Skipped when nothing
59
+ * runs: doctor is also used before the first `flisk up`.
60
+ */
61
+ export async function checkNativeServiceHealth(projectRoot, resources, timeoutMs) {
62
+ const state = readNativeRunState(projectRoot);
63
+ if (!state || !isProcessAlive(state.pid)) {
64
+ return {
65
+ name: "Service Health",
66
+ didPass: true,
67
+ isSkipped: true,
68
+ message: "Not running - start it with flisk up",
69
+ };
70
+ }
71
+ const failing = [];
72
+ for (const plan of planNativeRun(resources.filter(isNativeService)).services) {
73
+ if (plan.port === undefined)
74
+ continue;
75
+ const answered = plan.healthUrl
76
+ ? await fetch(plan.healthUrl, { signal: AbortSignal.timeout(timeoutMs) })
77
+ .then((response) => response.status < 400)
78
+ .catch(() => false)
79
+ : await isPortListening(plan.port, timeoutMs);
80
+ if (!answered)
81
+ failing.push(`${plan.name}: no answer from ${plan.healthUrl ?? localUrl(plan.port)}`);
82
+ }
83
+ if (failing.length === 0) {
84
+ return {
85
+ name: "Service Health",
86
+ didPass: true,
87
+ message: "Every running service answers",
88
+ };
89
+ }
90
+ return {
91
+ name: "Service Health",
92
+ didPass: false,
93
+ message: failing.join("\n "),
94
+ fix: "Read the service output in .flisk/native/logs/<service>.log, or the flisk up terminal",
95
+ };
96
+ }
@@ -1,4 +1,5 @@
1
1
  import { CONTAINER_RUNTIME_CHECK } from "./constants.js";
2
+ import { rankDoctorFixes } from "./doctor-fix-first.js";
2
3
  import { SCHEMA_VERSION } from "./machine-output.js";
3
4
  /** Wait for concurrent probes to finish even if one throws, so they can release resources. */
4
5
  export async function collectDoctorChecks(machineChecks, projectChecks) {
@@ -28,6 +29,7 @@ function containerRuntimeReachable(checks) {
28
29
  }
29
30
  /** Produce the same readiness decision for human and machine consumers. */
30
31
  export function createDoctorReport(checks, inProject, errors = [], portPlan, host) {
32
+ const fixFirst = rankDoctorFixes(checks);
31
33
  return {
32
34
  schemaVersion: SCHEMA_VERSION,
33
35
  data: {
@@ -35,6 +37,7 @@ export function createDoctorReport(checks, inProject, errors = [], portPlan, hos
35
37
  checks.every((check) => check.didPass || check.isSkipped || check.isWarning),
36
38
  inProject,
37
39
  checks,
40
+ ...(fixFirst.length > 0 ? { fixFirst } : {}),
38
41
  ...(host
39
42
  ? {
40
43
  host: {
@@ -0,0 +1,41 @@
1
+ // Copyright (c) 2026 Flisk Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ import { existsSync, readdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { SERVICE_JSON } from "./constants.js";
6
+ import { isNativeProject } from "./native-service.js";
7
+ import { findProjectRoot } from "./paths.js";
8
+ import { discoverResourcesWithProblems } from "./services.js";
9
+ /** True when `dir` or a directory up to two levels below it has a service.json; a cheap test before a full discovery scan. */
10
+ export function hasNearbyServiceJson(dir, depth = 0) {
11
+ if (existsSync(join(dir, SERVICE_JSON)))
12
+ return true;
13
+ if (depth >= 2)
14
+ return false;
15
+ try {
16
+ return readdirSync(dir, { withFileTypes: true }).some((entry) => entry.isDirectory() &&
17
+ !entry.name.startsWith(".") &&
18
+ entry.name !== "node_modules" &&
19
+ hasNearbyServiceJson(join(dir, entry.name), depth + 1));
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ /**
26
+ * The project a command for local processes works on: the Flisk project around `cwd`, or, before the first `flisk up` wrote
27
+ * .flisk/project.json, `cwd` itself when every service.json near it declares `command` (the directory `flisk up` adopts).
28
+ * Returns its services only when they all declare `command`; null otherwise, including outside any project.
29
+ */
30
+ export function findNativeProject(cwd = process.cwd()) {
31
+ const projectRoot = findProjectRoot(cwd) ?? (hasNearbyServiceJson(cwd) ? cwd : null);
32
+ if (!projectRoot)
33
+ return null;
34
+ try {
35
+ const { resources } = discoverResourcesWithProblems(projectRoot);
36
+ return isNativeProject(resources) ? { projectRoot, resources } : null;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }