@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,156 @@
1
+ // Copyright (c) 2026 Flisk Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ import { existsSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import chalk from "chalk";
6
+ import { readEnvFileValues } from "./env-selection.js";
7
+ import { showError } from "./errors.js";
8
+ import { formatCount } from "./formatting.js";
9
+ import { dependencyWaves, findMissingPrograms, formatPrefix, isPortListening, isProcessAlive, NATIVE_DIR, NativeSupervisor, nativeRunStatePath, planNativeRun, readNativeRunState, stopNativeRun, } from "./native-runtime.js";
10
+ import { findPortOccupant } from "./port-occupant.js";
11
+ /** Variables `${VAR}` in a service's env may reference: the env file of the run, overridden by the shell. */
12
+ function runVariables(projectRoot, envFilePath) {
13
+ const path = envFilePath ?? join(projectRoot, ".env");
14
+ const fromFile = existsSync(path) ? Object.fromEntries(readEnvFileValues(path)) : {};
15
+ const fromShell = Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
16
+ return { ...fromFile, ...fromShell };
17
+ }
18
+ /**
19
+ * `flisk up` for a selection where every service declares `command`: no Docker, no Compose, no Tilt and no generated files.
20
+ * Resolves only when the run ends (Ctrl+C, `flisk down`, or a failed start, which exits 1).
21
+ */
22
+ export async function runNativeUp(input) {
23
+ const { projectRoot, selected, emit } = input;
24
+ const exit = input.exit ?? ((code) => process.exit(code));
25
+ const quiet = Boolean(input.quiet);
26
+ const fail = (code, message, data = {}) => {
27
+ emit?.({ ok: false, runtime: "native", ...data }, [{ code, message }]);
28
+ showError(message);
29
+ return exit(1);
30
+ };
31
+ const vars = runVariables(projectRoot, input.envFilePath);
32
+ const plan = planNativeRun(selected, vars);
33
+ if (plan.errors.length > 0)
34
+ fail("INVALID_SERVICE_CONFIG", plan.errors.join("\n"));
35
+ if (plan.missingVars.length > 0 && !quiet) {
36
+ console.warn(chalk.yellow(`Unset variables expanded to an empty value: ${plan.missingVars.join(", ")}. Set them in .env or the shell.`));
37
+ }
38
+ const missing = findMissingPrograms(plan.services);
39
+ if (missing.length > 0) {
40
+ fail("NATIVE_COMMAND_NOT_FOUND", missing
41
+ .map(({ name, program }) => `service "${name}" runs ${program}, which is not installed (not found on PATH). Install it, or change "command" in its service.json.`)
42
+ .join("\n"));
43
+ }
44
+ const serviceNames = plan.services.map((service) => service.name);
45
+ const startOrder = dependencyWaves(plan.services);
46
+ const description = input.only
47
+ ? `${formatCount(input.only.length, "requested service")}${input.dependencies.length > 0 ? ` plus ${input.dependencies.join(", ")}` : ""}`
48
+ : input.stackName
49
+ ? `stack "${input.stackName}"`
50
+ : formatCount(serviceNames.length, "service");
51
+ if (!quiet) {
52
+ console.log(chalk.blue(`${input.dryRun ? "Would start" : "Starting"} ${description} as local processes (no Docker, no Tilt)...`));
53
+ for (const service of plan.services) {
54
+ const after = service.dependsOn.length > 0 ? ` (after ${service.dependsOn.join(", ")})` : "";
55
+ console.log(chalk.gray(` - ${service.name}: ${service.command}${after}`));
56
+ }
57
+ }
58
+ if (input.dryRun) {
59
+ emit?.({
60
+ ok: true,
61
+ dryRun: true,
62
+ runtime: "native",
63
+ stack: input.stackName ?? null,
64
+ services: serviceNames,
65
+ ...(input.only ? { requested: input.only, declaredDependencies: input.dependencies } : {}),
66
+ startOrder,
67
+ });
68
+ if (!quiet)
69
+ console.log(chalk.yellow("Dry run: not starting services"));
70
+ return;
71
+ }
72
+ const running = readNativeRunState(projectRoot);
73
+ if (running && running.pid !== process.pid && isProcessAlive(running.pid)) {
74
+ if (!input.force) {
75
+ const message = `This project is already running as local processes (flisk up, pid ${running.pid}).`;
76
+ emit?.({ ok: true, alreadyRunning: true, runtime: "native", pid: running.pid });
77
+ if (!quiet) {
78
+ console.log(chalk.green(message));
79
+ console.log(chalk.gray("Stop it with flisk down, or pass --force to restart it."));
80
+ }
81
+ return;
82
+ }
83
+ }
84
+ if (running) {
85
+ // A previous run that was replaced (--force) or killed without cleaning up: stop whatever it left behind.
86
+ await stopNativeRun(projectRoot);
87
+ }
88
+ for (const service of plan.services) {
89
+ if (service.port !== undefined && (await isPortListening(service.port))) {
90
+ const occupant = findPortOccupant(service.port);
91
+ fail("PORT_IN_USE", `Port ${service.port} for service "${service.name}" is already in use${occupant ? ` by ${occupant.command} (pid ${occupant.pid})` : ""}. Stop it, or change "port" in the service.json.`);
92
+ }
93
+ }
94
+ const width = Math.max(...serviceNames.map((name) => name.length));
95
+ const prefix = (name) => formatPrefix(name, serviceNames.indexOf(name), width);
96
+ const supervisor = new NativeSupervisor(projectRoot, plan.services, {
97
+ onLine: quiet ? undefined : (name, line) => console.log(`${prefix(name)}${line}`),
98
+ onNote: quiet
99
+ ? undefined
100
+ : (name, message) => console.log(`${prefix(name)}${chalk.gray(`[flisk] ${message}`)}`),
101
+ }, { readyTimeoutMs: input.readyTimeoutMs, baseEnv: vars });
102
+ let stopping;
103
+ const stopAndExit = (code) => {
104
+ stopping ??= (async () => {
105
+ if (!quiet)
106
+ console.log(chalk.gray("\nStopping local processes..."));
107
+ await supervisor.stop();
108
+ exit(code);
109
+ })();
110
+ };
111
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
112
+ process.on(signal, () => stopAndExit(0));
113
+ }
114
+ // If every service exits for good, nothing keeps `flisk up` alive; it ends and must not leave a run.json behind.
115
+ process.once("exit", () => {
116
+ if (readNativeRunState(projectRoot)?.pid === process.pid) {
117
+ rmSync(nativeRunStatePath(projectRoot), { force: true });
118
+ }
119
+ });
120
+ const failures = await supervisor.start();
121
+ if (stopping)
122
+ return;
123
+ if (failures.length > 0) {
124
+ const message = [
125
+ "Local processes did not start:",
126
+ ...failures.map((failure) => ` ✗ ${failure.name}: ${failure.reason}`),
127
+ `Output of every service: ${join(NATIVE_DIR, "logs")}/<service>.log`,
128
+ ].join("\n");
129
+ emit?.({ ok: false, runtime: "native", failures }, [{ code: "UP_RESOURCE_FAILED", message }]);
130
+ showError(message);
131
+ await supervisor.stop();
132
+ exit(1);
133
+ return;
134
+ }
135
+ emit?.({
136
+ ok: true,
137
+ runtime: "native",
138
+ stack: input.stackName ?? null,
139
+ services: serviceNames,
140
+ ...(input.only ? { requested: input.only, dependencies: input.dependencies } : {}),
141
+ urls: Object.fromEntries(plan.services.flatMap((service) => (service.url ? [[service.name, service.url]] : []))),
142
+ });
143
+ if (!quiet) {
144
+ const lines = ["Flisk is up (local processes, no Docker).", "Services:"];
145
+ for (const service of plan.services) {
146
+ const health = service.healthUrl ? ` (health: ${service.healthUrl})` : "";
147
+ lines.push(` ✓ ${service.name}${service.url ? ` → ${service.url}` : ""}${health}`);
148
+ }
149
+ lines.push("Stop: Ctrl+C here, or flisk down from another terminal");
150
+ lines.push(`Logs: flisk logs --service <name>, or ${join(NATIVE_DIR, "logs")}/<service>.log`);
151
+ for (const line of lines)
152
+ console.log(chalk.blue(line));
153
+ }
154
+ // The services keep the process alive; `flisk up` ends when it is stopped.
155
+ await new Promise(() => { });
156
+ }
@@ -33,7 +33,7 @@ export function binaryAssetNameFor(platformName, architecture) {
33
33
  return null;
34
34
  if (osName === "windows" && archName !== "amd64")
35
35
  return null;
36
- const base = `tdk-${osName}-${archName}`;
36
+ const base = `flisk-${osName}-${archName}`;
37
37
  return osName === "windows" ? `${base}.exe` : base;
38
38
  }
