@flisk-sh/flisk 1.4.1 → 1.4.5

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 (83) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +421 -92
  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 +16 -15
  9. package/cli/dist/commands/logs.js +1 -0
  10. package/cli/dist/commands/project.js +1 -0
  11. package/cli/dist/commands/resource.js +39 -11
  12. package/cli/dist/commands/status.js +18 -9
  13. package/cli/dist/commands/up.js +102 -17
  14. package/cli/dist/frontend-frameworks/next.js +197 -0
  15. package/cli/dist/frontend-frameworks/react.js +81 -12
  16. package/cli/dist/frontend-frameworks/registry.js +5 -4
  17. package/cli/dist/frontend-frameworks/vue.js +82 -12
  18. package/cli/dist/generator/resource-templates.js +2 -4
  19. package/cli/dist/generator/template-engine.js +5 -3
  20. package/cli/dist/utils/cold-preflight.js +3 -2
  21. package/cli/dist/utils/config-explain.js +197 -0
  22. package/cli/dist/utils/demo-reset.js +22 -0
  23. package/cli/dist/utils/discovery-paths.js +7 -1
  24. package/cli/dist/utils/host-port-plan.js +8 -3
  25. package/cli/dist/utils/port-conflict-fix.js +16 -14
  26. package/cli/dist/utils/port-occupant.js +35 -0
  27. package/cli/dist/utils/project-features.js +7 -16
  28. package/cli/dist/utils/project-templates.js +8 -8
  29. package/cli/dist/utils/resource-features.js +5 -5
  30. package/cli/dist/utils/service-manifest.js +1 -0
  31. package/cli/dist/utils/service-urls.js +11 -0
  32. package/cli/dist/utils/startup-report.js +4 -3
  33. package/cli/dist/utils/tar.js +33 -28
  34. package/cli/dist/utils/tilt-process.js +4 -3
  35. package/cli/dist/utils/windows-doctor.js +18 -15
  36. package/cli/dist-bundle/{chunk-UJXR6MZ2.js → chunk-4J2FLFUA.js} +14 -7
  37. package/cli/dist-bundle/{chunk-SRN64OPO.js → chunk-BMHPPX2Q.js} +10 -10
  38. package/cli/dist-bundle/chunk-BP2M4OJ4.js +2 -0
  39. package/cli/dist-bundle/{chunk-TZNXL3IO.js → chunk-CGTWXWBU.js} +1 -1
  40. package/cli/dist-bundle/chunk-GPUJ55MI.js +3 -0
  41. package/cli/dist-bundle/{chunk-KJOCVAMQ.js → chunk-LUUJ4JB7.js} +1 -1
  42. package/cli/dist-bundle/chunk-PEZQEZVV.js +17 -0
  43. package/cli/dist-bundle/{chunk-7HR5PVOD.js → chunk-SFOFTVYM.js} +5 -5
  44. package/cli/dist-bundle/{chunk-RX6O3PG4.js → chunk-TIYTGN5R.js} +1 -1
  45. package/cli/dist-bundle/{chunk-BB5BZ37Y.js → chunk-U42XZWXB.js} +1 -1
  46. package/cli/dist-bundle/chunk-ULMQVD7J.js +122 -0
  47. package/cli/dist-bundle/chunk-V36PT2HF.js +24 -0
  48. package/cli/dist-bundle/{chunk-7IXBBU27.js → chunk-VMVTECCK.js} +1 -1
  49. package/cli/dist-bundle/{chunk-M2R464RM.js → chunk-Z5QEE4SW.js} +1 -1
  50. package/cli/dist-bundle/cli.js +607 -375
  51. package/cli/dist-bundle/cold-preflight-3BNDOLGS.js +2 -0
  52. package/cli/dist-bundle/{demo-stage-HIHO4VV3.js → demo-stage-G4W23BXW.js} +1 -1
  53. package/cli/dist-bundle/{extension-fetch-VEFSCG4B.js → extension-fetch-KIPYDIRM.js} +1 -1
  54. package/cli/dist-bundle/{open-W3QX35R6.js → open-HQFT6WDC.js} +1 -1
  55. package/cli/dist-bundle/project-JZO2CPZZ.js +2 -0
  56. package/cli/dist-bundle/{project-templates-EYSQG2EO.js → project-templates-IMJ3OX5I.js} +1 -1
  57. package/cli/dist-bundle/{services-RGTQ2LXC.js → services-GLFKZBBZ.js} +1 -1
  58. package/cli/dist-bundle/{tilt-port-owner-6DO7OJRR.js → tilt-port-owner-PJN4MPYE.js} +1 -1
  59. package/cli/package.json +1 -1
  60. package/package.json +2 -1
  61. package/runtime/engine/topologies/platform/docker/compose/compose.star +40 -4
  62. package/runtime/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +6 -0
  63. package/runtime/engine/topologies/platform/docker/dockerfile/next_dockerfile.star +83 -0
  64. package/runtime/engine/topologies/platform/docker/index.star +4 -0
  65. package/runtime/engine/topologies/platform/docker/layers/prisma/filesystem_setup.star +7 -0
  66. package/runtime/engine/topologies/platform/docker/networking/traefik.star +85 -46
  67. package/runtime/engine/topologies/tilt/common/utils_output.star +1 -0
  68. package/runtime/engine/topologies/tilt/generators/validators.star +1 -1
  69. package/runtime/engine/topologies/tilt/generators/vite/frontend.star +2 -2
  70. package/runtime/engine/topologies/tilt/generators/vite/generators.star +3 -0
  71. package/runtime/engine/topologies/tilt/manifest/constants.star +3 -0
  72. package/runtime/engine/topologies/tilt/manifest/parser.star +9 -3
  73. package/runtime/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +8 -3
  74. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/dependencies/resolver.star +50 -24
  75. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/env.star +11 -2
  76. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +23 -9
  77. package/cli/dist-bundle/chunk-DUNCELUK.js +0 -24
  78. package/cli/dist-bundle/chunk-K6SOCO6V.js +0 -3
  79. package/cli/dist-bundle/chunk-L6U555CJ.js +0 -61
  80. package/cli/dist-bundle/chunk-VRESY4NR.js +0 -17
  81. package/cli/dist-bundle/chunk-YL56J2WX.js +0 -2
  82. package/cli/dist-bundle/cold-preflight-BBDDHWRV.js +0 -2
  83. package/cli/dist-bundle/project-FW5LL2KM.js +0 -2
