@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
@@ -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";
@@ -20,6 +20,9 @@ import { exportHostPortPlan, getHostPortPlan, writeSavedHostPortPlan, } from "..
20
20
  import { formatHostPortPlan } from "../utils/host-port-plan.js";
21
21
  import { createJsonEmitter } from "../utils/json-output.js";
22
22
  import { toMachineError } from "../utils/machine-output.js";
23
+ import { hasNearbyServiceJson } from "../utils/native-project.js";
24
+ import { formatMixedRuntimeRefusal, isNativeProject, isNativeService, resolveRuntimeMode, } from "../utils/native-service.js";
25
+ import { runNativeUp } from "../utils/native-up.js";
23
26
  import { nativeWindowsStackRefusal } from "../utils/native-windows.js";
24
27
  import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
25
28
  import { findAvailablePort } from "../utils/port-assignment.js";
@@ -31,14 +34,16 @@ import { enforceServiceConfigGate, warnSchemaVersions } from "../utils/service-c
31
34
  import { appendHealthPath, resolveServiceUrl, resolveSubdomainBases, } from "../utils/service-urls.js";
32
35
  import { discoverResourcesStrict, discoverResourcesWithProblems, discoverStacks, getResourcesForStack, stackExists, warnAboutSkippedFiles, } from "../utils/services.js";
33
36
  import { buildSmokePlans, formatSmokeFailure, runSmokePlans, safeSmokeDeps, } from "../utils/smoke.js";
34
- import { buildStartupReport, formatStartupReport, isStartupStalled, } from "../utils/startup-report.js";
37
+ import { buildStartupReport, formatStartupReport, isStartupStalled, machineProblemFixes, } from "../utils/startup-report.js";
35
38
  import { buildTiltUpArgs, runTilt } from "../utils/tilt.js";
36
39
  import { classifyTiltPorts } from "../utils/tilt-port-owner.js";
37
40
  import { stopTiltOnPort } from "../utils/tilt-process.js";
38
41
  import { parseTiltPort, resolveTiltPort, secondUpAction, stopTiltForUp, } from "../utils/tilt-startup.js";
42
+ import { upFailureHints } from "../utils/up-hints.js";
39
43
  import { findUnknownServices, resolveOnlySelection } from "../utils/up-only.js";
40
- import { tiltGetUiResources, waitForTiltResourcesReady } from "../utils/up-readiness.js";
41
- import { enableDiscoveredStacks } from "./project.js";
44
+ import { createUpProgressReporter, fetchWithProgress, shouldRedrawUpProgress, } from "../utils/up-progress.js";
45
+ import { tiltGetUiResources, tiltResourceLogs, waitForTiltResourcesReady, } from "../utils/up-readiness.js";
46
+ import { enableDiscoveredStacks, writeDefaultProjectConfig } from "./project.js";
42
47
  export function formatUpSuccess(port, options = {}) {
43
48
  const { appUrls = [], serviceHealth = [], showLogsHint = serviceHealth.length > 0 } = options;
44
49
  const lines = [];
@@ -235,6 +240,67 @@ function envFileFailure(error) {
235
240
  exitCode: error instanceof FliskError ? error.exitCode : 2,
236
241
  };
237
242
  }
