@flisk-sh/flisk 1.4.6 → 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 (66) hide show
  1. package/README.md +2 -2
  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 +12 -0
  5. package/cli/dist/commands/logs.js +71 -4
  6. package/cli/dist/commands/project.js +16 -0
  7. package/cli/dist/commands/resource.js +3 -1
  8. package/cli/dist/commands/status.js +43 -24
  9. package/cli/dist/commands/ui.js +41 -2
  10. package/cli/dist/commands/up.js +54 -11
  11. package/cli/dist/components/DetailPanel.js +23 -3
  12. package/cli/dist/mcp/tools.js +9 -6
  13. package/cli/dist/types/index.js +0 -2
  14. package/cli/dist/utils/agent-format.js +18 -0
  15. package/cli/dist/utils/agent-sessions.js +122 -0
  16. package/cli/dist/utils/agent-skills.js +64 -0
  17. package/cli/dist/utils/cold-preflight.js +3 -0
  18. package/cli/dist/utils/doctor-fix-first.js +93 -0
  19. package/cli/dist/utils/doctor-report.js +3 -0
  20. package/cli/dist/utils/native-runtime.js +3 -0
  21. package/cli/dist/utils/native-service.js +2 -2
  22. package/cli/dist/utils/service-manifest.js +8 -15
  23. package/cli/dist/utils/services.js +7 -10
  24. package/cli/dist/utils/startup-report.js +45 -3
  25. package/cli/dist/utils/status-groups.js +23 -0
  26. package/cli/dist/utils/up-hints.js +42 -0
  27. package/cli/dist/utils/up-progress.js +117 -0
  28. package/cli/dist/utils/up-readiness.js +5 -0
  29. package/cli/dist-bundle/{chunk-U5FCQF42.js → chunk-6OW2GVQV.js} +1 -1
  30. package/cli/dist-bundle/chunk-B2JOM3HN.js +35 -0
  31. package/cli/dist-bundle/{chunk-R7DWXISM.js → chunk-F2DLIM75.js} +1 -1
  32. package/cli/dist-bundle/{chunk-ETXZ5LUG.js → chunk-L6O7TT3K.js} +10 -10
  33. package/cli/dist-bundle/chunk-MFY7RZ76.js +130 -0
  34. package/cli/dist-bundle/{chunk-MUDE4DZD.js → chunk-MYAYJF4Q.js} +1 -1
  35. package/cli/dist-bundle/{chunk-DPQK5TIS.js → chunk-P7GZ6NGM.js} +1 -1
  36. package/cli/dist-bundle/{chunk-IDNES5PE.js → chunk-T44DEEL6.js} +1 -1
  37. package/cli/dist-bundle/{chunk-C4UFBXRD.js → chunk-TIPSERWO.js} +1 -1
  38. package/cli/dist-bundle/{chunk-BIVAAWBX.js → chunk-VYPB7MY5.js} +1 -1
  39. package/cli/dist-bundle/{chunk-WCGBVETR.js → chunk-XHENT4KO.js} +1 -1
  40. package/cli/dist-bundle/{chunk-CAEMWNPF.js → chunk-YTBTCAWY.js} +1 -1
  41. package/cli/dist-bundle/cli.js +462 -377
  42. package/cli/dist-bundle/{cold-preflight-KUWOL2Q2.js → cold-preflight-DQVJUGBB.js} +1 -1
  43. package/cli/dist-bundle/{demo-stage-QCC4IWXR.js → demo-stage-F4YA7UL2.js} +1 -1
  44. package/cli/dist-bundle/{open-AZNX4AC5.js → open-ZUSSVPD3.js} +1 -1
  45. package/cli/dist-bundle/project-JFCTPRP5.js +2 -0
  46. package/cli/dist-bundle/{services-EY5WKPHQ.js → services-DOHDHOCT.js} +1 -1
  47. package/cli/dist-bundle/{tilt-port-owner-UBU6JWOA.js → tilt-port-owner-XODQHXHX.js} +1 -1
  48. package/cli/package.json +1 -1
  49. package/cli/templates/agent-skills/flisk-agent-ops/SKILL.md +39 -0
  50. package/cli/templates/agent-skills/flisk-build/SKILL.md +85 -0
  51. package/cli/templates/agent-skills/flisk-doctor/SKILL.md +28 -0
  52. package/cli/templates/agent-skills/flisk-recipe-ai-agent/SKILL.md +45 -0
  53. package/cli/templates/agent-skills/flisk-recipe-blog/SKILL.md +36 -0
  54. package/cli/templates/agent-skills/flisk-recipe-saas/SKILL.md +39 -0
  55. package/cli/templates/agent-skills/flisk-recipe-shop/SKILL.md +41 -0
  56. package/cli/templates/agent-skills/flisk-recipe-starters/SKILL.md +46 -0
  57. package/cli/templates/agent-skills/flisk-resource/SKILL.md +27 -0
  58. package/cli/templates/agent-skills/flisk-status/SKILL.md +25 -0
  59. package/cli/templates/agent-skills/flisk-troubleshoot/SKILL.md +227 -0
  60. package/cli/templates/agent-skills/flisk-up/SKILL.md +37 -0
  61. package/package.json +1 -1
  62. package/runtime/engine/schemas/service-schema.json +3 -15
  63. package/runtime/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +7 -1
  64. package/cli/dist-bundle/chunk-CDKEHFW6.js +0 -35
  65. package/cli/dist-bundle/chunk-GT76VMUL.js +0 -130
  66. package/cli/dist-bundle/project-5SWIGCNP.js +0 -2
@@ -2,13 +2,16 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { Command } from "commander";
4
4
  import { STANDARD_PORTS } from "../utils/constants.js";
5
+ import { getDeferredResourceNames } from "../utils/doctor-runtime.js";
5
6
  import { errorFactories, FliskError, runCommand, showErrorAndExit } from "../utils/errors.js";
6
7
  import { createMachineEnvelope } from "../utils/machine-output.js";
7
- import { readNativeLogLines } from "../utils/native-runtime.js";
8
+ import { nativeServiceStates, readNativeLogLines } from "../utils/native-runtime.js";
8
9
  import { isNativeProject } from "../utils/native-service.js";