@@ -24,9 +24,14 @@ app.get('/health/db', async (c) => {
24
24
  `
25
25
  : "";
26
26
  return `import { Hono } from 'hono';
27
+ import { cors } from 'hono/cors';
27
28
  ${drizzleImports}
28
29
  const app = new Hono();
29
30
 
31
+ // The frontend runs on its own *.localhost origin (app.<project>.localhost), so the browser needs CORS to call this API.
32
+ const LOCAL_FRONTEND_ORIGIN = /^https?:\\/\\/([a-z0-9-]+\\.)+localhost(:\\d+)?$/;
33
+ app.use('*', cors({ origin: (origin) => (LOCAL_FRONTEND_ORIGIN.test(origin) ? origin : undefined) }));
34
+
30
35
  // Health check endpoint (required by TILT_RESOURCE_DEFAULTS.star)
31
36
  app.get('/health', (c) => {
32
37
  return c.json({ status: 'ok', service: '${name}' });
@@ -34,7 +34,9 @@ import (
34
34
  func writeJSON(w http.ResponseWriter, status int, body any) {
35
35
  w.Header().Set("Content-Type", "application/json")
36
36
  w.WriteHeader(status)
37
- _ = json.NewEncoder(w).Encode(body)
37
+ if err := json.NewEncoder(w).Encode(body); err != nil {
38
+ log.Printf("failed to encode JSON response: %v", err)
39
+ }
38
40
  }
39
41
 
40
42
  func newMux() *http.ServeMux {
package/cli/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ import { showHelp } from "./commands/help.js";
12
12
  import { createProgram } from "./program.js";
13
13
  import { ensureGlobalInstall } from "./utils/cli-command.js";
14
14
  import { formatColdPreflight, runColdPreflight } from "./utils/cold-preflight.js";
15
+ import { stopDemoStack } from "./utils/demo-reset.js";
15
16
  import { unknownHelpTarget } from "./utils/help-command.js";
16
17
  import { bareTemplateArg } from "./utils/project-templates.js";
17
18
  const program = createProgram();
@@ -68,6 +69,7 @@ if (templateArg) {
68
69
  }
69
70
  }
70
71
  const destination = isAbsolute(targetName) ? targetName : resolve(cwd(), targetName);
72
+ console.log(chalk.gray(`📁 Demo folder: ${destination}`));
71
73
  // Reusing an existing checkout is what keeps a second take free: the clone and the
72
74
  // dependency install are skipped, so the recording starts on the interesting part.
73
75
  let reused = false;
@@ -85,6 +87,13 @@ if (templateArg) {
85
87
  await openUrl(running.url);
86
88
  process.exit(0);
87
89
  }
90
+ // Take the old stack down before its folder goes, so a Traefik left behind does not keep port 80.
91
+ try {
92
+ stopDemoStack(destination);
93
+ }
94
+ catch (error) {
95
+ console.log(chalk.yellow(`Could not stop the previous demo stack: ${error.message}`));
96
+ }
88
97
  if (keep) {
89
98
  reused = true;
90
99
  }
@@ -463,7 +463,14 @@ _flisk_resources() {
463
463
  ${subCommandLists}
464
464
 
465
465
  ${typeValueFunctions}
466
- compdef _flisk flisk
466
+
467
+ # Autoloaded from fpath, this file is the body of _flisk, so run it now to complete the first TAB.
468
+ # Sourced (\`source <(flisk completion zsh)\`), register it instead.
469
+ if [ "$funcstack[1]" = "_flisk" ]; then
470
+ _flisk "$@"
471
+ else
472
+ compdef _flisk flisk
473
+ fi
467
474
  `;
468
475
  }
469
476
  /** The `_arguments` spec of one flag, with the values it completes when it has them. */
@@ -9,7 +9,8 @@ import { hasVerdaccioLicense } from "../generator/extension-fetch.js";
9
9
  import { generateMasterConfigs, readProjectConfig, TemplateEngine, verifyMasterConfigs, } from "../generator/template-engine.js";
10
10
  import { isMasterConfigFileName } from "../types/index.js";
11
11
  import { assertValid, handleDryRun } from "../utils/command-helpers.js";
12
- import { MASTER_CONFIG_FILES, PROJECT_JSON } from "../utils/constants.js";
12
+ import { explainConfigFile } from "../utils/config-explain.js";
13
+ import { MASTER_CONFIG_FILES, PROJECT_JSON, RESOURCE_DEFAULTS_FILE, TECH_STACK_FILE, } from "../utils/constants.js";
13
14
  import { clearDiscoveryCache } from "../utils/discovery-context.js";
14
15
  import { checkPrismaConsistency } from "../utils/doctor-wiring.js";
15
16
  import { errorFactories, requireProjectRoot, runCommand, showError } from "../utils/errors.js";
@@ -20,6 +21,12 @@ import { SERVICE_MANIFEST_SCHEMA_VERSION, validateServiceManifest, } from "../ut
20
21
  import { discoverResourcesWithProblems, discoverServiceManifestPaths, warnAboutSkippedFiles, } from "../utils/services.js";
21
22
  import { evaluateSharedPlatformPostgres } from "../utils/shared-platform-postgres.js";
22
23
  import { validateOptionalInfraService } from "../utils/validation.js";
24
+ /** Human wording for `flisk config explain`, one line per drift outcome. */
25
+ const DRIFT_LABELS = {
26
+ "blocks-up": "yes, until the edit is removed or --ignore-drift is passed",
27
+ none: "no",
28
+ unknown: "unknown",
29
+ };
23
30
  export const configCommand = new Command("config")
24
31
  .description("Manage project configuration and regenerate master files")
25
32
  .addCommand(new Command("regenerate")
@@ -36,72 +43,104 @@ export const configCommand = new Command("config")
36
43
  const newFiles = engine.generateAll(projectConfig);
37
44
  const outputDir = join(projectRoot, ".flisk", ".flisk-out");
38
45
  const filesToCheck = MASTER_CONFIG_FILES;
46
+ const fileGroups = [
47
+ {
48
+ title: "📋 Core orchestration",
49
+ files: [TECH_STACK_FILE, RESOURCE_DEFAULTS_FILE],
50
+ description: "Tilt tech stack and resource defaults; drives all service generation",
51
+ },
52
+ {
53
+ title: "📦 Project spec",
54
+ files: ["spec.master"],
55
+ description: "Compiled project specification; used by Tilt for service discovery",
56
+ },
57
+ ];
39
58
  let hasChanges = false;
40
- for (const filename of filesToCheck) {
41
- // Runtime validation with type guard eliminates type assertion
42
- if (!isMasterConfigFileName(filename)) {
43
- continue; // Skip unknown files (shouldn't happen with const array)
44
- }
45
- const newContent = newFiles[filename];
46
- const filePath = join(outputDir, filename);
47
- if (!existsSync(filePath)) {
48
- console.log(chalk.yellow(`📁 ${filename}`));
49
- console.log(chalk.gray(" Status: NEW (file does not exist)\n"));
50
- hasChanges = true;
51
- continue;
52
- }
53
- const currentContent = readFileSync(filePath, "utf-8");
54
- if (currentContent === newContent) {
55
- console.log(chalk.green(`✓ ${filename}`));
56
- console.log(chalk.gray(" Status: No changes\n"));
57
- }
58
- else {
59
- console.log(chalk.yellow(`📝 ${filename}`));
60
- console.log(chalk.gray(" Status: MODIFIED"));
61
- const currentLines = currentContent.split("\n").length;
62
- const newLines = newContent.split("\n").length;
63
- const lineDiff = newLines - currentLines;
64
- if (lineDiff > 0) {
65
- console.log(chalk.gray(` Lines: ${currentLines} → ${newLines} (+${lineDiff})`));
59
+ for (const group of fileGroups) {
60
+ let groupHasChanges = false;
61
+ const groupChanges = [];
62
+ for (const filename of group.files) {
63
+ if (!filesToCheck.includes(filename))
64
+ continue;
65
+ if (!isMasterConfigFileName(filename))
66
+ continue;
67
+ const newContent = newFiles[filename];
68
+ const filePath = join(outputDir, filename);
69
+ if (!existsSync(filePath)) {
70
+ groupHasChanges = true;
71
+ groupChanges.push({ filename, isNew: true, lineDiff: 0, diffLines: [] });
72
+ continue;
66
73
  }
67
- else if (lineDiff < 0) {
68
- console.log(chalk.gray(` Lines: ${currentLines} → ${newLines} (${lineDiff})`));
69
- }
70
- else {
71
- console.log(chalk.gray(` Lines: ${currentLines} (content changed)`));
72
- }
73
- const currentLinesArr = currentContent.split("\n");
74
- const newLinesArr = newContent.split("\n");
75
- let firstDiffLine = -1;
76
- for (let i = 0; i < Math.max(currentLinesArr.length, newLinesArr.length); i++) {
77
- if (currentLinesArr[i] !== newLinesArr[i]) {
78
- firstDiffLine = i;
79
- break;
74
+ const currentContent = readFileSync(filePath, "utf-8");
75
+ if (currentContent !== newContent) {
76
+ groupHasChanges = true;
77
+ const currentLines = currentContent.split("\n").length;
78
+ const newLines = newContent.split("\n").length;
79
+ const lineDiff = newLines - currentLines;
80
+ const currentLinesArr = currentContent.split("\n");
81
+ const newLinesArr = newContent.split("\n");
82
+ let firstDiffLine = -1;
83
+ for (let i = 0; i < Math.max(currentLinesArr.length, newLinesArr.length); i++) {
84
+ if (currentLinesArr[i] !== newLinesArr[i]) {
85
+ firstDiffLine = i;
86
+ break;
87
+ }
80
88
  }
81
- }
82
- if (firstDiffLine >= 0) {
83
- console.log(chalk.gray(` First change around line ${firstDiffLine + 1}:`));
84
- const contextStart = Math.max(0, firstDiffLine - 1);
85
- const contextEnd = Math.min(currentLinesArr.length, firstDiffLine + 2);
86
- for (let i = contextStart; i < contextEnd; i++) {
87
- const line = currentLinesArr[i];
88
- const newLine = newLinesArr[i];
89
- if (line !== newLine) {
90
- if (line !== undefined) {
91
- console.log(chalk.red(` - ${line.substring(0, 60)}${line.length > 60 ? "..." : ""}`));
89
+ const diffLines = [];
90
+ if (firstDiffLine >= 0) {
91
+ const contextStart = Math.max(0, firstDiffLine - 1);
92
+ const contextEnd = Math.min(currentLinesArr.length, firstDiffLine + 2);
93
+ for (let i = contextStart; i < contextEnd; i++) {
94
+ const line = currentLinesArr[i];
95
+ const newLine = newLinesArr[i];
96
+ if (line !== newLine) {
97
+ if (line !== undefined) {
98
+ diffLines.push(chalk.red(` - ${line.substring(0, 60)}${line.length > 60 ? "..." : ""}`));
99
+ }
100
+ if (newLine !== undefined) {
101
+ diffLines.push(chalk.green(` + ${newLine.substring(0, 60)}${newLine.length > 60 ? "..." : ""}`));
102
+ }
92
103
  }
93
- if (newLine !== undefined) {
94
- console.log(chalk.green(` + ${newLine.substring(0, 60)}${newLine.length > 60 ? "..." : ""}`));
104
+ else {
105
+ diffLines.push(chalk.gray(` ${line.substring(0, 60)}${line.length > 60 ? "..." : ""}`));
95
106
  }
96
107
  }
108
+ }
109
+ groupChanges.push({ filename, isNew: false, lineDiff, diffLines });
110
+ }
111
+ }
112
+ if (groupHasChanges) {
113
+ console.log(chalk.blue(`\n${group.title}`));
114
+ console.log(chalk.gray(` ${group.description}\n`));
115
+ for (const change of groupChanges) {
116
+ if (change.isNew) {
117
+ console.log(chalk.yellow(` 📁 ${change.filename}`));
118
+ console.log(chalk.gray(" Status: NEW (file does not exist)\n"));
119
+ }
120
+ else {
121
+ console.log(chalk.yellow(` 📝 ${change.filename}`));
122
+ console.log(chalk.gray(" Status: MODIFIED"));
123
+ if (change.lineDiff > 0) {
124
+ console.log(chalk.gray(` Lines: +${change.lineDiff}`));
125
+ }
126
+ else if (change.lineDiff < 0) {
127
+ console.log(chalk.gray(` Lines: ${change.lineDiff}`));
128
+ }
97
129
  else {
98
- console.log(chalk.gray(` ${line.substring(0, 60)}${line.length > 60 ? "..." : ""}`));
130
+ console.log(chalk.gray(" Lines: content changed"));
99
131
  }
132
+ for (const diffLine of change.diffLines) {
133
+ console.log(diffLine);
134
+ }
135
+ console.log("");
100
136
  }
101
137
  }
102
- console.log("");
103
138
  hasChanges = true;
104
139
  }
140
+ else {
141
+ console.log(chalk.green(`\n✅ ${group.title} — all up to date`));
142
+ console.log(chalk.gray(` ${group.description}\n`));
143
+ }
105
144
  }
106
145
  if (hasChanges) {
107
146
  console.log(chalk.blue("💡 Run without --dry-run to apply these changes.\n"));
@@ -268,6 +307,42 @@ export const configCommand = new Command("config")
268
307
  return;
269
308
  }
270
309
  await runCommand(action);
310
+ }))
311
+ .addCommand(new Command("explain")
312
+ .description("Explain whether a project file is Flisk-generated and what editing it does on flisk up")
313
+ .argument("<file>", "Project file to explain, relative to the current directory or absolute")
314
+ .option("--json", "Emit a machine-readable explanation")
315
+ .action(async (file, options) => {
316
+ const action = async () => {
317
+ const projectRoot = options.json ? findProjectRoot() : requireProjectRoot();
318
+ if (!projectRoot)
319
+ throw errorFactories.notInProject();
320
+ const explanation = explainConfigFile(projectRoot, file);
321
+ if (options.json) {
322
+ console.log(JSON.stringify(createMachineEnvelope(explanation)));
323
+ return;
324
+ }
325
+ console.log(chalk.blue(`${explanation.file}\n`));
326
+ console.log(` Ownership: ${explanation.ownership}`);
327
+ console.log(` ${explanation.reason}`);
328
+ if (explanation.source)
329
+ console.log(` Source: ${explanation.source}`);
330
+ console.log(` Edited now: ${explanation.isEditedNow ? "yes" : "no"}`);
331
+ console.log(` Editing it blocks flisk up: ${DRIFT_LABELS[explanation.driftOnUp]}`);
332
+ console.log(chalk.blue("\nNext:"));
333
+ for (const step of explanation.next)
334
+ console.log(chalk.gray(` - ${step}`));
335
+ };
336
+ if (options.json) {
337
+ try {
338
+ await action();
339
+ }
340
+ catch (error) {
341
+ writeMachineError(error);
342
+ }
343
+ return;
344
+ }
345
+ await runCommand(action);
271
346
  }))
272
347
  .addCommand(new Command("edit").description("Open .flisk/project.json in your $EDITOR").action(async () => {
273
348
  await runCommand(async () => {
@@ -291,7 +291,8 @@ export async function checkColima() {
291
291
  name: "Colima",
292
292
  didPass: false,
293
293
  message: status.problem ?? "The Colima VM is not running",
294
- fix: status.fix,
294
+ fix: status.fix ??
295
+ "Start Colima: colima start\n Verify: colima status\n Docs: https://github.com/abiosoft/colima",
295
296
  };
296
297
  }
297
298
  export async function checkDockerRuntime() {
@@ -309,7 +310,7 @@ export async function checkDockerRuntime() {
309
310
  name: CONTAINER_RUNTIME_CHECK,
310
311
  didPass: false,
311
312
  message: "Docker is not running",
312
- fix: DOCTOR_FIXES.dockerDaemonDown,
313
+ fix: "Start Docker Desktop, enable WSL2 integration, then retry: flisk doctor\n Verify: docker ps\n Docs: https://docs.docker.com/desktop/wsl/",
313
314
  };
314
315
  }
315
316
  }
@@ -327,8 +328,8 @@ export async function checkDockerRuntime() {
327
328
  return {
328
329
  name: CONTAINER_RUNTIME_CHECK,
329
330
  didPass: false,
330
- message: "Docker daemon is not responding",
331
- fix: DOCTOR_FIXES.dockerDaemonDown,
331
+ message: "Docker daemon is not responding (timeout)",
332
+ 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
333
  };
333
334
  }
334
335
  }
@@ -349,7 +350,7 @@ export async function checkDockerRuntime() {
349
350
  name: CONTAINER_RUNTIME_CHECK,
350
351
  didPass: false,
351
352
  message: "Colima is installed but not running",
352
- fix: DOCTOR_FIXES.dockerDaemonDown,
353
+ fix: "Start Colima: colima start\n Verify: colima status\n Docs: https://github.com/abiosoft/colima",
353
354
  };
354
355
  }
355
356
  if (await succeeds("podman ps")) {
@@ -364,14 +365,14 @@ export async function checkDockerRuntime() {
364
365
  name: CONTAINER_RUNTIME_CHECK,
365
366
  didPass: false,
366
367
  message: "Docker daemon is not running",
367
- fix: DOCTOR_FIXES.dockerDaemonDown,
368
+ 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
369
  };
369
370
  }
370
371
  return {
371
372
  name: CONTAINER_RUNTIME_CHECK,
372
373
  didPass: false,
373
- message: "Docker is not running",
374
- fix: DOCTOR_FIXES.dockerMissing,
374
+ message: "Docker is not installed",
375
+ fix: "Install Docker: https://docs.docker.com/get-docker/\n Verify: docker --version\n After install, start Docker and retry: flisk doctor",
375
376
  };
376
377
  }
377
378
  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 +443,7 @@ export async function checkDockerVersions(exec = execAsync) {
442
443
  name: "Docker Versions",
443
444
  didPass: false,
444
445
  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",
446
+ 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
447
  };
447
448
  }
448
449
  checkDockerVersions.requiresContainerRuntime = true;
@@ -457,7 +458,7 @@ export async function checkTilt(exec = execAsync) {
457
458
  name: "Tilt CLI",
458
459
  didPass: false,
459
460
  message: "Tilt CLI not found",
460
- fix: DOCTOR_FIXES.tiltMissing,
461
+ 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
462
  };
462
463
  }
463
464
  if (!versionMeetsMinimum(raw, MIN_TILT_VERSION)) {
@@ -465,7 +466,7 @@ export async function checkTilt(exec = execAsync) {
465
466
  name: "Tilt CLI",
466
467
  didPass: false,
467
468
  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`,
469
+ 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
470
  };
470
471
  }
471
472
  return { name: "Tilt CLI", didPass: true, message: `Tilt ${raw} is available` };
@@ -490,7 +491,7 @@ export async function checkDockerOperatingSystem(exec = execAsync) {
490
491
  name: "Docker OS",
491
492
  didPass: false,
492
493
  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.",
494
+ 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
495
  };
495
496
  }
496
497
  return { name: "Docker OS", didPass: true, message: "Docker is using Linux containers" };
@@ -522,7 +523,7 @@ export async function checkBun(exec = execAsync) {
522
523
  name: "Bun",
523
524
  didPass: false,
524
525
  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`,
526
+ 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
527
  };
527
528
  }
528
529
  return {
@@ -535,8 +536,8 @@ export async function checkBun(exec = execAsync) {
535
536
  return {
536
537
  name: "Bun",
537
538
  didPass: false,
538
- message: "Bun is not available",
539
- fix: DOCTOR_FIXES.bunMissing,
539
+ message: "Bun is not installed",
540
+ fix: "Install Bun: curl -fsSL https://bun.sh/install | bash\n Verify: bun --version\n Docs: https://bun.sh/docs/installation",
540
541
  };
541
542
  }
542
543
  }
@@ -27,6 +27,7 @@ async function tiltResourceNames(port) {
27
27
  }
28
28
  export const logsCommand = new Command("logs")
29
29
  .description("Print a bounded snapshot of recent logs from the running stack")
30
+ .showHelpAfterError("\nService names are not positional. Use: flisk logs --service <name>")
30
31
  .option("-s, --service <names...>", "Only these services (Tilt resource names)")
31
32
  .option("--tail <n>", "Number of most recent lines to return", String(DEFAULT_LOG_TAIL))
32
33
  .option("--since <duration>", "Only logs newer than this duration, for example 30s, 5m, 1h")
@@ -214,6 +214,7 @@ const DEFAULT_PROJECT_JSON = {
214
214
  overrides: {},
215
215
  };
216
216
  export const projectCommand = addYesOption(new Command("project"))
217
+ .alias("init")
217
218
  .description("Initialize or validate project-level master configuration")
218
219
  .argument("[template]", `Clone a starter example instead of a blank project (${Object.keys(PROJECT_TEMPLATES).join(", ")})`)
219
220
  .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
  })) ?? []));
@@ -55,6 +55,15 @@ export const statusCommand = new Command("status")
55
55
  // The same look at Tilt feeds readiness and each service's state, so `flisk status` and `flisk ui` cannot disagree.
56
56
  let serviceStates = null;
57
57
  const tiltPort = getTiltPollingPort(process.env.TILT_PORT);
58
+ // Port plan is best effort; service URLs are shown in both JSON and terminal output.
59
+ let portPlan = null;
60
+ try {
61
+ const root = findProjectRoot();
62
+ portPlan = root ? await getHostPortPlan(root, { inspectDocker: false }) : null;
63
+ }
64
+ catch {
65
+ // Port planning failed; service URLs will be omitted from terminal output.
66
+ }
58
67
  if (tiltAvailable) {
59
68
  const text = await tiltGetUiResources(tiltPort);
60
69
  // `tilt get` has no -f flag; resources come from the running Tilt's API on its port, the same source as readiness.
@@ -89,14 +98,6 @@ export const statusCommand = new Command("status")
89
98
  }
90
99
  }
91
100
  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
101
  const errors = queryError ? [{ code: "TILT_STATUS_UNAVAILABLE", message: queryError }] : [];
101
102
  const data = {
102
103
  tilt: {
@@ -178,9 +179,17 @@ export const statusCommand = new Command("status")
178
179
  : state.status === "error"
179
180
  ? chalk.red("✗")
180
181
  : chalk.yellow("…");
181
- showDetail(`${mark} ${resource.name} ${state.status}${state.reason ? ` (${state.reason})` : ""}`);
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})` : ""}`);
182
185
  }
183
186
  }
187
+ else if (discovery.resources.length > 0) {
188
+ // Without a running Tilt, no service state can be read. Say so instead of printing nothing.
189
+ console.log();
190
+ console.log(chalk.bold("Services:"));
191
+ showDetail(`unknown: no Tilt answers on port ${tiltPort}, so nothing is confirmed running. Start with: flisk up`);
192
+ }
184
193
  if (options.tilt && tiltAvailable) {
185
194
  console.log();
186
195
  showStep("Tilt Resources:");