243
+ /**
244
+ * Decides whether `flisk up` runs the selection as local processes (#2229). Returns null for today's Docker + Tilt path, which
245
+ * a project without any `command` service always takes. Outside a project, a directory whose services all declare `command`
246
+ * becomes the project. Refuses (through `fail`) an unknown stack or service, an invalid service.json, and a run that would mix
247
+ * native and container services.
248
+ */
249
+ export function selectNativeRun(stackName, only, fail, cwd = process.cwd()) {
250
+ const foundRoot = findProjectRoot(cwd);
251
+ if (!foundRoot && !hasNearbyServiceJson(cwd))
252
+ return null;
253
+ const projectRoot = foundRoot ?? cwd;
254
+ let discovered;
255
+ try {
256
+ discovered = discoverResourcesWithProblems(projectRoot);
257
+ }
258
+ catch {
259
+ return null;
260
+ }
261
+ const all = discovered.resources;
262
+ if (!all.some(isNativeService))
263
+ return null;
264
+ if (!foundRoot && !isNativeProject(all))
265
+ return null;
266
+ if (discovered.problems.length > 0) {
267
+ fail("INVALID_SERVICE_CONFIG", discovered.problems
268
+ .map((problem) => `Invalid service.json at ${problem.path}: ${problem.reason}`)
269
+ .join("\n"));
270
+ }
271
+ let requested = all;
272
+ if (stackName) {
273
+ if (!stackExists(stackName, all)) {
274
+ const error = errorFactories.stackNotFound(stackName, discoverStacks(all).map((s) => s.name));
275
+ fail("STACK_NOT_FOUND", error.message, error.suggestions);
276
+ }
277
+ requested = getResourcesForStack(stackName, all);
278
+ }
279
+ if (only) {
280
+ const unknown = findUnknownServices(only, requested);
281
+ if (unknown.length > 0) {
282
+ const error = errorFactories.unknownServices(unknown, requested.map((service) => service.name));
283
+ fail("UNKNOWN_SERVICE", error.message, error.suggestions);
284
+ }
285
+ }
286
+ // A process cannot start before what it depends on, so a stack or --only selection always brings its dependsOn closure.
287
+ const names = only ?? requested.map((service) => service.name);
288
+ const selection = resolveOnlySelection(names, all);
289
+ const { mode, mixed } = resolveRuntimeMode(selection.selected, all);
290
+ if (mode === "container") {
291
+ const containerNames = (selection.selected.some(isNativeService) ? selection.selected : all)
292
+ .filter((service) => !isNativeService(service))
293
+ .map((service) => service.name);
294
+ fail("MIXED_RUNTIME", formatMixedRuntimeRefusal(mixed, containerNames));
295
+ }
296
+ return {
297
+ projectRoot,
298
+ isNewProject: !foundRoot,
299
+ all,
300
+ selected: selection.selected,
301
+ dependencies: only ? selection.dependencies : [],
302
+ };
303
+ }
238
304
  export const upCommand = new Command("up")
239
305
  .description("Start all services (optionally filtered by stack)")
240
306
  .alias("deploy")
@@ -254,8 +320,11 @@ export const upCommand = new Command("up")
254
320
  options.quiet = true;
255
321
  const portSetting = parseTiltPort(process.env.TILT_PORT);
256
322
  if (!portSetting.ok) {
257
- emit?.({ ok: false }, [{ code: "INVALID_TILT_PORT", message: portSetting.message }]);
258
- 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);
259
328
  }
260
329
  const configuredTiltPort = portSetting.port;
261
330
  // First, before the host/Docker checks below: an old CLI must stop with one clear line, not a half-working stack.
@@ -266,6 +335,60 @@ export const upCommand = new Command("up")
266
335
  onFail: (message) => emit?.({ ok: false }, [{ code: "FLISK_VERSION_TOO_OLD", message }]),
267
336
  });
268
337
  }
338
+ // Every selected service declares `command`: run them as local processes, with no Docker, Compose, Tilt or generated
339
+ // files (#2229). Decided before any Docker or Tilt check; a project without `command` services never enters this branch.
340
+ const nativeRun = selectNativeRun(stackName, options.only, (code, message, suggestions) => {
341
+ emit?.({ ok: false }, [
342
+ { code, message, ...(suggestions && suggestions.length > 0 ? { suggestions } : {}) },
343
+ ]);
344
+ return showErrorAndExit(message, 1, suggestions);
345
+ });
346
+ if (nativeRun) {
347
+ const platformRefusal = nativeWindowsUpRefusal(process.platform, process.env.FLISK_ALLOW_NATIVE_WINDOWS);
348
+ if (platformRefusal)
349
+ showErrorAndExit(platformRefusal);
350
+ const { projectRoot } = nativeRun;
351
+ let envFilePath;
352
+ if (options.envFile !== undefined) {
353
+ try {
354
+ envFilePath = selectEnvFile(projectRoot, options.envFile).path;
355
+ }
356
+ catch (error) {
357
+ const { failure, exitCode } = envFileFailure(error);
358
+ emit?.({ ok: false }, [failure]);
359
+ showErrorAndExit(failure.message, exitCode, failure.suggestions);
360
+ }
361
+ }
362
+ if (!nativeRun.isNewProject) {
363
+ enforceSchemaVersionGate(projectRoot, {
364
+ resources: nativeRun.all,
365
+ onInvalid: (message) => emit?.({ ok: false }, [{ code: "INVALID_SCHEMA_VERSION", message }]),
366
+ });
367
+ }
368
+ enforceServiceConfigGate(projectRoot, {
369
+ resources: nativeRun.all,
370
+ onInvalid: (message) => emit?.({ ok: false }, [{ code: "INVALID_SERVICE_CONFIG", message }]),
371
+ });
372
+ if (nativeRun.isNewProject && !options.dryRun) {
373
+ writeDefaultProjectConfig(projectRoot);
374
+ if (!options.quiet) {
375
+ console.log(chalk.gray("Created .flisk/project.json (this directory is now a Flisk project)"));
376
+ }
377
+ }
378
+ await runNativeUp({
379
+ projectRoot,
380
+ selected: nativeRun.selected,
381
+ dependencies: nativeRun.dependencies,
382
+ stackName,
383
+ only: options.only,
384
+ envFilePath,
385
+ dryRun: options.dryRun,
386
+ quiet: options.quiet,
387
+ force: options.force,
388
+ emit,
389
+ });
390
+ return;
391
+ }
269
392
  // The env file of the run. `--env-file` replaces the project .env for this run, so the file is
