@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.
- package/README.md +37 -14
- package/cli/dist/agent-runtimes/custom.js +94 -13
- package/cli/dist/commands/agent.js +144 -123
- package/cli/dist/commands/doctor.js +62 -12
- package/cli/dist/commands/down.js +33 -1
- package/cli/dist/commands/logs.js +107 -3
- package/cli/dist/commands/project.js +27 -0
- package/cli/dist/commands/resource.js +3 -1
- package/cli/dist/commands/status.js +82 -34
- package/cli/dist/commands/ui.js +41 -2
- package/cli/dist/commands/up.js +173 -12
- package/cli/dist/commands/upgrade.js +2 -2
- package/cli/dist/components/DetailPanel.js +23 -3
- package/cli/dist/mcp/tools.js +9 -6
- package/cli/dist/types/index.js +0 -2
- package/cli/dist/utils/agent-format.js +18 -0
- package/cli/dist/utils/agent-sessions.js +122 -0
- package/cli/dist/utils/agent-skills.js +64 -0
- package/cli/dist/utils/bun-requirement.js +7 -4
- package/cli/dist/utils/cold-preflight.js +55 -32
- package/cli/dist/utils/doctor-fix-first.js +93 -0
- package/cli/dist/utils/doctor-native.js +96 -0
- package/cli/dist/utils/doctor-report.js +3 -0
- package/cli/dist/utils/native-project.js +41 -0
- package/cli/dist/utils/native-runtime.js +516 -0
- package/cli/dist/utils/native-service.js +95 -0
- package/cli/dist/utils/native-up.js +156 -0
- package/cli/dist/utils/platform.js +1 -1
- package/cli/dist/utils/project-templates.js +1 -1
- package/cli/dist/utils/service-manifest.js +10 -1
- package/cli/dist/utils/services.js +11 -1
- package/cli/dist/utils/startup-report.js +45 -3
- package/cli/dist/utils/status-groups.js +23 -0
- package/cli/dist/utils/tilt-logs.js +8 -0
- package/cli/dist/utils/up-hints.js +42 -0
- package/cli/dist/utils/up-progress.js +117 -0
- package/cli/dist/utils/up-readiness.js +5 -0
- package/cli/dist-bundle/{chunk-Z5QEE4SW.js → chunk-6OW2GVQV.js} +1 -1
- package/cli/dist-bundle/chunk-B2JOM3HN.js +35 -0
- package/cli/dist-bundle/{chunk-VMVTECCK.js → chunk-F2DLIM75.js} +1 -1
- package/cli/dist-bundle/chunk-L6O7TT3K.js +18 -0
- package/cli/dist-bundle/chunk-MFY7RZ76.js +130 -0
- package/cli/dist-bundle/{chunk-SFOFTVYM.js → chunk-MYAYJF4Q.js} +1 -1
- package/cli/dist-bundle/{chunk-LUUJ4JB7.js → chunk-P7GZ6NGM.js} +1 -1
- package/cli/dist-bundle/{chunk-BP2M4OJ4.js → chunk-QEBN6ZVQ.js} +1 -1
- package/cli/dist-bundle/{chunk-4J2FLFUA.js → chunk-T44DEEL6.js} +1 -1
- package/cli/dist-bundle/{chunk-U42XZWXB.js → chunk-TIPSERWO.js} +1 -1
- package/cli/dist-bundle/{chunk-V36PT2HF.js → chunk-VYPB7MY5.js} +1 -1
- package/cli/dist-bundle/{chunk-TIYTGN5R.js → chunk-XHENT4KO.js} +1 -1
- package/cli/dist-bundle/{chunk-CGTWXWBU.js → chunk-YTBTCAWY.js} +1 -1
- package/cli/dist-bundle/cli.js +486 -396
- package/cli/dist-bundle/cold-preflight-DQVJUGBB.js +2 -0
- package/cli/dist-bundle/{demo-stage-G4W23BXW.js → demo-stage-F4YA7UL2.js} +1 -1
- package/cli/dist-bundle/{open-HQFT6WDC.js → open-ZUSSVPD3.js} +1 -1
- package/cli/dist-bundle/project-JFCTPRP5.js +2 -0
- package/cli/dist-bundle/{project-templates-IMJ3OX5I.js → project-templates-2VQPUBSQ.js} +1 -1
- package/cli/dist-bundle/{services-GLFKZBBZ.js → services-DOHDHOCT.js} +1 -1
- package/cli/dist-bundle/{tilt-port-owner-PJN4MPYE.js → tilt-port-owner-XODQHXHX.js} +1 -1
- package/cli/package.json +1 -1
- package/cli/templates/agent-skills/flisk-agent-ops/SKILL.md +39 -0
- package/cli/templates/agent-skills/flisk-build/SKILL.md +85 -0
- package/cli/templates/agent-skills/flisk-doctor/SKILL.md +28 -0
- package/cli/templates/agent-skills/flisk-recipe-ai-agent/SKILL.md +45 -0
- package/cli/templates/agent-skills/flisk-recipe-blog/SKILL.md +36 -0
- package/cli/templates/agent-skills/flisk-recipe-saas/SKILL.md +39 -0
- package/cli/templates/agent-skills/flisk-recipe-shop/SKILL.md +41 -0
- package/cli/templates/agent-skills/flisk-recipe-starters/SKILL.md +46 -0
- package/cli/templates/agent-skills/flisk-resource/SKILL.md +27 -0
- package/cli/templates/agent-skills/flisk-status/SKILL.md +25 -0
- package/cli/templates/agent-skills/flisk-troubleshoot/SKILL.md +227 -0
- package/cli/templates/agent-skills/flisk-up/SKILL.md +37 -0
- package/examples/{tdk-example → flisk-example}/README.md +2 -2
- package/package.json +2 -2
- package/runtime/discovery/AGENTS.md +1 -1
- package/runtime/discovery/TILT_DISCOVERY.star +1 -1
- package/runtime/discovery/deterministic.star +1 -1
- package/runtime/engine/AGENTS.md +3 -3
- package/runtime/engine/README.md +1 -1
- package/runtime/engine/REFACTORING_MANIFEST_SYSTEM.md +1 -1
- package/runtime/engine/assets/docker/wake-gateway/decide.ts +1 -1
- package/runtime/engine/assets/docker/wake-gateway/server.ts +2 -2
- package/runtime/engine/assets/scripts/mac-m1-resource-monitor.sh +6 -6
- package/runtime/engine/schemas/service-schema.json +87 -21
- package/runtime/engine/scripts/debug-container.sh +1 -1
- package/runtime/engine/scripts/generate-manifest.sh +2 -2
- package/runtime/engine/topologies/platform/docker/compose/traefik_standalone.star +19 -4
- package/runtime/engine/topologies/platform/docker/constants.star +2 -2
- package/runtime/engine/topologies/platform/docker/networking/traefik_static_routes.star +2 -2
- package/runtime/engine/topologies/tilt/manifest/mapper.star +1 -1
- package/runtime/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +8 -2
- package/runtime/ext/ide-components/README.md +1 -1
- package/runtime/ext/ui-enhancements/extension.yaml +2 -2
- package/runtime/specs/specs/TILT_SERVICE_DEFAULTS.star +1 -1
- package/runtime/specs/specs/TILT_TECH_STACK.star +1 -1
- package/cli/dist-bundle/chunk-BMHPPX2Q.js +0 -35
- package/cli/dist-bundle/chunk-PEZQEZVV.js +0 -17
- package/cli/dist-bundle/chunk-ULMQVD7J.js +0 -122
- package/cli/dist-bundle/cold-preflight-3BNDOLGS.js +0 -2
- package/cli/dist-bundle/project-JZO2CPZZ.js +0 -2
- /package/examples/{tdk-example → flisk-example}/LICENSE +0 -0
- /package/examples/{tdk-example → flisk-example}/TILT_SERVICE_DEFAULTS.star +0 -0
- /package/examples/{tdk-example → flisk-example}/TILT_TECH_STACK.star +0 -0
- /package/examples/{tdk-example → flisk-example}/Tiltfile +0 -0
- /package/examples/{tdk-example → flisk-example}/services/platform/messaging/docker-compose.yml +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/package.json +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/service.json +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/src/index.ts +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/index.html +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/package.json +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/public/.gitkeep +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/service.json +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/main.tsx +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/orders.tsx +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/style.css +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/package.json +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/service.json +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/src/index.ts +0 -0
package/cli/dist/commands/ui.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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"
|
package/cli/dist/commands/up.js
CHANGED
|
@@ -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,
|
|
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 {
|
|
41
|
-
import {
|
|
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
|
-
|
|
258
|
-
|
|
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
|
|
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
|
|
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
|
-
{
|
|
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
|
-
|
|
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 = "
|
|
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
|
|
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
|
-
|
|
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));
|
package/cli/dist/mcp/tools.js
CHANGED
|
@@ -328,23 +328,26 @@ export function createFliskTools(run = runFliskCli, up = defaultUpDeps) {
|
|
|
328
328
|
},
|
|
329
329
|
{
|
|
330
330
|
name: "get_agent_sessions",
|
|
331
|
-
description: "
|
|
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: {
|
|
336
|
+
sessionId: {
|
|
337
|
+
type: "string",
|
|
338
|
+
description: "Session ID to read; omit it to list all sessions",
|
|
339
|
+
},
|
|
337
340
|
}),
|
|
338
|
-
required: ["resource"
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
+
return sessionId
|
|
349
|
+
? ["agent", "sessions", resource, `--session-id=${sessionId}`, "--json"]
|
|
350
|
+
: ["agent", "sessions", resource, "--json"];
|
|
348
351
|
}),
|
|
349
352
|
},
|
|
350
353
|
];
|
package/cli/dist/types/index.js
CHANGED
|
@@ -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
|
+
}
|