9
10
  import { findProjectRoot } from "../utils/paths.js";
10
11
  import { EnvUnreadableError, envSecretValues, redactSecrets, redactValue, } from "../utils/secret-redaction.js";
12
+ import { deriveServiceStates } from "../utils/service-runtime-state.js";
11
13
  import { discoverResourcesFromRoot } from "../utils/services.js";
14
+ import { unhealthyServiceNames } from "../utils/status-groups.js";
12
15
  import { isTiltAvailable, runTilt } from "../utils/tilt.js";
13
16
  import { DEFAULT_LOG_TAIL, isTiltConnectionFailure, isValidPort, isValidSince, isValidTail, MAX_LOG_TAIL, parseTiltLogLines, sinceMilliseconds, } from "../utils/tilt-logs.js";
14
17
  import { tiltUnreachableMessage } from "../utils/tilt-unreachable-message.js";
@@ -28,6 +31,38 @@ async function tiltResourceNames(port) {
28
31
  return null;
29
32
  }
30
33
  }
34
+ /** Raw `tilt get uiresources` JSON, or null when Tilt cannot be read. */
35
+ async function tiltResourcesJson(port) {
36
+ try {
37
+ const result = await runTilt("get", ["uiresources", "-o", "json", "--port", port], {
38
+ inheritStdio: false,
39
+ });
40
+ return result.exitCode === 0 ? result.stdout : null;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ /** Failing or pending services among the project's, from the running Tilt; null when Tilt cannot be read. */
47
+ async function tiltUnhealthyNames(projectRoot, port) {
48
+ const text = await tiltResourcesJson(port);
49
+ if (!text)
50
+ return null;
51
+ let resources = [];
52
+ try {
53
+ resources = discoverResourcesFromRoot(projectRoot);
54
+ }
55
+ catch {
56
+ // Without discovery there are no dependsOn edges; Tilt's own states still decide.
57
+ }
58
+ try {
59
+ const states = deriveServiceStates(text, Object.fromEntries(resources.map((r) => [r.name, r.config?.dependsOn ?? []])), getDeferredResourceNames(resources));
60
+ return unhealthyServiceNames(Object.keys(states).map((name) => ({ name })), states);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
31
66
  /** The project's services when every one declares `command` (they run as local processes), else null. */
32
67
  function nativeResources(projectRoot) {
33
68
  try {
@@ -45,6 +80,7 @@ export const logsCommand = new Command("logs")
45
80
  .option("-s, --service <names...>", "Only these services (Tilt resource names)")
46
81
  .option("--tail <n>", "Number of most recent lines to return", String(DEFAULT_LOG_TAIL))
47
82
  .option("--since <duration>", "Only logs newer than this duration, for example 30s, 5m, 1h")
83
+ .option("--unhealthy", "Only services that are failing or still starting", false)
48
84
  .option("--port <n>", "Tilt UI port (default: TILT_PORT or 10350)")
49
85
  .option("--json", "Output one JSON object and exit", false)
50
86
  .action(async (options) => {
@@ -78,6 +114,19 @@ export const logsCommand = new Command("logs")
78
114
  fail("USAGE", "--port must be a valid port number", 2);
79
115
  const tail = Number(options.tail);
80
116
  const services = options.service ?? [];
117
+ const reportAllHealthy = () => {
118
+ if (options.json) {
119
+ console.log(JSON.stringify(createMachineEnvelope({
120
+ services,
121
+ tail,
122
+ since: options.since ?? null,
123
+ lines: [],
124
+ unhealthy: [],
125
+ })));
126
+ return;
127
+ }
128
+ console.log("All services are healthy; nothing to show with --unhealthy.");
129
+ };
81
130
  const action = async () => {
82
131
  const projectRoot = findProjectRoot();
83
132
  if (!projectRoot) {
@@ -104,7 +153,15 @@ export const logsCommand = new Command("logs")
104
153
  const error = errorFactories.unknownServices(unknown, known);
105
154
  fail("UNKNOWN_SERVICE", error.message, error.exitCode, error.suggestions);
106
155
  }
107
- const lines = readNativeLogLines(projectRoot, services.length > 0 ? services : known, {
156
+ let selected = services.length > 0 ? services : known;
157
+ if (options.unhealthy) {
158
+ const states = nativeServiceStates(projectRoot, known);
159
+ const bad = new Set(unhealthyServiceNames(resources, states));
160
+ selected = selected.filter((name) => bad.has(name));
161
+ if (selected.length === 0)
162
+ return reportAllHealthy();
163
+ }
164
+ const lines = readNativeLogLines(projectRoot, selected, {
108
165
  tail,
109
166
  sinceMs: options.since === undefined ? undefined : sinceMilliseconds(options.since),
110
167
  });
@@ -130,14 +187,24 @@ export const logsCommand = new Command("logs")
130
187
  fail("UNKNOWN_SERVICE", error.message, error.exitCode, suggestions);
131
188
  }
132
189
  }
190
+ let selectedServices = services;
191
+ if (options.unhealthy) {
192
+ const bad = await tiltUnhealthyNames(projectRoot, portText);
193
+ if (bad === null)
194
+ fail("TILT_NOT_RUNNING", tiltUnreachableMessage(portText));
195
+ selectedServices =
196
+ services.length > 0 ? services.filter((n) => bad?.includes(n)) : (bad ?? []);
197
+ if (selectedServices.length === 0)
198
+ return reportAllHealthy();
199
+ }
133
200
  const args = ["--port", portText, "--tail", String(tail)];
134
201
  if (options.since !== undefined)
135
202
  args.push("--since", options.since);
136
203
  if (options.json)
137
204
  args.push("--json");
138
205
  // `--` so a service name that starts with "-" is never read as a Tilt flag.
139
- if (services.length > 0)
140
- args.push("--", ...services);
206
+ if (selectedServices.length > 0)
207
+ args.push("--", ...selectedServices);
141
208
  const result = await runTilt("logs", args, { inheritStdio: false });
142
209
  if (result.exitCode !== 0) {
143
210
  const detail = result.stderr.trim();
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  import chalk from "chalk";
9
9
  import { Command } from "commander";
10
10
  import { generateMasterConfigs, readProjectConfig } from "../generator/template-engine.js";
11
+ import { installAgentSkills } from "../utils/agent-skills.js";
11
12
  import { addYesOption } from "../utils/command-helpers.js";
12
13
  import { MASTER_CONFIG_FILES, PROJECT_JSON } from "../utils/constants.js";
13
14
  import { clearDiscoveryCache } from "../utils/discovery-context.js";
@@ -233,6 +234,7 @@ export const projectCommand = addYesOption(new Command("project"))
233
234
  .option("--force", "Overwrite existing configuration (dangerous)")
234
235
  .option("--config-file <path>", "Load project config from existing JSON file")
235
236
  .option("-q, --quiet", "Only print what fails; used by `flisk demo`", false)
237
+ .option("--no-agent-skills", "Do not write the Flisk agent skills (.claude/skills, .agents/skills) and AGENTS.md")
236
238
  .action(async (template, options) => {
237
239
  const { assertMachineReadyOrExit, detectRuntimePrerequisites, formatRuntimeNextSteps } = await import("../utils/cold-preflight.js");
238
240
  // Project setup only writes files: it must not need a running Docker daemon
@@ -493,6 +495,20 @@ export const projectCommand = addYesOption(new Command("project"))
493
495
  if (ignored.length > 0 && !quiet) {
494
496
  showSuccess(`Added to .gitignore: ${ignored.join(", ")}`);
495
497
  }
498
+ // A coding agent opened in the project then knows to build and run the app with Flisk.
499
+ // The project works without them, so a failure here is a warning, not an exit.
500
+ if (options.agentSkills !== false) {
501
+ try {
502
+ const skillFiles = installAgentSkills(projectRoot);
503
+ if (skillFiles.length > 0 && !quiet) {
504
+ showSuccess("Added agent skills: .claude/skills, .agents/skills and AGENTS.md (start with flisk-build)");
505
+ }
506
+ }
507
+ catch (error) {
508
+ const reason = error instanceof Error ? error.message : String(error);
509
+ console.log(chalk.yellow(`⚠️ Could not add the agent skills: ${reason}`));
510
+ }
511
+ }
496
512
  if (!quiet) {
497
513
  console.log(chalk.green("\n✅ Project configuration complete!"));
498
514
  showDetail("\nGenerated files in .flisk/.flisk-out/:", 0);
@@ -231,7 +231,9 @@ export function createPackageJson(name, type, frameworkId, prismaEnabled = false
231
231
  // vitest 5 peers on vite ^6.4.0 || ^7 || ^8; vite 5 makes `vitest run` fail at startup with
232
232
  // ERR_PACKAGE_PATH_NOT_EXPORTED ('./module-runner'), in every resource once it is hoisted.
233
233
  ...(isFrontend && !framework?.scripts ? { vite: "^6.4.1" } : {}),
234
- ...(drizzleEnabled ? { "drizzle-kit": DRIZZLE_KIT_RANGE, "@types/pg": "^8.11.0" } : {}),
234
+ ...(drizzleEnabled ? { "drizzle-kit": DRIZZLE_KIT_RANGE } : {}),
235
+ // Both import 'pg' (src/db.ts, the agent's checkpoint store); without the types the image's `tsc` fails with TS7016.
236
+ ...(drizzleEnabled || isPostgresAgent ? { "@types/pg": "^8.11.0" } : {}),
235
237
  ...(framework?.devDependencies ?? {}),
236
238
  ...(backendFramework?.devDependencies ?? {}),
237
239
  },
@@ -13,6 +13,7 @@ import { isNativeProject } from "../utils/native-service.js";
13
13
  import { findProjectRoot } from "../utils/paths.js";
14
14
  import { deriveServiceStates } from "../utils/service-runtime-state.js";
15
15
  import { groupSharedPostgres, readProjectName } from "../utils/shared-infra.js";
16
+ import { groupServicesByStack, unhealthyServiceNames } from "../utils/status-groups.js";
16
17
  import { buildServicePorts, buildStackPorts } from "../utils/status-ports.js";
17
18
  import { isTiltAvailable, runTilt } from "../utils/tilt.js";
18
19
  import { getTiltPollingPort } from "../utils/tilt-startup.js";
@@ -24,6 +25,7 @@ export const statusCommand = new Command("status")
24
25
  .option("--stacks", "Show stack information (default)", true)
25
26
  .option("--resources", "Show all discovered resources", false)
26
27
  .option("--tilt", "Show tilt resource status", false)
28
+ .option("--unhealthy", "Show only services that are failing or still starting", false)
27
29
  .action(async (options) => {
28
30
  const action = async () => {
29
31
  // Discover first: outside a project this fails before any status line is printed.
@@ -155,9 +157,12 @@ export const statusCommand = new Command("status")
155
157
  name: stack.name,
156
158
  resourceCount: stack.resourceCount,
157
159
  resources: stack.resources.map((resource) => resource.name),
160
+ healthyCount: stack.resources.filter((resource) => serviceStates?.[resource.name]?.status === "ready").length,
158
161
  })),
159
162
  unassignedResources: discovery.unassignedResources.map((resource) => resource.name),
160
163
  sharedInfra: sharedInfraGroup,
164
+ // Names of the failing or pending services, so a caller need not filter `resources` itself.
165
+ unhealthy: unhealthyServiceNames(discovery.resources, serviceStates),
161
166
  };
162
167
  console.log(JSON.stringify(createMachineEnvelope(data, errors)));
163
168
  if (errors.length > 0) {
@@ -168,27 +173,29 @@ export const statusCommand = new Command("status")
168
173
  }
169
174
  console.log(chalk.bold("Resources:"), `${discovery.resources.length} discovered`);
170
175
  console.log(chalk.bold("Stacks:"), `${discovery.stacks.length} defined`);
171
- if (discovery.stacks.length > 0) {
176
+ const hasStates = serviceStates !== null && Object.keys(serviceStates).length > 0;
177
+ const unhealthyOnly = Boolean(options.unhealthy);
178
+ // With service states the grouped "Services:" section below replaces the plain per-stack counts.
179
+ if (!hasStates && !unhealthyOnly) {
172
180
  for (const stack of discovery.stacks) {
173
- const resourcesInStack = stack.resources.length;
174
- showDetail(`${stack.name}: ${formatCount(resourcesInStack, "resource")}`);
181
+ showDetail(`${stack.name}: ${formatCount(stack.resources.length, "resource")}`);
175
182
  if (options.verbose) {
176
183
  for (const resource of stack.resources) {
177
184
  showDetail(`${resource.name}`, 6);
178
185
  }
179
186
  }
180
187
  }
181
- }
182
- if (discovery.unassignedResources.length > 0) {
183
- console.log();
184
- console.log(chalk.yellow(`${formatCount(discovery.unassignedResources.length, "resource")} not in any stack:`));
185
- if (options.verbose) {
186
- for (const resource of discovery.unassignedResources) {
187
- showDetail(`${resource.name}`);
188
+ if (discovery.unassignedResources.length > 0) {
189
+ console.log();
190
+ console.log(chalk.yellow(`${formatCount(discovery.unassignedResources.length, "resource")} not in any stack:`));
191
+ if (options.verbose) {
192
+ for (const resource of discovery.unassignedResources) {
193
+ showDetail(`${resource.name}`);
194
+ }
188
195
  }
189
196
  }
190
197
  }
191
- if (sharedInfraGroup) {
198
+ if (sharedInfraGroup && !unhealthyOnly) {
192
199
  console.log();
193
200
  console.log(chalk.bold("Shared infrastructure:"));
194
201
  showDetail(`${sharedInfraGroup.name} (one instance, ${formatCount(sharedInfraGroup.databases.length, "database")})`);
@@ -196,21 +203,33 @@ export const statusCommand = new Command("status")
196
203
  showDetail(` ${database.name} (stack ${database.stack}): ${database.services.join(", ")}`);
197
204
  }
198
205
  }
199
- if (serviceStates && Object.keys(serviceStates).length > 0) {
206
+ if (hasStates && serviceStates) {
207
+ const states = serviceStates;
200
208
  console.log();
201
- console.log(chalk.bold("Services:"));
202
- for (const resource of discovery.resources) {
203
- const state = serviceStates[resource.name];
204
- if (!state)
209
+ console.log(chalk.bold(unhealthyOnly ? "Unhealthy services:" : "Services:"));
210
+ const unhealthy = new Set(unhealthyServiceNames(discovery.resources, states));
211
+ if (unhealthyOnly && unhealthy.size === 0) {
212
+ showDetail(chalk.green("All services are healthy."));
213
+ }
214
+ for (const group of groupServicesByStack(discovery.stacks, discovery.unassignedResources, states)) {
215
+ const shown = group.resources.filter((resource) => states[resource.name] && (!unhealthyOnly || unhealthy.has(resource.name)));
216
+ if (unhealthyOnly && shown.length === 0)
205
217
  continue;
206
- const mark = state.status === "ready"
207
- ? chalk.green("✓")
208
- : state.status === "error"
209
- ? chalk.red("✗")
210
- : chalk.yellow("…");
211
- const ports = buildServicePorts(resource, portPlan?.ingressHttp);
212
- const urlPart = ports.url ? ` → ${ports.url}` : "";
213
- showDetail(`${mark} ${resource.name}${urlPart} ${state.status}${state.reason ? ` (${state.reason})` : ""}`);
218
+ const summary = `${group.healthy}/${group.total} healthy`;
219
+ showDetail(`${chalk.bold(group.name)}: ${group.healthy === group.total ? chalk.green(summary) : chalk.yellow(summary)}`);
220
+ for (const resource of shown) {
221
+ const state = states[resource.name];
222
+ if (!state)
223
+ continue;
224
+ const mark = state.status === "ready"
225
+ ? chalk.green("✓")
226
+ : state.status === "error"
227
+ ? chalk.red("✗")
228
+ : chalk.yellow("…");
229
+ const ports = buildServicePorts(resource, portPlan?.ingressHttp);
230
+ const urlPart = ports.url ? ` → ${ports.url}` : "";
231
+ showDetail(`${mark} ${resource.name}${urlPart} ${state.status}${state.reason ? ` (${state.reason})` : ""}`, 6);
232
+ }
214
233
  }
215
234
  }
216
235
  else if (discovery.resources.length > 0) {
@@ -7,6 +7,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
7
  import { DetailPanel, FileTree, ResourceSelectInput, ResourceTable, ServiceIssues, TabBar, TUIHeader, } from "../components/index.js";
8
8
  import { TABS } from "../components/TabBar.js";
9
9
  import { createTUITheme, sectionTitle, TUIThemeContext, useTUITheme, } from "../components/ui-theme.js";
10
+ import { fetchAgentOverview } from "../utils/agent-sessions.js";
10
11
  import { errorFactories, requireProjectRoot } from "../utils/errors.js";
11
12
  import { readSavedHostPortPlan } from "../utils/host-port-config.js";
12
13
  import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
@@ -16,6 +17,7 @@ import { buildAdvertisedTargets } from "../utils/service-urls.js";
16
17
  import { clearMetadataCache, discoverResources, discoverStacks, getResourceMetadata, getStackMetadata, } from "../utils/services.js";
17
18
  import { singleFlight } from "../utils/single-flight.js";
18
19
  import { createStatusMessageController } from "../utils/status-message.js";
20
+ import { buildServicePorts } from "../utils/status-ports.js";
19
21
  import { getListRowFromMouseY, getTerminalRuleWidth } from "../utils/terminal-layout.js";
20
22
  import { isTiltAvailable } from "../utils/tilt.js";
21
23
  import { loadTiltEvents, stripTerminalControls, TiltEventsLoadError, } from "../utils/tilt-events.js";
@@ -134,12 +136,45 @@ export const TUIApp = ({ animated = true }) => {
134
136
  });
135
137
  const poll = () => run(() => fetchServiceStates(services, tiltPort));
136
138
  void poll();
137
- const timer = setInterval(() => void poll(), 5000);
139
+ // The agent panel rides on this same timer: the selected agent is read once per tick, never one loop per agent.
140
+ const timer = setInterval(() => {
141
+ void poll();
142
+ void agentPollRef.current();
143
+ }, 5000);
138
144
  return () => {
139
145
  cancelled = true;
140
146
  clearInterval(timer);
141
147
  };
142
148
  }, [services]);
149
+ // Readiness and sessions of the selected agent resource, read from the agent itself through its ingress route.
150
+ const [agentOverview, setAgentOverview] = useState(null);
151
+ const agentRoute = useMemo(() => {
152
+ const resource = services.find((s) => s.name === selectedService);
153
+ if (!resource || (resource.config?.appType ?? resource.type) !== "agent")
154
+ return null;
155
+ const url = buildServicePorts(resource, ingressPort).url;
156
+ return url ? { name: resource.name, url } : null;
157
+ }, [services, selectedService, ingressPort]);
158
+ const agentPollRef = useRef(async () => { });
159
+ useEffect(() => {
160
+ if (!agentRoute) {
161
+ agentPollRef.current = async () => { };
162
+ setAgentOverview(null);
163
+ return;
164
+ }
165
+ let cancelled = false;
166
+ const run = singleFlight((overview) => {
167
+ if (!cancelled)
168
+ setAgentOverview({ name: agentRoute.name, overview });
169
+ });
170
+ agentPollRef.current = () => run(() => fetchAgentOverview(agentRoute.url));
171
+ setAgentOverview(null);
172
+ void agentPollRef.current();
173
+ return () => {
174
+ cancelled = true;
175
+ agentPollRef.current = async () => { };
176
+ };
177
+ }, [agentRoute]);
143
178
  // Re-read service.json files from disk. An empty project is not an error:
144
179
  // it renders EmptyState. Only a failed discovery shows ErrorScreen.
145
180
  const refresh = useCallback(() => {
@@ -648,7 +683,11 @@ export const TUIApp = ({ animated = true }) => {
648
683
  ? theme.info
649
684
  : theme.success, children: [formatEventTime(event.occurredAt), " ", event.resourceName, " ", event.title] }), event.details && (_jsx(Text, { color: theme.muted, children: formatEventDetails(event.details, mainPanelWidth) }))] }, event.id))) })) : (_jsx(Text, { color: theme.muted, children: "No recent build events are available from Tilt yet." }))] }))] })), activeTab === "config" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.muted, children: sectionTitle(theme, "Configuration") }) }), selectedServiceData ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.accent, children: selectedServiceData.service.configPath }), _jsx(Box, { marginTop: 1, ...(theme.ascii
650
685
  ? {}
651
- : { borderStyle: "single", borderColor: theme.border }), paddingX: 1, children: _jsx(Text, { color: theme.muted, wrap: "wrap", children: JSON.stringify(selectedServiceData.service.config, null, 2).slice(0, 1000) }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: "Select a service to view configuration" }), _jsx(Box, { marginTop: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, isActive: !isSearching, maxVisibleItems: pageSize, width: mainPanelWidth, onLayout: handleListLayout }) })] }))] }))] }), showSidebar && (_jsx(Box, { marginLeft: 2, children: _jsx(DetailPanel, { stack: selectedStackData?.stack || null, service: selectedServiceData?.service || null, stackMetadata: selectedStackData?.metadata || null, visible: !!selectedStack || !!selectedService }) }))] }), _jsxs(Box, { ...(theme.ascii ? {} : { borderStyle: "single", borderColor: theme.border }), paddingX: 1, height: 3, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: theme.accent, bold: true, children: [theme.bannerStart, " ", activeTab] }), _jsxs(Text, { color: theme.success, children: [theme.ascii ? "[+]" : "\u25cf", " ", services.filter((s) => s.stack).length, " in stack"] }), _jsxs(Text, { color: theme.warning, children: [theme.ascii ? "-" : "\u25cb", " ", services.filter((s) => !s.stack).length, " no stack"] })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: theme.muted, children: ["Stacks: ", stacks.length] }), _jsxs(Text, { color: theme.muted, children: ["Services: ", services.length] }), _jsxs(Text, { color: theme.muted, children: [theme.ascii ? "Mouse" : "🖱️", " ", mouseEnabled ? "ON" : "OFF", theme.separator, theme.ascii ? "Info" : "ℹ️", " ", showTooltips ? "ON" : "OFF", theme.separator, _jsx(Text, { color: showEnabledOnly ? theme.success : theme.warning, children: showEnabledOnly
686
+ : { borderStyle: "single", borderColor: theme.border }), paddingX: 1, children: _jsx(Text, { color: theme.muted, wrap: "wrap", children: JSON.stringify(selectedServiceData.service.config, null, 2).slice(0, 1000) }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: "Select a service to view configuration" }), _jsx(Box, { marginTop: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, isActive: !isSearching, maxVisibleItems: pageSize, width: mainPanelWidth, onLayout: handleListLayout }) })] }))] }))] }), showSidebar && (_jsx(Box, { marginLeft: 2, children: _jsx(DetailPanel, { stack: selectedStackData?.stack || null, service: selectedServiceData?.service || null, stackMetadata: selectedStackData?.metadata || null, agent: agentRoute && selectedServiceData
687
+ ? agentOverview?.name === agentRoute.name
688
+ ? agentOverview.overview
689
+ : null
690
+ : undefined, visible: !!selectedStack || !!selectedService }) }))] }), _jsxs(Box, { ...(theme.ascii ? {} : { borderStyle: "single", borderColor: theme.border }), paddingX: 1, height: 3, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: theme.accent, bold: true, children: [theme.bannerStart, " ", activeTab] }), _jsxs(Text, { color: theme.success, children: [theme.ascii ? "[+]" : "\u25cf", " ", services.filter((s) => s.stack).length, " in stack"] }), _jsxs(Text, { color: theme.warning, children: [theme.ascii ? "-" : "\u25cb", " ", services.filter((s) => !s.stack).length, " no stack"] })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: theme.muted, children: ["Stacks: ", stacks.length] }), _jsxs(Text, { color: theme.muted, children: ["Services: ", services.length] }), _jsxs(Text, { color: theme.muted, children: [theme.ascii ? "Mouse" : "🖱️", " ", mouseEnabled ? "ON" : "OFF", theme.separator, theme.ascii ? "Info" : "ℹ️", " ", showTooltips ? "ON" : "OFF", theme.separator, _jsx(Text, { color: showEnabledOnly ? theme.success : theme.warning, children: showEnabledOnly
652
691
  ? theme.ascii
653
692
  ? "[+] enabled"
654
693
  : "\u2713 enabled"
@@ -12,7 +12,7 @@ import { STANDARD_PORTS } from "../utils/constants.js";
12
12
  import { getDeferredResourceNames, summarizeTiltBuildError } from "../utils/doctor-runtime.js";
13
13
  import { applyEnvFileSelection, assertEngineSupportsEnvFile, EnvFileError, envFileWarning, PROJECT_ENV_FILE_VAR, selectEnvFile, } from "../utils/env-selection.js";
14
14
  import { completeEnvFile } from "../utils/env-validator.js";
15
- import { errorFactories, FliskError, getErrorMessage, handleTiltFailure, requireProjectRoot, runCommand, showError, showErrorAndExit, withTiltCheck, } from "../utils/errors.js";
15
+ import { errorFactories, FliskError, getErrorMessage, requireProjectRoot, runCommand, showError, showErrorAndExit, withTiltCheck, } from "../utils/errors.js";
16
16
  import { ensureSmokeRecordsIgnored } from "../utils/file-helpers.js";
17
17
  import { evaluateFliskVersionFloor } from "../utils/flisk-version.js";
18
18
  import { formatCount } from "../utils/formatting.js";
@@ -34,13 +34,15 @@ import { enforceServiceConfigGate, warnSchemaVersions } from "../utils/service-c
34
34
  import { appendHealthPath, resolveServiceUrl, resolveSubdomainBases, } from "../utils/service-urls.js";
35
35
  import { discoverResourcesStrict, discoverResourcesWithProblems, discoverStacks, getResourcesForStack, stackExists, warnAboutSkippedFiles, } from "../utils/services.js";
36
36
  import { buildSmokePlans, formatSmokeFailure, runSmokePlans, safeSmokeDeps, } from "../utils/smoke.js";
37
- import { buildStartupReport, formatStartupReport, isStartupStalled, } from "../utils/startup-report.js";
37
+ import { buildStartupReport, formatStartupReport, isStartupStalled, machineProblemFixes, } from "../utils/startup-report.js";
38
38
  import { buildTiltUpArgs, runTilt } from "../utils/tilt.js";
39
39
  import { classifyTiltPorts } from "../utils/tilt-port-owner.js";
40
40
  import { stopTiltOnPort } from "../utils/tilt-process.js";
41
41
  import { parseTiltPort, resolveTiltPort, secondUpAction, stopTiltForUp, } from "../utils/tilt-startup.js";
42
+ import { upFailureHints } from "../utils/up-hints.js";
42
43
  import { findUnknownServices, resolveOnlySelection } from "../utils/up-only.js";
43
- import { tiltGetUiResources, waitForTiltResourcesReady } from "../utils/up-readiness.js";
44
+ import { createUpProgressReporter, fetchWithProgress, shouldRedrawUpProgress, } from "../utils/up-progress.js";
45
+ import { tiltGetUiResources, tiltResourceLogs, waitForTiltResourcesReady, } from "../utils/up-readiness.js";
44
46
  import { enableDiscoveredStacks, writeDefaultProjectConfig } from "./project.js";
45
47
  export function formatUpSuccess(port, options = {}) {
46
48
  const { appUrls = [], serviceHealth = [], showLogsHint = serviceHealth.length > 0 } = options;
@@ -318,8 +320,11 @@ export const upCommand = new Command("up")
318
320
  options.quiet = true;
319
321
  const portSetting = parseTiltPort(process.env.TILT_PORT);
320
322
  if (!portSetting.ok) {
321
- emit?.({ ok: false }, [{ code: "INVALID_TILT_PORT", message: portSetting.message }]);
322
- showErrorAndExit(portSetting.message);
323
+ const suggestions = upFailureHints({ kind: "invalid-tilt-port" });
324
+ emit?.({ ok: false }, [
325
+ { code: "INVALID_TILT_PORT", message: portSetting.message, suggestions },
326
+ ]);
327
+ showErrorAndExit(portSetting.message, 1, suggestions);
323
328
  }
324
329
  const configuredTiltPort = portSetting.port;
325
330
  // First, before the host/Docker checks below: an old CLI must stop with one clear line, not a half-working stack.
@@ -441,7 +446,7 @@ export const upCommand = new Command("up")
441
446
  }
442
447
  if (!options.dryRun) {
443
448
  const { assertMachineReadyOrExit } = await import("../utils/cold-preflight.js");
444
- await assertMachineReadyOrExit();
449
+ await assertMachineReadyOrExit({ hints: upFailureHints({ kind: "machine" }) });
445
450
  }
446
451
  const action = async () => {
447
452
  const foundRoot = options.dryRun ? requireProjectRoot() : findProjectRoot();
@@ -716,12 +721,16 @@ export const upCommand = new Command("up")
716
721
  let startedDependencies = dependencyNames;
717
722
  if (uiReady && emit) {
718
723
  const deferred = getDeferredResourceNames(discoveredResources);
724
+ const dependsOn = Object.fromEntries(discoveredResources.map((r) => [r.name, r.config?.dependsOn ?? []]));
719
725
  const readiness = await waitForTiltResourcesReady(port, {
720
726
  deferred,
721
727
  // Only what the caller named must be enabled. Dependencies are the Tiltfile's call (it skips some on purpose).
722
728
  expected: options.only
723
729
  ? options.only.filter((name) => !deferred.has(name))
724
730
  : undefined,
731
+ // As for the human output: stop as soon as everything left is blocked by a failure (for example init-networks),
732
+ // instead of waiting out the whole timeout.
733
+ isStalled: (text) => isStartupStalled(text, dependsOn, deferred),
725
734
  });
726
735
  if (readiness.ready) {
727
736
  jsonReady = true;
@@ -735,11 +744,21 @@ export const upCommand = new Command("up")
735
744
  }
736
745
  else {
737
746
  startupFailed = true;
738
- const detail = readiness.failures.map((f) => `${f.name}: ${f.message}`).join("; ");
747
+ const fixes = await machineProblemFixes(readiness.failures, (name) => tiltResourceLogs(port, name));
748
+ const detail = readiness.failures
749
+ .map((f) => {
750
+ const fix = fixes.get(f.name);
751
+ return `${f.name}: ${f.message}${fix ? `. ${fix}` : ""}`;
752
+ })
753
+ .join("; ");
739
754
  emit({ ok: false, tiltUrl: `http://localhost:${port}`, failures: readiness.failures }, [
740
755
  {
741
756
  code: readiness.timedOut ? "UP_TIMEOUT" : "UP_RESOURCE_FAILED",
742
757
  message: `${readiness.timedOut ? "Timed out waiting for resources" : "Resources failed"}${detail ? ` (${detail})` : ""}. Tilt is still running; inspect it or run: flisk down`,
758
+ suggestions: upFailureHints({
759
+ kind: "services",
760
+ failed: readiness.failures.map((failure) => failure.name),
761
+ }),
743
762
  },
744
763
  ]);
745
764
  }
@@ -749,10 +768,22 @@ export const upCommand = new Command("up")
749
768
  if (uiReady && !emit) {
750
769
  const deferred = getDeferredResourceNames(discoveredResources);
751
770
  const dependsOn = Object.fromEntries(discoveredResources.map((r) => [r.name, r.config?.dependsOn ?? []]));
771
+ const progress = createUpProgressReporter({
772
+ write: (text) => process.stdout.write(text),
773
+ redraw: shouldRedrawUpProgress({
774
+ isTTY: process.stdout.isTTY,
775
+ quiet: Boolean(options.quiet),
776
+ env: process.env,
777
+ }),
778
+ deferred,
779
+ columns: process.stdout.columns,
780
+ });
752
781
  const readiness = await waitForTiltResourcesReady(port, {
753
782
  deferred,
754
783
  isStalled: (text) => isStartupStalled(text, dependsOn, deferred),
784
+ fetchJson: fetchWithProgress(tiltGetUiResources, progress),
755
785
  });
786
+ progress.finish();
756
787
  if (!readiness.ready) {
757
788
  startupFailed = true;
758
789
  const resourcesJson = await tiltGetUiResources(port);
@@ -766,9 +797,13 @@ export const upCommand = new Command("up")
766
797
  blocked: [],
767
798
  starting: [],
768
799
  };
769
- const reportLines = formatStartupReport(report, readiness.timedOut, (port) => findPortOccupant(port));
800
+ const machineFixes = await machineProblemFixes(report.failed, (name) => tiltResourceLogs(port, name));
801
+ const reportLines = formatStartupReport(report, readiness.timedOut, (port) => findPortOccupant(port), machineFixes);
770
802
  if (reportLines.length > 0) {
771
- showError(reportLines.join("\n"));
803
+ showError(reportLines.join("\n"), undefined, upFailureHints({
804
+ kind: "services",
805
+ failed: report.failed.map((failure) => failure.name),
806
+ }));
772
807
  }
773
808
  }
774
809
  }
@@ -814,6 +849,7 @@ export const upCommand = new Command("up")
814
849
  envFiles: envFileSelection ? [envFileSelection.path] : [],
815
850
  }),
816
851
  });
852
+ const smokeFailures = [];
817
853
  for (const smokeResult of results) {
818
854
  if (smokeResult.ok) {
819
855
  if (!options.quiet)
@@ -821,13 +857,20 @@ export const upCommand = new Command("up")
821
857
  }
822
858
  else {
823
859
  smokeFailed = true;
860
+ smokeFailures.push(smokeResult.name);
824
861
  showError(formatSmokeFailure(smokeResult));
825
862
  }
826
863
  }
827
864
  if (smokeFailed) {
828
865
  stopTiltOnPort(port);
866
+ const suggestions = upFailureHints({ kind: "smoke", failed: smokeFailures });
867
+ showError("A smoke check failed; Tilt was stopped", undefined, suggestions);
829
868
  emit?.({ ok: false }, [
830
- { code: "UP_SMOKE_FAILED", message: "A smoke check failed; Tilt was stopped" },
869
+ {
870
+ code: "UP_SMOKE_FAILED",
871
+ message: "A smoke check failed; Tilt was stopped",
872
+ suggestions,
873
+ },
831
874
  ]);
832
875
  }
833
876
  }
@@ -849,7 +892,7 @@ export const upCommand = new Command("up")
849
892
  process.exit(1);
850
893
  }