270
393
  // resolved and read before any prerequisite check, any write and any service starts.
271
394
  const envFileRoot = versionRoot;
@@ -323,7 +446,7 @@ export const upCommand = new Command("up")
323
446
  }
324
447
  if (!options.dryRun) {
325
448
  const { assertMachineReadyOrExit } = await import("../utils/cold-preflight.js");
326
- await assertMachineReadyOrExit();
449
+ await assertMachineReadyOrExit({ hints: upFailureHints({ kind: "machine" }) });
327
450
  }
328
451
  const action = async () => {
329
452
  const foundRoot = options.dryRun ? requireProjectRoot() : findProjectRoot();
@@ -598,12 +721,16 @@ export const upCommand = new Command("up")
598
721
  let startedDependencies = dependencyNames;
599
722
  if (uiReady && emit) {
600
723
  const deferred = getDeferredResourceNames(discoveredResources);
724
+ const dependsOn = Object.fromEntries(discoveredResources.map((r) => [r.name, r.config?.dependsOn ?? []]));
601
725
  const readiness = await waitForTiltResourcesReady(port, {
602
726
  deferred,
603
727
  // Only what the caller named must be enabled. Dependencies are the Tiltfile's call (it skips some on purpose).
604
728
  expected: options.only
605
729
  ? options.only.filter((name) => !deferred.has(name))
606
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),
607
734
  });
608
735
  if (readiness.ready) {
609
736
  jsonReady = true;
@@ -617,11 +744,21 @@ export const upCommand = new Command("up")
617
744
  }
618
745
  else {
619
746
  startupFailed = true;
620
- 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("; ");
621
754
  emit({ ok: false, tiltUrl: `http://localhost:${port}`, failures: readiness.failures }, [
622
755
  {
623
756
  code: readiness.timedOut ? "UP_TIMEOUT" : "UP_RESOURCE_FAILED",
624
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
+ }),
625
762
  },
626
763
  ]);
627
764
  }
@@ -631,10 +768,22 @@ export const upCommand = new Command("up")
631
768
  if (uiReady && !emit) {
632
769
  const deferred = getDeferredResourceNames(discoveredResources);
633
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
+ });
634
781
  const readiness = await waitForTiltResourcesReady(port, {
635
782
  deferred,
636
783
  isStalled: (text) => isStartupStalled(text, dependsOn, deferred),
784
+ fetchJson: fetchWithProgress(tiltGetUiResources, progress),
637
785
  });
786
+ progress.finish();
638
787
  if (!readiness.ready) {
639
788
  startupFailed = true;
640
789
  const resourcesJson = await tiltGetUiResources(port);
@@ -648,9 +797,13 @@ export const upCommand = new Command("up")
648
797
  blocked: [],
649
798
  starting: [],
650
799
  };
651
- 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);
652
802
  if (reportLines.length > 0) {
653
- showError(reportLines.join("\n"));
803
+ showError(reportLines.join("\n"), undefined, upFailureHints({
804
+ kind: "services",
805
+ failed: report.failed.map((failure) => failure.name),
806
+ }));
654
807
  }
655
808
  }
656
809
  }
@@ -696,6 +849,7 @@ export const upCommand = new Command("up")
696
849
  envFiles: envFileSelection ? [envFileSelection.path] : [],
697
850
  }),
698
851
  });
852
+ const smokeFailures = [];
699
853
  for (const smokeResult of results) {
700
854
  if (smokeResult.ok) {
701
855
  if (!options.quiet)
@@ -703,13 +857,20 @@ export const upCommand = new Command("up")
703
857
  }
704
858
  else {
705
859
  smokeFailed = true;
860
+ smokeFailures.push(smokeResult.name);
706
861
  showError(formatSmokeFailure(smokeResult));
707
862
  }
708
863
  }