39
39
  export function binaryAssetName() {
@@ -27,7 +27,7 @@ export const PROJECT_TEMPLATES = {
27
27
  },
28
28
  example: {
29
29
  repo: "https://github.com/flisk-sh/flisk-example.git",
30
- bundledPath: "examples/tdk-example",
30
+ bundledPath: "examples/flisk-example",
31
31
  description: "Hono + Postgres + NATS worker + Vite example with a routed write path",
32
32
  },
33
33
  "agent-stack": {
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) 2026 Flisk Landscape contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { existsSync, readFileSync } from "node:fs";
4
+ import { declaresCommand, validateNativeServiceFields } from "./native-service.js";
4
5
  import { validateSmoke } from "./smoke.js";
5
6
  export const SERVICE_MANIFEST_SCHEMA_VERSION = 1;
6
7
  export const SERVICE_MANIFEST_SCHEMA_URL = "https://flisk.sh/schema.service.json";
@@ -24,12 +25,14 @@ const KNOWN_SERVICE_FIELDS = new Set([
24
25
  "basePath",
25
26
  "build",
26
27
  "buildContext",
28
+ "command",
27
29
  "databaseName",
28
30
  "dependencies",
29
31
  "dev",
30
32
  "dependsOn",
31
33
  "dockerfile",
32
34
  "enabled",
35
+ "env",
33
36
  "exposeViaProxy",
34
37
  "featuresEnabled",
35
38
  "framework",
@@ -183,7 +186,12 @@ export function validateServiceManifest(value, displayPath) {
183
186
  }
184
187
  const manifest = value;
185
188
  const errors = [];
186
- for (const field of ["appName", "appType", "stack", "schemaVersion"]) {
189
+ // A `command` service runs as a local process, so it has no stack to group generated output by (the minimal contract in
190
+ // utils/native-service.ts). Every other service keeps the full set.
191
+ const requiredFields = declaresCommand(manifest)
192
+ ? ["appName", "appType", "schemaVersion"]
193
+ : ["appName", "appType", "stack", "schemaVersion"];
194
+ for (const field of requiredFields) {
187
195
  if (!(field in manifest))
188
196
  errors.push(`${displayPath}.${field}: required field is missing`);
189
197
  }
@@ -206,6 +214,7 @@ export function validateServiceManifest(value, displayPath) {
206
214
  errors.push(`${displayPath}.dev.liveReload: expected true or false`);
207
215
  }
208
216
  errors.push(...validateGeneratedStringFields(manifest, displayPath));
217
+ errors.push(...validateNativeServiceFields(manifest).map((error) => `${displayPath}: ${error}`));
209
218
  if ("smoke" in manifest) {
210
219
  for (const message of validateSmoke(manifest.smoke))
211
220
  errors.push(`${displayPath}.${message}`);
@@ -6,6 +6,7 @@ import { Cache, createCacheValidator } from "./cache.js";
6
6
  import { AUTOGENERATED_FILE_PATTERNS, DOCKER_COMPOSE_FILE, PACKAGE_JSON, SERVICE_JSON, SKIP_DIRECTORIES, } from "./constants.js";
7
7
  import { errorFactories } from "./errors.js";
8
8
  import { formatCount } from "./formatting.js";
9
+ import { declaresCommand, validateNativeServiceFields } from "./native-service.js";
9
10
  import { findProjectRoot } from "./paths.js";
10
11
  import { getInvalidServiceRoutePathFields, validateGeneratedStringFields, validateServiceManifest, } from "./service-manifest.js";
11
12
  import { deriveStackStatus } from "./stack-status.js";
@@ -40,6 +41,10 @@ function isValidResourceConfig(value) {
40
41
  if (!value || typeof value !== "object")
41
42
  return false;
42
43
  const config = value;
44
+ // A `command` service (the minimal contract) has no legacy forms: appName and appType.
45
+ if (declaresCommand(config)) {
46
+ return typeof config.appName === "string" && typeof config.appType === "string";
47
+ }
43
48
  // Accept runtime, appType, or type (for backward compatibility with existing service.json files)
44
49
  const hasRuntimeOrAppType = typeof config.runtime === "string" ||
45
50
  typeof config.appType === "string" ||
@@ -79,8 +84,13 @@ function parseResource(serviceJsonPath) {
79
84
  throw new InvalidServiceJsonError(`invalid JSON: ${errorMessage(error)}`);
80
85
  }
81
86
  if (!isValidResourceConfig(parsed)) {
82
- throw new InvalidServiceJsonError("missing required fields (appName, runtime or appType)");
87
+ throw new InvalidServiceJsonError(declaresCommand(parsed)
88
+ ? "missing required fields (appName and appType)"
89
+ : "missing required fields (appName, runtime or appType)");
83
90
  }
91
+ const nativeErrors = validateNativeServiceFields(parsed);
92
+ if (nativeErrors.length > 0)
93
+ throw new InvalidServiceJsonError(nativeErrors.join("; "));
84
94
  const invalidRoutePathFields = getInvalidServiceRoutePathFields(parsed);
85
95
  if (invalidRoutePathFields.length > 0) {
86
96
  throw new InvalidServiceJsonError(invalidRoutePathFields.map((field) => `${field}: expected a string`).join("; "));
@@ -4,6 +4,35 @@ import { parseTiltResourceFailures, summarizeTiltBuildError } from "./doctor-run
4
4
  import { conflictPort, portConflictFix } from "./port-conflict-fix.js";
5
5
  import { effectiveRuntimeStatus, isTiltResourcePending } from "./tilt-resource-state.js";
6
6
  import { onlyEnabledResources } from "./up-readiness.js";
7
+ /** The resources Tilt says this one is waiting for. */
8
+ function waitingOn(item) {
9
+ return (item.status?.waiting?.on ?? [])
10
+ .filter((ref) => (ref.kind ?? "UIResource") === "UIResource" && ref.name)
11
+ .map((ref) => ref.name);
12
+ }
13
+ // Docker has no free subnet left for a new network (every project creates several and `flisk down` keeps them).
14
+ const NETWORK_POOL_EXHAUSTED = /all predefined address pools have been fully subnetted/i;
15
+ /**
16
+ * Tilt's error for a failed local command is only "exit status 1"; the reason (for example Docker's "all predefined
17
+ * address pools have been fully subnetted") is in the resource's log. Returns the fix for each failed resource whose
18
+ * message or log names a known machine problem.
19
+ */
20
+ export async function machineProblemFixes(failures, fetchLogs) {
21
+ const fixes = new Map();
22
+ for (const { name, message } of failures.slice(0, 5)) {
23
+ const fix = machineProblemFix(message) ?? machineProblemFix((await fetchLogs(name)) ?? "");
24
+ if (fix)
25
+ fixes.set(name, fix);
26
+ }
27
+ return fixes;
28
+ }
29
+ /** A plain fix for a failure message that names a known machine problem, or null. */
30
+ export function machineProblemFix(message) {
31
+ if (NETWORK_POOL_EXHAUSTED.test(message)) {
32
+ return "Fix: Docker has no free network ranges left. Run `flisk down --prune-networks` in Flisk projects you no longer use (or `docker network prune` to remove every unused network), then run flisk up again";
33
+ }
34
+ return null;
35
+ }
7
36
  export function transitiveDependencies(name, dependsOn) {
8
37
  const seen = new Set();
9
38
  const visit = (current) => {
@@ -31,13 +60,24 @@ export function buildStartupReport(jsonText, dependsOn, deferred = new Set()) {
31
60
  }));
32
61
  const failedNames = new Set(failed.map((entry) => entry.name));
33
62
  const items = JSON.parse(enabledJson).items ?? [];
63
+ // service.json dependsOn, plus what Tilt reports as waited for (shared infrastructure such as init-networks, which no
64
+ // service.json names).
65
+ const deps = { ...dependsOn };
66
+ for (const item of items) {
67
+ const name = item.metadata?.name;
68
+ if (!name)
69
+ continue;
70
+ const waits = waitingOn(item);
71
+ if (waits.length > 0)
72
+ deps[name] = [...new Set([...(deps[name] ?? []), ...waits])];
73
+ }
34
74
  const blocked = [];
35
75
  const starting = [];
36
76
  for (const item of items) {
37
77
  const name = item.metadata?.name ?? "unknown";
38
78
  if (name === "(Tiltfile)" || failedNames.has(name) || deferred.has(name))
39
79
  continue;
40
- const because = [...transitiveDependencies(name, dependsOn)]
80
+ const because = [...transitiveDependencies(name, deps)]
41
81
  .filter((dep) => failedNames.has(dep))
42
82
  .sort();
43
83
  if (because.length > 0)
@@ -52,12 +92,14 @@ export function isStartupStalled(jsonText, dependsOn, deferred = new Set()) {
52
92
  const report = buildStartupReport(jsonText, dependsOn, deferred);
53
93
  return report.failed.length > 0 && report.starting.length === 0;
54
94
  }
55
- export function formatStartupReport(report, timedOut, occupantOf) {
95
+ export function formatStartupReport(report, timedOut, occupantOf, machineFixes = new Map()) {
56
96
  const lines = [];
57
97
  for (const { name, message } of report.failed) {
58
98
  lines.push(`✗ ${name} failed: ${message}`);
59
99
  const port = conflictPort(message);
60
- const fix = portConflictFix(message, port !== null && occupantOf ? occupantOf(port) : null);
100
+ const fix = portConflictFix(message, port !== null && occupantOf ? occupantOf(port) : null) ??
101
+ machineFixes.get(name) ??
102
+ machineProblemFix(message);
61
103
  if (fix)
62
104
  lines.push(` ${fix}`);
63
105
  }
@@ -0,0 +1,23 @@
1
+ /** Label of the group that holds services declared without a stack. */
2
+ export const NO_STACK_LABEL = "(no stack)";
3
+ /** A service that is failing or still starting. Unknown (not running, starts on request) is neither. */
4
+ export function isUnhealthyState(state) {
5
+ return state?.status === "error" || state?.status === "pending";
6
+ }
7
+ /** Services grouped by stack, then the ones without a stack, each with its healthy/total count. */
8
+ export function groupServicesByStack(stacks, unassigned, states) {
9
+ const build = (name, resources) => ({
10
+ name,
11
+ resources: [...resources],
12
+ healthy: resources.filter((resource) => states?.[resource.name]?.status === "ready").length,
13
+ total: resources.length,
14
+ });
15
+ const groups = stacks.map((stack) => build(stack.name, stack.resources));
16
+ if (unassigned.length > 0)
17
+ groups.push(build(NO_STACK_LABEL, unassigned));
18
+ return groups;
19
+ }
20
+ /** Names of the failing or pending services, in discovery order. */
21
+ export function unhealthyServiceNames(resources, states) {
22
+ return resources.filter((r) => isUnhealthyState(states?.[r.name])).map((r) => r.name);
23
+ }
@@ -26,6 +26,14 @@ export function isValidSince(value) {
26
26
  }
27
27
  return total <= MAX_DURATION_NANOSECONDS;
28
28
  }
29
+ /** A valid --since duration in milliseconds, for logs Flisk reads itself (local processes) rather than through Tilt. */
30
+ export function sinceMilliseconds(value) {
31
+ let total = 0;
32
+ for (const [, amount, unit] of value.matchAll(/(\d+(?:\.\d+)?)(ns|us|µs|ms|s|m|h)/g)) {
33
+ total += Number(amount) * (NANOSECONDS[unit] ?? 0);
34
+ }
35
+ return total / 1e6;
36
+ }
29
37
  export function isValidPort(value) {
30
38
  return /^[1-9]\d{0,4}$/.test(value) && Number(value) <= 65535;
31
39
  }
@@ -0,0 +1,42 @@
1
+ // Copyright (c) 2026 Flisk Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ const DOCS = "https://github.com/flisk-sh/flisk/blob/main/docs";
4
+ /** Troubleshooting entries the hints point at. Each anchor is a heading in docs/troubleshooting.md. */
5
+ export const UP_HINT_DOCS = {
6
+ coldStart: `${DOCS}/troubleshooting.md#cold-start-blocked-docker-or-tilt-missing`,
7
+ unhealthy: `${DOCS}/troubleshooting.md#services-never-turn-healthy-or-are-left-behind`,
8
+ smoke: `${DOCS}/smoke.md`,
9
+ };
10
+ /**
11
+ * One or two commands to run next after a failed `flisk up`, plus the troubleshooting page for that failure. Printed
12
+ * under the error and, with `--json`, carried in the error's `suggestions` field.
13
+ */
14
+ export function upFailureHints(failure) {
15
+ switch (failure.kind) {
16
+ case "machine":
17
+ return [
18
+ "Fix the checks above, then run: flisk doctor",
19
+ `Troubleshooting: ${UP_HINT_DOCS.coldStart}`,
20
+ ];
21
+ case "invalid-tilt-port":
22
+ return ["Unset it to use the default port: unset TILT_PORT"];
23
+ case "tilt-exited":
24
+ return [
25
+ "Check Docker, Tilt and the project: flisk doctor",
26
+ "Rerun with Tilt's own output: flisk up --verbose",
27
+ ];
28
+ case "services":
29
+ case "smoke": {
30
+ const logs = failure.failed.length > 0
31
+ ? [`See why ${failure.failed[0]} failed: flisk logs -s ${failure.failed[0]}`]
32
+ : [];
33
+ return failure.kind === "smoke"
34
+ ? [...logs, `Smoke checks: ${UP_HINT_DOCS.smoke}`]
35
+ : [
36
+ ...logs,
37
+ "Check the whole stack: flisk doctor",
38
+ `Troubleshooting: ${UP_HINT_DOCS.unhealthy}`,
39
+ ];
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,117 @@
1
+ // Copyright (c) 2026 Flisk Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ import { effectiveRuntimeStatus } from "./tilt-resource-state.js";
4
+ import { isAdvisoryResource } from "./up-readiness.js";
5
+ /**
6
+ * The state of each resource `flisk up` waits for, from `tilt get uiresources -o json`. Disabled, deferred, advisory and
7
+ * the `(Tiltfile)` resource are left out, so the count matches what readiness gates on.
8
+ */
9
+ export function classifyUpProgress(jsonText, deferred) {
10
+ const items = JSON.parse(jsonText).items ?? [];
11
+ const services = [];
12
+ for (const item of items) {
13
+ const name = item.metadata?.name;
14
+ if (!name || name === "(Tiltfile)" || deferred.has(name) || isAdvisoryResource(name))
15
+ continue;
16
+ if (item.status?.disableStatus?.state === "Disabled")
17
+ continue;
18
+ const rawUpdate = item.status?.updateStatus ?? "";
19
+ // A serve-only local resource has nothing to build.
20
+ const update = rawUpdate === "not_applicable" ? "ok" : rawUpdate;
21
+ const runtime = effectiveRuntimeStatus(item.status);
22
+ let state;
23
+ if (update === "error" || runtime === "error")
24
+ state = "failed";
25
+ else if (update === "in_progress")
26
+ state = "building";
27
+ else if (update === "ok")
28
+ state = runtime === "pending" || runtime === "none" ? "starting" : "ready";
29
+ else
30
+ state = "waiting";
31
+ services.push({ name, state });
32
+ }
33
+ return services.sort((a, b) => a.name.localeCompare(b.name));
34
+ }
35
+ /** `5/9 ready`, then the services in each unfinished state. */
36
+ export function formatUpProgressLine(services) {
37
+ const ready = services.filter((service) => service.state === "ready").length;
38
+ const parts = [`${ready}/${services.length} ready`];
39
+ for (const state of ["failed", "building", "starting", "waiting"]) {
40
+ const names = services.filter((service) => service.state === state).map((s) => s.name);
41
+ if (names.length > 0)
42
+ parts.push(`${state}: ${names.join(", ")}`);
43
+ }
44
+ return parts.join(" · ");
45
+ }
46
+ export function formatServiceFailedLine(name) {
47
+ return `✗ ${name} failed. See its logs: flisk logs -s ${name}`;
48
+ }
49
+ /**
50
+ * Redraw one line in place only on an interactive terminal. `--quiet`, a pipe, CI and `TERM=dumb` get plain lines with
51
+ * no cursor control.
52
+ */
53
+ export function shouldRedrawUpProgress(input) {
54
+ if (!input.isTTY || input.quiet)
55
+ return false;
56
+ if (input.env.CI && input.env.CI !== "false" && input.env.CI !== "0")
57
+ return false;
58
+ return input.env.TERM?.toLowerCase().startsWith("dumb") !== true;
59
+ }
60
+ /**
61
+ * Shows per-service progress while `flisk up` waits for Tilt. Redraw mode keeps one line current and prints a failure as
62
+ * a line of its own; plain mode prints one line per state change.
63
+ */
64
+ export function createUpProgressReporter(options) {
65
+ const previous = new Map();
66
+ let lineShown = false;
67
+ const clearLine = () => {
68
+ if (lineShown)
69
+ options.write("\r\x1b[2K");
70
+ lineShown = false;
71
+ };
72
+ return {
73
+ update(jsonText) {
74
+ let services;
75
+ try {
76
+ services = classifyUpProgress(jsonText, options.deferred);
77
+ }
78
+ catch {
79
+ return; // Tilt can answer with partial output while it starts.
80
+ }
81
+ if (services.length === 0)
82
+ return;
83
+ const ready = services.filter((service) => service.state === "ready").length;
84
+ const changed = services.filter((service) => previous.get(service.name) !== service.state);
85
+ for (const service of services)
86
+ previous.set(service.name, service.state);
87
+ if (options.redraw) {
88
+ const failed = changed.filter((service) => service.state === "failed");
89
+ if (failed.length > 0)
90
+ clearLine();
91
+ for (const service of failed)
92
+ options.write(`${formatServiceFailedLine(service.name)}\n`);
93
+ const width = Math.max(20, (options.columns ?? 80) - 1);
94
+ const line = `⏳ ${formatUpProgressLine(services)}`;
95
+ options.write(`\r\x1b[2K${line.length > width ? `${line.slice(0, width - 1)}…` : line}`);
96
+ lineShown = true;
97
+ return;
98
+ }
99
+ for (const service of changed) {
100
+ const count = `(${ready}/${services.length} ready)`;
101
+ options.write(service.state === "failed"
102
+ ? `${formatServiceFailedLine(service.name)} ${count}\n`
103
+ : `${service.name}: ${service.state} ${count}\n`);
104
+ }
105
+ },
106
+ finish: clearLine,
107
+ };
108
+ }
109
+ /** Wraps the readiness poll so every answer from Tilt also updates the progress. */
110
+ export function fetchWithProgress(fetchJson, progress) {
111
+ return async (port) => {
112
+ const text = await fetchJson(port);
113
+ if (text)
114
+ progress.update(text);
115
+ return text;
116
+ };
117
+ }
@@ -8,6 +8,11 @@ export function tiltGetUiResources(port) {
8
8
  execFile(findOnPath("tilt") ?? "tilt", ["get", "uiresources", "-o", "json", "--port", String(port)], { timeout: 15_000, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => resolve(error ? null : stdout));
9
9
  });
10
10
  }
11
+ export function tiltResourceLogs(port, name) {
12
+ return new Promise((resolve) => {
13
+ execFile(findOnPath("tilt") ?? "tilt", ["logs", name, "--port", String(port)], { timeout: 15_000, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => resolve(error ? null : stdout));
14
+ });
15
+ }
11
16
  /**
12
17
  * Tilt lists resources outside the current focus (the other release phases, or everything but a `--only` selection) with
13
18
  * `disableStatus.state: Disabled` and both statuses `none`, which would read as pending forever. A resource whose update
@@ -1,5 +1,5 @@
1
1
  import{createRequire as __fliskRequire}from'node:module';const require=__fliskRequire(import.meta.url);
2
- import{b}from"./chunk-MINZ25AC.js";import{g as m,r as T}from"./chunk-V36PT2HF.js";import{a as S}from"./chunk-TIYTGN5R.js";import{F as k,H as $,fa as x,p as y,q as w}from"./chunk-PEZQEZVV.js";import{b as n,f as d,m as g,x as h}from"./chunk-U42XZWXB.js";import{Command as R}from"commander";import{readFileSync as N}from"node:fs";import{join as O}from"node:path";function F(o){let e=o.config;return e?Array.isArray(e.featuresEnabled)&&e.featuresEnabled.includes(d)?!0:(Array.isArray(e.dependsOn)?e.dependsOn:[]).some(a=>S.includes(a)):!1}function J(o,e){let s=o.replace(/-/g,"_"),a=[];for(let t of[...e].sort((r,c)=>r.name.localeCompare(c.name))){let r=t.resources.filter(F).map(c=>c.name);r.length!==0&&a.push({name:`${s}_${t.name.replace(/-/g,"_")}`,stack:t.name,services:r.sort()})}return a.length===0?null:{name:"postgres",databases:a}}function E(o){try{let e=JSON.parse(N(O(o,".flisk",g),"utf-8"));return typeof e?.project?.name=="string"?e.project.name:null}catch{return null}}var P=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function A(o){let e=process.stderr,s=e.isTTY===!0,a=0,t=o,r,c=()=>e.write(`\r\x1B[2K${n.cyan(P[a])} ${t}`);s?(c(),r=setInterval(()=>{a=(a+1)%P.length,c()},80)):e.write(`- ${t}
2
+ import{b}from"./chunk-MINZ25AC.js";import{g as m,r as T}from"./chunk-VYPB7MY5.js";import{a as S}from"./chunk-XHENT4KO.js";import{K as k,M as $,ka as x,p as y,q as w}from"./chunk-L6O7TT3K.js";import{b as n,f as d,m as g,x as h}from"./chunk-TIPSERWO.js";import{Command as R}from"commander";import{readFileSync as N}from"node:fs";import{join as O}from"node:path";function F(o){let e=o.config;return e?Array.isArray(e.featuresEnabled)&&e.featuresEnabled.includes(d)?!0:(Array.isArray(e.dependsOn)?e.dependsOn:[]).some(a=>S.includes(a)):!1}function J(o,e){let s=o.replace(/-/g,"_"),a=[];for(let t of[...e].sort((r,c)=>r.name.localeCompare(c.name))){let r=t.resources.filter(F).map(c=>c.name);r.length!==0&&a.push({name:`${s}_${t.name.replace(/-/g,"_")}`,stack:t.name,services:r.sort()})}return a.length===0?null:{name:"postgres",databases:a}}function E(o){try{let e=JSON.parse(N(O(o,".flisk",g),"utf-8"));return typeof e?.project?.name=="string"?e.project.name:null}catch{return null}}var P=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function A(o){let e=process.stderr,s=e.isTTY===!0,a=0,t=o,r,c=()=>e.write(`\r\x1B[2K${n.cyan(P[a])} ${t}`);s?(c(),r=setInterval(()=>{a=(a+1)%P.length,c()},80)):e.write(`- ${t}
3
3
  `);let l=(i,u)=>{r&&clearInterval(r),e.write(`${s?"\r\x1B[2K":""}${i} ${u??t}
4
4
  `)};return{get text(){return t},set text(i){t=i,s||e.write(`- ${t}
5
5
  `)},succeed:i=>l(n.green("\u2714"),i),fail:i=>l(n.red("\u2716"),i),warn:i=>l(n.yellow("\u26A0"),i)}}async function j(o){let{resources:e}=x(o),s=await m(o,{inspectDocker:!1}),{appBase:a}=k(s?.ingressHttp,o);return e.filter(t=>t.config?.appType==="frontend").map(t=>({name:t.name,url:`${a}${$(t)}`}))}async function v(o,e=2e3){try{return(await fetch(o,{signal:AbortSignal.timeout(e)})).status<500}catch{return!1}}async function D(o,e=3e4,s){if(o.length===0)return;let a=Date.now();for(;;){for(let r of o)if(await v(r.url))return r;let t=Date.now()-a;if(t>=e)return;s?.(t),await new Promise(r=>setTimeout(r,2e3))}}async function te(o,e=0,s){let a=await j(o);return D(a,e,s)}async function C(o){let e=o??h();if(!e)return console.log(n.yellow("Not in a Flisk project, so there is no frontend to open.")),console.log(n.gray("Run `flisk project --yes` first, or `flisk open` inside a project.")),null;let s=await j(e);if(s.length===0)return console.log(n.yellow("This project has no frontend service to open.")),console.log(n.gray("Start the stack with `flisk up` and add a frontend with `flisk resource`.")),null;let a=E(e);y(`Opening the first frontend of ${a??"this project"}`);for(let l of s)console.log(n.gray(` - ${l.name}: ${l.url}`));let t=A("Waiting for the frontend to answer..."),r=await D(s,12e4,l=>{t.text=`Waiting for the frontend to answer... ${Math.round(l/1e3)}s`});if(!r){t.fail("No frontend answered yet.");let i=(await m(e,{inspectDocker:!1}))?.ingressHttp,u=i?await T(i)==="in-use":!1;if(i&&!u){console.log(n.yellow(`The stack is running, but this machine cannot reach port ${i}.`)),console.log(n.gray(" Docker is not publishing the port to the host."));let{checkColima:f}=await import("./colima-GBURKN54.js"),p=f();p.isColima&&!p.running?(console.log(n.gray(` ${p.problem??"The Colima VM is not running"}`)),console.log(n.gray(` Fix: ${p.fix??"colima start"}`))):console.log(n.gray(" Restart the Docker runtime (Docker Desktop or Colima), then run `flisk open` again."))}else console.log(n.yellow("The stack is not serving the frontend yet.")),console.log(n.gray(" - Start the stack in another terminal: flisk up")),console.log(n.gray(" - Then run this again: flisk open"));for(let f of s)console.log(n.gray(` - ${f.name}: ${f.url}`));return null}t.succeed(`The frontend is up: ${r.name}`);let c=await b(r.url);return console.log(c?n.green(`
@@ -0,0 +1,35 @@
1
+ import{createRequire as __fliskRequire}from'node:module';const require=__fliskRequire(import.meta.url);
2
+ import{a as T}from"./chunk-QEBN6ZVQ.js";import{b as C,c as V,d as Y,m as me,p as L,q as H,v as J,w as I,y as O}from"./chunk-MYAYJF4Q.js";import{C as re,D as K,E as W,fa as ae,h as D,i as B,ka as ce,la as le,na as fe,o as g,p as v,pa as de,q as o,qa as pe,ra as ue}from"./chunk-L6O7TT3K.js";import{N as U,P as z,R as A,b as m,f as E,m as R,o as P,x as N}from"./chunk-TIPSERWO.js";import{execFileSync as Fe}from"node:child_process";import{cpSync as Re,existsSync as b,readdirSync as Ne,readFileSync as Q,writeFileSync as De}from"node:fs";import{basename as be,dirname as Le,isAbsolute as Je,join as j,resolve as q}from"node:path";import{cwd as je}from"node:process";import{fileURLToPath as Ie}from"node:url";import{Command as Ge}from"commander";import{existsSync as ge,mkdirSync as _e,writeFileSync as he}from"node:fs";import G from"node:path";var xe=["flisk-build","flisk-recipe-starters","flisk-recipe-shop","flisk-recipe-saas","flisk-recipe-ai-agent","flisk-recipe-blog","flisk-doctor","flisk-up","flisk-resource","flisk-status","flisk-troubleshoot","flisk-agent-ops"],Ee=[".claude/skills",".agents/skills"],Pe="https://github.com/flisk-sh/flisk/blob/main/docs/",Ae="# AGENTS.md\n\nThis 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`.\n\n- Add a service: `flisk resource <name> --type <frontend|backend|worker|agent|mcp> --stack <stack> --yes`\n- Start it: `flisk up <stack>`, then check every URL from `flisk networks`\n- When something fails: `flisk doctor`, then `flisk logs -s <service>`\n- Do not hand-write Dockerfiles, Compose files or Tiltfiles, and do not edit `.autogenerated/`: change `service.json` instead.\n";function Ce(e){return e.replace(/\]\((?:\.\.\/)+docs\//g,`](${Pe}`)}function ke(e){let t=[];for(let a of xe){let f=Ce(me(`agent-skills/${a}/SKILL.md`));for(let d of Ee){let i=G.join(d,a,"SKILL.md"),n=G.join(e,i);ge(n)||(_e(G.dirname(n),{recursive:!0}),he(n,f),t.push(i))}}let c=G.join(e,"AGENTS.md");return ge(c)||(he(c,Ae),t.push("AGENTS.md")),t}var Oe="-y, --yes",Te="Skip prompts and confirmations; use defaults";function Se(e,t=Te){return e.option(Oe,t,!1)}async function $e(e,t=!0){let c=await I({message:e,initial:t});return c||D(),c}function Ze(e,t=1){e.valid||A(e.error??"Validation failed",t)}function et(e,t,c,a){return e.dryRun?(a?a():(console.log(`Dry run - ${t}`),console.log(`Would run: ${c}`)),!0):!1}async function tt(e,t){return await $e(e,!0)?!0:(t&&t(),!1)}var M=new Map;function ye(e){e!==void 0?M.delete(e):M.clear()}function st(e=!1){let t=N(),c=t??"";if(!e){let p=M.get(c);if(p)return p}let{resources:a,problems:f}=t?ce(t):{resources:fe(),problems:[]},d=ue(a),i=pe(a);le(f);let n=a.filter(p=>!p.stack),u=new Map;for(let p of a)if(p.stack){let l=u.get(p.stack)||[];l.push(p),u.set(p.stack,l)}let h={resources:a,problems:f,stacks:d,stackNames:i,unassignedResources:n,resourcesByStack:u};return M.set(c,h),h}var $={proxy:{name:"proxy",description:"Traefik reverse proxy and API gateway for routing all services",category:"core",phase:"pre_alpha",enabled_by_default:!0,dependsOn:[]},verdaccio:{name:"verdaccio",description:"Verdaccio npm registry for package management and local publishing (Premium - requires FLISK_LICENSE_KEY)",category:"premium",phase:"pre_alpha",enabled_by_default:!1,dependsOn:[]},[E]:{name:E,description:"PostgreSQL database for data persistence and SQL operations",category:"core",phase:"pre_alpha",enabled_by_default:!0,dependsOn:[]},monitoring:{name:"monitoring",description:"SigNoz or SkyWalking observability platform for monitoring and tracing",category:"optional",phase:"pre_alpha",enabled_by_default:!1,dependsOn:[E]},elk:{name:"elk",description:"Elasticsearch, Logstash, Kibana stack for log aggregation and analysis",category:"optional",phase:"pre_alpha",enabled_by_default:!1,dependsOn:[]},debezium:{name:"debezium",description:"Change Data Capture for real-time data streaming from databases",category:"optional",phase:"pre_alpha",enabled_by_default:!1,dependsOn:[E]},"golden-image":{name:"golden-image",description:"Pre-built Docker image layer for faster builds and smaller deployments",category:"optional",phase:"pre_alpha",enabled_by_default:!0,dependsOn:[]}};function X(e,t){try{return de(e,t)}catch{return[]}}function Me(e){return new Set([...e.phases.pre_alpha.enabledStacks,...e.phases.alpha.enabledStacks,...e.phases.beta.enabledStacks,...e.phases.out_of_scope.enabledStacks])}function Z(e,t){let c=Me(e),a=t.filter(f=>!c.has(f));return a.length===0?(e.phases.pre_alpha.enabledStacks=Array.from(new Set(e.phases.pre_alpha.enabledStacks)),[]):(e.phases.pre_alpha.enabledStacks=Array.from(new Set([...e.phases.pre_alpha.enabledStacks,...a])),a)}function Tt(e,t){let c=j(e,".flisk",R);if(!b(c))return[];let a=L(e),f=Z(a,X(e,t));return f.length>0&&C(c,a),f}function qe(){return Object.values($).filter(e=>e.enabled_by_default&&e.category==="core").map(e=>e.name)}function Ue(e,t){return t||(T[e]?.repo.split("/").pop()??e).replace(/\.git$/,"")}function ve(e){return e.split(`
3
+ `).map(c=>c.trim()).filter(Boolean).pop()??"unknown error"}async function ze(e,t,c,a={}){let f=T[e];if(!f){console.log(m.red(`
4
+ \u274C Unknown template: "${e}"`)),console.log(m.yellow(`
5
+ Available templates:`));for(let[n,u]of Object.entries(T))console.log(` ${m.cyan(n)} ${m.gray(`- ${u.description}`)}`);console.log(m.gray("\nOr run `flisk project` with no template for a blank project.")),process.exit(1)}let d=Ue(e,t);ae(d)||U.invalidPath(d).exit();let i=Je(d)?d:q(je(),d);b(i)&&Ne(i).length>0&&U.directoryExists(i).exit(),a.quiet||(B(`Cloning template: ${e}`),o(`${f.description}
6
+ `,0),o(`Source: ${f.bundledPath?"bundled Flisk example":f.repo}`,0),o(`Destination: ${i}
7
+ `,0));try{if(f.bundledPath){let n=q(Le(Ie(new URL("../dist/commands/project.js",import.meta.url).href)),"../../../");Re(q(n,f.bundledPath),i,{recursive:!0});let u=be(i).replace(/-/g,"_");for(let h of["services/platform/messaging/docker-compose.yml","services/shop/orders-api/service.json","services/shop/orders-api/src/index.ts","services/shop/orders-worker/service.json","services/shop/orders-worker/src/index.ts"]){let p=j(i,h),l=Q(p,"utf-8");De(p,l.replaceAll("flisk_example",u))}}else Fe("git",["clone",f.repo,i],{stdio:["ignore","pipe","pipe"],encoding:"utf-8"})}catch(n){let u=n instanceof Error?n.message:String(n),h=f.bundledPath?`Bundled example copy failed (${ve(u)}). Reinstall the Flisk CLI package and try again.`:`git clone failed: ${ve(u)}. Check you have git installed and network access.`;if(a.quiet)throw new Error(h);A(h)}if(ye(),!a.quiet){console.log(m.green(`
8
+ \u2705 Cloned "${e}" into ${d}/`)),o(`
9
+ Next steps:`,0),o(`1. cd ${d}`),o("2. flisk doctor # check Docker/Tilt are ready"),o("3. flisk up # start the stack"),o(`
10
+ Runtime readiness:`,0);for(let n of c)o(n,0)}}var _={version:"1.0",project:{name:"",version:"1.0.0"},phases:{pre_alpha:{name:"Pre-Alpha",description:"Core infrastructure and MVP services",enabledStacks:qe()},alpha:{name:"Alpha",description:"Essential business services",enabledStacks:[]},beta:{name:"Beta",description:"Extended features",enabledStacks:[]},out_of_scope:{name:"Out of Scope",description:"Future releases",enabledStacks:[]}},optional_infra:{monitoring:!1,elk:!1,debezium:!1,golden_image:!0,verdaccio:!1},discovery:{paths:["services/*/*"]},overrides:{}};function $t(e){let t=JSON.parse(JSON.stringify(_));t.project.name=be(e)||"my-project",V(j(e,".flisk")),C(j(e,".flisk",R),t)}var Ft=Se(new Ge("project")).alias("init").description("Initialize or validate project-level master configuration").argument("[template]",`Clone a starter example instead of a blank project (${Object.keys(T).join(", ")})`).option("--path <dir>","Directory to clone the template into (default: the template's repo name)").option("--check","Check if master configs exist and are in sync").option("--force","Overwrite existing configuration (dangerous)").option("--config-file <path>","Load project config from existing JSON file").option("-q, --quiet","Only print what fails; used by `flisk demo`",!1).option("--no-agent-skills","Do not write the Flisk agent skills (.claude/skills, .agents/skills) and AGENTS.md").action(async(e,t)=>{let{assertMachineReadyOrExit:c,detectRuntimePrerequisites:a,formatRuntimeNextSteps:f}=await import("./cold-preflight-DQVJUGBB.js");await c({scope:"scaffold"});let d=f(a()),i=!!t.quiet;if(e){await z(async()=>ze(e,t.path,d,{quiet:i}));return}await z(async()=>{let n=N();n||(n=je(),i||(v(`\u{1F680} Initializing new Flisk project...
11
+ `),o(`Location: ${n}
12
+ `,0)));let u=j(n,".flisk"),h=j(u,R);if(t.check){let r=P.every(S=>b(j(n,".flisk",".flisk-out",S))),k=b(h);if(r&&k){let S=L(n);g("Project configuration is valid"),o(`Project: ${S.project.name}`,3),o(`Phases: ${Object.keys(S.phases).join(", ")}`,3),process.exit(0)}else{if(console.log(m.yellow("\u26A0\uFE0F Project configuration incomplete:")),k||o(".flisk/project.json (not found)",3),!r)for(let S of P.filter(w=>!b(j(n,".flisk/.flisk-out",w))))o(`.flisk/.flisk-out/${S} (not found)`,3);o("Run `flisk project` to create them."),process.exit(1)}}i||(B("Project Configuration"),o(`Project root: ${n}
13
+ `,0)),b(u)||(V(u),i||g("Created: .flisk/ directory"));let p=b(h);if(p&&!t.force){i||g(".flisk/project.json exists");let r=JSON.parse(Q(h,"utf-8")),k=L(n),S=X(n),w=Z(k,S);if((w.length>0||!r.phases)&&C(h,k),w.length>0&&!i&&o(`Auto-enabled discovered service stacks: ${w.join(", ")}`,0),i||v(`
14
+ \u{1F4CB} Regenerating master configuration files...
15
+ `),await H(n,{quiet:i}),W(n))i||g("Created: .env (the defaults work as-is)");else{let y=K(n);y.length>0&&!i&&g(`Updated: .env (added ${y.join(", ")}; existing values unchanged)`)}let F=Y(n);if(F.length>0&&!i&&g(`Added to .gitignore: ${F.join(", ")}`),!i){console.log(m.green(`
16
+ \u2705 Project configuration regenerated!`)),o(`
17
+ Generated in .flisk/.flisk-out/:`,0);for(let y of P)o(`- ${y}`);o(`
18
+ Runtime readiness:`,0);for(let y of d)o(y,0)}return}if(p&&t.force&&!t.yes&&(console.log(m.red(`
19
+ \u26A0\uFE0F WARNING: --force will overwrite .flisk/project.json!`)),!await I({message:"This will reset your project configuration. Continue?",initial:!1}))){D();return}let l,ee=X(n);if(t.configFile){let r=q(t.configFile);b(r)||A(`Config file not found: ${r}`);let k=Q(r,"utf-8");l=JSON.parse(k),i||g(`Loaded config from: ${r}`)}else if(t.yes)l=JSON.parse(JSON.stringify(_)),l.project.name=n.split("/").pop()||"my-project",i||console.log(m.gray("Using default configuration (non-interactive mode)"));else{v(`\u{1F4DD} Project Setup Wizard
20
+ `);let r=await J({message:"Project name:",initial:n.split("/").pop()||"my-project",validate:s=>s.trim()!==""||"Project name is required"}),k=await J({message:"Project version:",initial:"1.0.0-alpha"}),S=Object.values($).filter(s=>s.category==="core"&&s.phase==="pre_alpha"),w=new Set(S.map(s=>s.name)),ne=await O({message:"Select Pre-Alpha enabled stacks (core infrastructure):",choices:[...S.map(s=>({title:s.description,value:s.name,selected:s.enabled_by_default})),...ee.filter(s=>!w.has(s)).map(s=>({title:`${s} (discovered service stack)`,value:s,selected:!0}))]}),F=await O({message:"Select Alpha enabled stacks (core business):",choices:[{title:"api (Backend API)",value:"api"},{title:"app (Frontend app)",value:"app"}]}),y=await O({message:"Select Beta enabled stacks (extended features):",choices:[{title:"worker (Background jobs)",value:"worker"},{title:"migrator (Database migrations)",value:"migrator"}]}),x=await O({message:"Enable optional infrastructure (high resource):",choices:Object.values($).filter(s=>s.category==="optional").concat(Object.values($).filter(s=>s.category==="premium")).map(s=>({title:s.description,value:s.name,selected:s.enabled_by_default}))});v(`
21
+ \u{1F4C2} Service discovery`),o("What: folder patterns Flisk scans for a service.json in each match."),o("Where: type one or more globs, comma-separated."),o("How: saved as discovery.paths in .flisk/project.json - editable later."),o(`Example: ${_.discovery.paths.join(", ")}
22
+ `);let we=await J({message:"Folders to scan:",initial:_.discovery.paths.join(", ")});l=JSON.parse(JSON.stringify(_)),l.project.name=r,l.project.version=k,l.phases.pre_alpha.enabledStacks=Array.from(new Set(ne)),l.phases.alpha.enabledStacks=Array.from(new Set(F)),l.phases.beta.enabledStacks=Array.from(new Set(y)),l.optional_infra.monitoring=x.includes("monitoring"),l.optional_infra.elk=x.includes("elk"),l.optional_infra.debezium=x.includes("debezium"),l.optional_infra.golden_image=x.includes("golden_image"),l.optional_infra.verdaccio=x.includes("verdaccio");let se=we.split(",").map(s=>s.trim()).filter(s=>s.length>0);l.discovery.paths=se.length>0?se:_.discovery.paths}let te=Z(l,ee);if(te.length>0&&!i&&o(`Auto-enabled discovered service stacks: ${te.join(", ")}`,0),i||v(`
23
+ \u{1F4CB} Creating project configuration...
24
+ `),C(h,l),i||(g("Created: .flisk/project.json"),o(`\u2192 Project: ${l.project.name}`),v(`
25
+ \u{1F4CB} Generating master configuration files...
26
+ `)),await H(n,{quiet:i}),i||v(`
27
+ \u{1F527} Setting up environment variables...
28
+ `),W(n))i||g("Created: .env (the defaults work as-is)");else{let r=K(n);r.length>0?i||g(`Updated: .env (added ${r.join(", ")}; existing values unchanged)`):i||g("Found: .env (environment already configured)")}let oe=re(n);if(oe.missing.length>0){console.log(m.yellow(`
29
+ \u26A0\uFE0F Missing required environment variables:`));for(let r of oe.missing)o(`- ${r}`,1);o("\u2192 Edit .env and fill in these values before running `flisk up`",1)}let ie=Y(n);if(ie.length>0&&!i&&g(`Added to .gitignore: ${ie.join(", ")}`),t.agentSkills!==!1)try{ke(n).length>0&&!i&&g("Added agent skills: .claude/skills, .agents/skills and AGENTS.md (start with flisk-build)")}catch(r){let k=r instanceof Error?r.message:String(r);console.log(m.yellow(`\u26A0\uFE0F Could not add the agent skills: ${k}`))}if(!i){console.log(m.green(`
30
+ \u2705 Project configuration complete!`)),o(`
31
+ Generated files in .flisk/.flisk-out/:`,0);for(let r of P)o(`- ${r}`);o(`
32
+ Environment configuration:`,0),o("- .env (contains all required variables)"),o(`
33
+ Source file:`,0),o("- .flisk/project.json (edit this to change project structure)"),o(`
34
+ Next steps:`,0),o("1. flisk resource api --type backend --stack app # scaffold a service"),o("2. flisk up app # start it"),o(`
35
+ Runtime readiness:`,0);for(let r of d)o(r,0);o("\nAfter editing .flisk/project.json, run `flisk config regenerate`.",0)}})});export{ye as a,st as b,Se as c,Ze as d,et as e,tt as f,Tt as g,Ue as h,ze as i,$t as j,Ft as k};
@@ -1,2 +1,2 @@
1
1
  import{createRequire as __fliskRequire}from'node:module';const require=__fliskRequire(import.meta.url);
2
- import{r as c}from"./chunk-4J2FLFUA.js";import{execFileSync as u}from"node:child_process";import{realpathSync as l}from"node:fs";import{resolve as p}from"node:path";var m=(o,t)=>u(o,t,{encoding:"utf-8",stdio:"pipe",timeout:5e3,windowsHide:!0});function a(o,t=process.platform,e=m){if(t==="win32")return null;try{let s=e("lsof",["-nP",`-iTCP:${o}`,"-sTCP:LISTEN","-t"]).split(/\r?\n/).map(r=>r.trim()).filter(Boolean);for(let r of s){let n=e("ps",["-p",r,"-o","command="]).trim(),[i]=c(`${r} ${n}`);if(i)return i}}catch{}return null}function f(o){try{return l(o)}catch{return p(o)}}function d(o,t){return o!==null&&f(o.root)!==f(t)}async function P(o,t,e,s=a){let r={running:[],foreign:[]};for(let n of o){if(!await e(n))continue;let i=s(n);i&&d(i,t)?r.foreign.push({port:n,root:i.root}):r.running.push(n)}return r}export{a,d as b,P as c};
2
+ import{r as c}from"./chunk-T44DEEL6.js";import{execFileSync as u}from"node:child_process";import{realpathSync as l}from"node:fs";import{resolve as p}from"node:path";var m=(o,t)=>u(o,t,{encoding:"utf-8",stdio:"pipe",timeout:5e3,windowsHide:!0});function a(o,t=process.platform,e=m){if(t==="win32")return null;try{let s=e("lsof",["-nP",`-iTCP:${o}`,"-sTCP:LISTEN","-t"]).split(/\r?\n/).map(r=>r.trim()).filter(Boolean);for(let r of s){let n=e("ps",["-p",r,"-o","command="]).trim(),[i]=c(`${r} ${n}`);if(i)return i}}catch{}return null}function f(o){try{return l(o)}catch{return p(o)}}function d(o,t){return o!==null&&f(o.root)!==f(t)}async function P(o,t,e,s=a){let r={running:[],foreign:[]};for(let n of o){if(!await e(n))continue;let i=s(n);i&&d(i,t)?r.foreign.push({port:n,root:i.root}):r.running.push(n)}return r}export{a,d as b,P as c};