851
894
  if (result.exitCode !== 0) {
852
- handleTiltFailure("up", result.exitCode);
895
+ showErrorAndExit(`tilt up failed with exit code ${result.exitCode}`, result.exitCode, upFailureHints({ kind: "tilt-exited" }));
853
896
  }
854
897
  if (startupFailed && result.exitCode === 0) {
855
898
  process.exit(1);
@@ -1,10 +1,30 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Copyright (c) 2026 Flisk Landscape contributors
3
3
  // SPDX-License-Identifier: MIT
4
4
  import { Box, Text } from "ink";
5
+ import { formatAge } from "../utils/agent-format.js";
5
6
  import { formatDate, getStatusColor, getStatusIcon } from "../utils/formatting.js";
6
7
  import { getTUIStatusColor, getTUIStatusIcon, useTUITheme } from "./ui-theme.js";
7
- export const DetailPanel = ({ stack, service, stackMetadata, visible, }) => {
8
+ const MAX_LISTED_SESSIONS = 5;
9
+ /** Readiness, session count, last activity and the newest sessions of an agent; `null` while the first poll is pending. */
10
+ const AgentSection = ({ agent }) => {
11
+ const theme = useTUITheme();
12
+ const row = (label, value, color = theme.foreground) => (_jsxs(Box, { children: [_jsxs(Text, { color: theme.muted, children: [label, ": "] }), _jsx(Text, { color: color, children: value })] }));
13
+ let body;
14
+ if (agent === null) {
15
+ body = row("Agent", "checking...", theme.muted);
16
+ }
17
+ else if (agent.state === "not-running") {
18
+ body = row("Agent", "not running", theme.warning);
19
+ }
20
+ else {
21
+ const { sessions } = agent;
22
+ const newest = sessions?.sessions[0]?.lastActivity ?? null;
23
+ body = (_jsxs(_Fragment, { children: [row("Ready", agent.ready ? "yes" : "no", agent.ready ? theme.success : theme.warning), sessions ? (_jsxs(_Fragment, { children: [row("Sessions", String(sessions.total)), row("Last activity", formatAge(newest)), sessions.sessions.slice(0, MAX_LISTED_SESSIONS).map((s) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.foreground, children: s.sessionId.slice(0, 18) }), _jsxs(Text, { color: theme.muted, children: [" ", formatAge(s.lastActivity), " ", s.status] })] }, s.sessionId))), sessions.total > MAX_LISTED_SESSIONS && (_jsxs(Text, { color: theme.muted, children: ["... ", sessions.total - MAX_LISTED_SESSIONS, " more"] }))] })) : (row("Sessions", "not available (agent has no /sessions)", theme.muted))] }));
24
+ }
25
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.muted, underline: true, children: "Agent" }), body] }));
26
+ };
27
+ export const DetailPanel = ({ stack, service, stackMetadata, agent, visible, }) => {
8
28
  const theme = useTUITheme();
9
29
  if (!visible) {
10
30
  return _jsx(Box, { width: 0 });
@@ -13,7 +33,7 @@ export const DetailPanel = ({ stack, service, stackMetadata, visible, }) => {
13
33
  const title = theme.ascii
14
34
  ? `--- ${service.name.toUpperCase()} ---`
15
35
  : `\u250C\u2500 ${service.name.toUpperCase()} \u2500\u2510`;
16
- return (_jsxs(Box, { width: 40, flexDirection: "column", borderStyle: theme.ascii ? undefined : "single", borderColor: theme.border, paddingX: 1, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, justifyContent: "center", children: _jsx(Text, { color: theme.accent, bold: true, children: title }) }), _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "Name: " }), _jsx(Text, { color: theme.foreground, children: service.name })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "Stack: " }), _jsx(Text, { color: theme.foreground, children: service.stack || "unknown" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "Type: " }), _jsx(Text, { color: theme.warning, bold: theme.highContrast, children: (service.stack || "") === "platform" ? "PLATFORM" : "PRODUCT" })] })] }), _jsx(Box, { marginTop: 2, children: _jsxs(Text, { color: theme.muted, dimColor: theme.dimMuted, children: ["Press ", _jsx(Text, { color: theme.accent, children: "[Esc]" }), " to close"] }) })] }));
36
+ return (_jsxs(Box, { width: 40, flexDirection: "column", borderStyle: theme.ascii ? undefined : "single", borderColor: theme.border, paddingX: 1, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, justifyContent: "center", children: _jsx(Text, { color: theme.accent, bold: true, children: title }) }), _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "Name: " }), _jsx(Text, { color: theme.foreground, children: service.name })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "Stack: " }), _jsx(Text, { color: theme.foreground, children: service.stack || "unknown" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "Type: " }), _jsx(Text, { color: theme.warning, bold: theme.highContrast, children: (service.stack || "") === "platform" ? "PLATFORM" : "PRODUCT" })] })] }), agent !== undefined && _jsx(AgentSection, { agent: agent }), _jsx(Box, { marginTop: 2, children: _jsxs(Text, { color: theme.muted, dimColor: theme.dimMuted, children: ["Press ", _jsx(Text, { color: theme.accent, children: "[Esc]" }), " to close"] }) })] }));
17
37
  }