709
864
  if (smokeFailed) {
710
865
  stopTiltOnPort(port);
866
+ const suggestions = upFailureHints({ kind: "smoke", failed: smokeFailures });
867
+ showError("A smoke check failed; Tilt was stopped", undefined, suggestions);
711
868
  emit?.({ ok: false }, [
712
- { 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
+ },
713
874
  ]);
714
875
  }
715
876
  }
@@ -731,7 +892,7 @@ export const upCommand = new Command("up")
731
892
  process.exit(1);
732
893
  }
733
894
  if (result.exitCode !== 0) {
734
- handleTiltFailure("up", result.exitCode);
895
+ showErrorAndExit(`tilt up failed with exit code ${result.exitCode}`, result.exitCode, upFailureHints({ kind: "tilt-exited" }));
735
896
  }
736
897
  if (startupFailed && result.exitCode === 0) {
737
898
  process.exit(1);
@@ -140,7 +140,7 @@ export function detectAllInstallations() {
140
140
  return [...unique.values()];
141
141
  }
142
142
  const BINARY_RELEASE_REPO = "flisk-sh/releases";
143
- const ENGINE_ASSET_NAME = "tdk-cli-engine.tar.gz";
143
+ const ENGINE_ASSET_NAME = "flisk-cli-engine.tar.gz";
144
144
  const CHECKSUMS_ASSET_NAME = "checksums.txt";
145
145
  /** Parses `shasum -a 256` / `sha256sum` output ("<hash> <name>" or "<hash> *<name>"). */
146
146
  /** @internal Exported for unit tests only; not part of the package API. */
@@ -656,7 +656,7 @@ export function createUpgradeCommand() {
656
656
  showErrorAndExit("Flisk Windows v1 supports AMD64 only. Use WSL2 Ubuntu or a 64-bit Intel/AMD PC.");
657
657
  }
658
658
  if (process.platform === "win32") {
659
- showErrorAndExit("No Windows AMD64 binary in this Flisk release. Need asset tdk-windows-amd64.exe.");
659
+ showErrorAndExit("No Windows AMD64 binary in this Flisk release. Need asset flisk-windows-amd64.exe.");
660
660
  }
661
661
  showErrorAndExit(`Unsupported OS: Flisk does not support this OS: ${process.platform}. Supported: linux, darwin, win32.`);
662
662
  }
@@ -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",
@@ -0,0 +1,18 @@
1
+ // Copyright (c) 2026 Flisk Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ /** "12s ago", "5m ago", "3h ago", "2d ago"; "-" when the agent sent no time. */
4
+ export function formatAge(iso, now = Date.now()) {
5
+ if (!iso)
6
+ return "-";
7
+ const at = Date.parse(iso);
8
+ if (Number.isNaN(at))
9
+ return iso;
10
+ const seconds = Math.max(0, Math.round((now - at) / 1000));
11
+ if (seconds < 60)
12
+ return `${seconds}s ago`;
13
+ if (seconds < 3600)
14
+ return `${Math.floor(seconds / 60)}m ago`;
15
+ if (seconds < 86_400)
16
+ return `${Math.floor(seconds / 3600)}h ago`;
17
+ return `${Math.floor(seconds / 86_400)}d ago`;
18
+ }
@@ -0,0 +1,122 @@
1
+ // Copyright (c) 2026 Flisk Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ import * as http from "node:http";
4
+ export const AGENT_REQUEST_TIMEOUT_MS = 10_000;
5
+ /** GET with node:http because fetch silently drops a custom Host header, which is what Traefik routes on. */
6
+ export function httpGet(url, headers, timeoutMs) {
7
+ return new Promise((resolve, reject) => {
8
+ // One signal covers the connection and the body read, so a hung agent cannot block the CLI.
9
+ const req = http.get(url, { headers, signal: AbortSignal.timeout(timeoutMs) }, (res) => {
10
+ const chunks = [];
11
+ res.on("data", (chunk) => chunks.push(chunk));
12
+ res.on("end", () => {
13
+ const status = res.statusCode ?? 0;
14
+ resolve({
15
+ ok: status >= 200 && status < 300,
16
+ status,
17
+ text: Buffer.concat(chunks).toString("utf-8"),
18
+ });
19
+ });
20
+ res.on("error", reject);
21
+ });
22
+ req.on("error", reject);
23
+ });
24
+ }
25
+ /**
26
+ * Agent containers publish no host port; the only way in is the Traefik route that `flisk status` prints as `url`
27
+ * (for example http://api.<project>.localhost:8080/api/bot). `*.localhost` does not resolve everywhere (glibc without
28
+ * systemd-resolved), so connect to loopback and keep the route's host in the Host header, which is all Traefik matches on.
29
+ */
30
+ export function agentRequest(routeUrl, path, headers = {}) {
31
+ const route = new URL(routeUrl);
32
+ const allHeaders = { ...headers };
33
+ if (route.hostname.endsWith(".localhost")) {
34
+ allHeaders.host = route.host;
35
+ route.hostname = "127.0.0.1";
36
+ }
37
+ return { url: `${route.origin}${route.pathname.replace(/\/$/, "")}${path}`, headers: allHeaders };
38
+ }
39
+ /** The request for one session's checkpoint: `GET /session`, keyed by the `x-session-id` header. */
40
+ export function agentSessionRequest(routeUrl, sessionId) {
41
+ return agentRequest(routeUrl, "/session", { "x-session-id": sessionId });
42
+ }
43
+ /** The request for the session list: `GET /sessions`, which needs no session id. */
44
+ export function agentSessionListRequest(routeUrl) {
45
+ return agentRequest(routeUrl, "/sessions");
46
+ }
47
+ export async function fetchAgent(request, timeoutMs = AGENT_REQUEST_TIMEOUT_MS) {
48
+ let reply;
49
+ try {
50
+ reply = await httpGet(request.url, request.headers, timeoutMs);
51
+ }
52
+ catch {
53
+ return { kind: "not-running" };
54
+ }
55
+ let body = {};
56
+ let isJson = true;
57
+ try {
58
+ body = reply.text ? JSON.parse(reply.text) : {};
59
+ }
60
+ catch {
61
+ body = { raw: reply.text };
62
+ isJson = false;
63
+ }
64
+ if (reply.ok)
65
+ return { kind: "ok", body };
66
+ // Traefik answers 502/503/504 when the route has no live container and a plain-text 404 when it has no route at
67
+ // all. An agent's own errors are JSON, so a non-JSON 404 is the gateway, not the agent.
68
+ if ([502, 503, 504].includes(reply.status) || (reply.status === 404 && !isJson)) {
69
+ return { kind: "not-running" };
70
+ }
71
+ if (reply.status === 404)
72
+ return { kind: "not-found", body };
73
+ const detail = body?.error;
74
+ return {
75
+ kind: "error",
76
+ status: reply.status,
77
+ detail: typeof detail === "string" ? detail : reply.text,
78
+ };
79
+ }
80
+ /**
81
+ * Reads a `GET /sessions` body. Returns null when it is not the list shape, so a caller can tell a wrong endpoint from
82
+ * an agent with no sessions. Rows without a usable id are dropped; the agent owns its other fields.
83
+ */
84
+ export function parseAgentSessionList(body) {
85
+ if (typeof body !== "object" || body === null)
86
+ return null;
87
+ const raw = body.sessions;
88
+ if (!Array.isArray(raw))
89
+ return null;
90
+ const sessions = [];
91
+ for (const row of raw) {
92
+ if (typeof row !== "object" || row === null)
93
+ continue;
94
+ const r = row;
95
+ if (typeof r.sessionId !== "string" || !r.sessionId)
96
+ continue;
97
+ sessions.push({
98
+ sessionId: r.sessionId,
99
+ lastActivity: typeof r.lastActivity === "string" ? r.lastActivity : null,
100
+ status: typeof r.status === "string" ? r.status : "unknown",
101
+ checkpointBytes: typeof r.checkpointBytes === "number" ? r.checkpointBytes : null,
102
+ });
103
+ }
104
+ const total = body.total;
105
+ return { sessions, total: typeof total === "number" ? total : sessions.length };
106
+ }
107
+ /** Reads `/ready` and `/sessions` once, in parallel. Never throws: a failure is part of the answer. */
108
+ export async function fetchAgentOverview(routeUrl, timeoutMs = 3_000) {
109
+ const [ready, list] = await Promise.all([
110
+ fetchAgent(agentRequest(routeUrl, "/ready"), timeoutMs),
111
+ fetchAgent(agentRequest(routeUrl, "/sessions"), timeoutMs),
112
+ ]);
113
+ if (ready.kind === "not-running" && list.kind === "not-running")
114
+ return { state: "not-running" };
115
+ // /ready answers 503 with { ready: false } when the checkpoint store is down, which fetchAgent reports as "not-running".
116
+ const isReady = ready.kind === "ok" && ready.body?.ready === true;
117
+ return {
118
+ state: "up",
119
+ ready: isReady,
120
+ sessions: list.kind === "ok" ? parseAgentSessionList(list.body) : null,
121
+ };
122
+ }