18
38
  if (stack && stackMetadata) {
19
39
  const statusColor = getTUIStatusColor(theme, getStatusColor(stackMetadata.overallStatus));
@@ -328,23 +328,26 @@ export function createFliskTools(run = runFliskCli, up = defaultUpDeps) {
328
328
  },
329
329
  {
330
330
  name: "get_agent_sessions",
331
- description: "Inspect session checkpoints from a running AI agent resource. Set projectPath when the MCP server is not running from the project root.",
331
+ description: "List the sessions of a running AI agent resource (id, last activity, status, checkpoint size), or read one session's checkpoint when sessionId is set. Set projectPath when the MCP server is not running from the project root.",
332
332
  inputSchema: {
333
333
  ...OBJECT({
334
334
  projectPath: PROJECT_PATH,
335
335
  resource: { type: "string", description: "Name of the agent resource" },
336
- sessionId: { type: "string", description: "Session ID to fetch (required)" },
336
+ sessionId: {
337
+ type: "string",
338
+ description: "Session ID to read; omit it to list all sessions",
339
+ },
337
340
  }),
338
- required: ["resource", "sessionId"],
341
+ required: ["resource"],
339
342
  },
340
343
  handler: viaCli((args) => {
341
344
  const resource = optionalName(args.resource, "resource");
342
345
  if (!resource)
343
346
  throw new Error("resource is required");
344
347
  const sessionId = optionalName(args.sessionId, "sessionId");
345
- if (!sessionId)
346
- throw new Error("sessionId is required");
347
- return ["agent", "sessions", resource, `--session-id=${sessionId}`, "--json"];
348
+ return sessionId
349
+ ? ["agent", "sessions", resource, `--session-id=${sessionId}`, "--json"]
350
+ : ["agent", "sessions", resource, "--json"];
348
351
  }),
349
352
  },
350
353
  ];
@@ -1,5 +1,3 @@
1
- // Copyright (c) 2026 Flisk Landscape contributors
2
- // SPDX-License-Identifier: MIT
3
1
  import { RESOURCE_DEFAULTS_FILE, TECH_STACK_FILE } from "../utils/constants.js";
4
2
  export const RESOURCE_CONFIG_APP_TYPES = [
5
3
  "backend",