@tilt-launcher/sdk 1.2.0 → 1.2.1
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 +86 -37
- package/dist/index.d.ts +1 -0
- package/dist/index.js +264 -1
- package/dist/tiltClient.d.ts +168 -0
- package/dist/tiltClient.js +265 -0
- package/package.json +2 -2
- package/src/index.ts +10 -0
- package/src/tiltClient.ts +474 -0
package/README.md
CHANGED
|
@@ -12,6 +12,58 @@ npm install @tilt-launcher/sdk
|
|
|
12
12
|
|
|
13
13
|
## Quick Start
|
|
14
14
|
|
|
15
|
+
### One-Shot Query (CLI tools, CI, health checks)
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { queryTilt } from '@tilt-launcher/sdk';
|
|
19
|
+
|
|
20
|
+
const status = await queryTilt(10350);
|
|
21
|
+
if (!status.allHealthy) {
|
|
22
|
+
for (const err of status.errors) {
|
|
23
|
+
console.error(`${err.name}: ${err.lastBuildError ?? err.runtimeStatus}`);
|
|
24
|
+
}
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
console.log(`All ${status.healthy.length} resources healthy`);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Streaming (dashboards, monitoring)
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { watchTilt } from '@tilt-launcher/sdk';
|
|
34
|
+
|
|
35
|
+
const stop = await watchTilt(10350, (event) => {
|
|
36
|
+
for (const r of event.resources) {
|
|
37
|
+
if (r.runtimeStatus === 'error') console.error(`${r.name} failed!`);
|
|
38
|
+
}
|
|
39
|
+
for (const log of event.logs) {
|
|
40
|
+
console.log(`[${log.resourceName ?? 'system'}] ${log.text}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Stop watching when done
|
|
45
|
+
setTimeout(stop, 60_000);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Full Client (multiple operations)
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { TiltClient } from '@tilt-launcher/sdk';
|
|
52
|
+
|
|
53
|
+
const tilt = new TiltClient(10350);
|
|
54
|
+
|
|
55
|
+
if (await tilt.isReachable()) {
|
|
56
|
+
const resources = await tilt.getResources();
|
|
57
|
+
const status = await tilt.getStatus();
|
|
58
|
+
const myService = await tilt.getResource('my-service');
|
|
59
|
+
await tilt.triggerResource('my-service');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
tilt.close();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Full Manager (multi-env orchestration)
|
|
66
|
+
|
|
15
67
|
```ts
|
|
16
68
|
import { TiltManagerSDK } from '@tilt-launcher/sdk';
|
|
17
69
|
import type { Config, StatusUpdate, LogDelta } from '@tilt-launcher/sdk';
|
|
@@ -31,34 +83,35 @@ const config: Config = {
|
|
|
31
83
|
};
|
|
32
84
|
|
|
33
85
|
const sdk = new TiltManagerSDK(config, {
|
|
34
|
-
onStatusUpdate: (update: StatusUpdate) =>
|
|
35
|
-
|
|
36
|
-
},
|
|
37
|
-
onLogDelta: (delta: LogDelta) => {
|
|
38
|
-
console.log('Logs:', delta);
|
|
39
|
-
},
|
|
86
|
+
onStatusUpdate: (update: StatusUpdate) => console.log('Status:', update.envs),
|
|
87
|
+
onLogDelta: (delta: LogDelta) => console.log('Logs:', delta),
|
|
40
88
|
});
|
|
41
89
|
|
|
42
|
-
// Start polling the Tilt API
|
|
43
90
|
sdk.startPolling(5000);
|
|
44
|
-
|
|
45
|
-
// Start an environment
|
|
46
91
|
await sdk.startEnv('my-app');
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API
|
|
47
95
|
|
|
48
|
-
|
|
49
|
-
const status = sdk.currentStatusUpdate();
|
|
96
|
+
### `queryTilt(port, options?)` → `Promise<TiltStatus>`
|
|
50
97
|
|
|
51
|
-
|
|
52
|
-
await sdk.triggerResource('my-app', 'web-server');
|
|
53
|
-
await sdk.disableResource('my-app', 'slow-service');
|
|
54
|
-
await sdk.enableResource('my-app', 'slow-service');
|
|
98
|
+
One-shot: fetch all resources and return a structured status summary. No cleanup needed.
|
|
55
99
|
|
|
56
|
-
|
|
57
|
-
await sdk.stopEnv('my-app');
|
|
58
|
-
sdk.stopPolling();
|
|
59
|
-
```
|
|
100
|
+
### `watchTilt(port, callback, options?)` → `Promise<() => void>`
|
|
60
101
|
|
|
61
|
-
|
|
102
|
+
Stream resource + log updates via WebSocket. Returns an unsubscribe function.
|
|
103
|
+
|
|
104
|
+
### `TiltClient`
|
|
105
|
+
|
|
106
|
+
| Method | Description |
|
|
107
|
+
| ----------------------- | ----------------------------------------- |
|
|
108
|
+
| `isReachable()` | Check if Tilt is running at this port |
|
|
109
|
+
| `getResources()` | Fetch all resource statuses |
|
|
110
|
+
| `getStatus()` | Fetch resources as a `TiltStatus` summary |
|
|
111
|
+
| `getResource(name)` | Get a single resource by name |
|
|
112
|
+
| `triggerResource(name)` | Trigger a resource update |
|
|
113
|
+
| `watch(callback)` | Subscribe to WebSocket updates |
|
|
114
|
+
| `close()` | Close all connections |
|
|
62
115
|
|
|
63
116
|
### `TiltManagerSDK`
|
|
64
117
|
|
|
@@ -77,28 +130,24 @@ sdk.stopPolling();
|
|
|
77
130
|
| `startPolling(intervalMs)` | Start polling Tilt APIs |
|
|
78
131
|
| `stopPolling()` | Stop polling |
|
|
79
132
|
|
|
80
|
-
### Types
|
|
81
|
-
|
|
82
|
-
- `Config` — app configuration with environment list
|
|
83
|
-
- `Environment` — a single Tilt environment definition
|
|
84
|
-
- `StatusUpdate` — full status snapshot (all envs + resources)
|
|
85
|
-
- `ResourceRow` — individual resource status (health, pid, labels, etc.)
|
|
86
|
-
- `LogDelta` — incremental log update
|
|
87
|
-
- `DiscoverResult` — result of resource discovery
|
|
88
|
-
- `LauncherBridge` — abstract UI bridge interface
|
|
89
|
-
|
|
90
|
-
### Callbacks
|
|
133
|
+
### Key Types
|
|
91
134
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
135
|
+
| Type | Description |
|
|
136
|
+
| ---------------- | ----------------------------------------------------- |
|
|
137
|
+
| `TiltResource` | Single resource from `TiltClient` (name, status, etc) |
|
|
138
|
+
| `TiltStatus` | Status summary: resources, errors, healthy, pending |
|
|
139
|
+
| `TiltWatchEvent` | WebSocket event: resources + logs |
|
|
140
|
+
| `ResourceRow` | Resource from `TiltManagerSDK` (includes health) |
|
|
141
|
+
| `StatusUpdate` | Full status snapshot from `TiltManagerSDK` |
|
|
142
|
+
| `LogDelta` | Incremental log update |
|
|
143
|
+
| `Config` | App configuration with environment list |
|
|
144
|
+
| `Environment` | Single Tilt environment definition |
|
|
145
|
+
| `LauncherBridge` | Abstract UI bridge interface |
|
|
97
146
|
|
|
98
147
|
## Requirements
|
|
99
148
|
|
|
100
|
-
- `tilt` must be on `$PATH`
|
|
101
149
|
- Node.js ≥ 18 or Bun
|
|
150
|
+
- `tilt` on `$PATH` (only required for `TiltManagerSDK`; `TiltClient` uses HTTP)
|
|
102
151
|
|
|
103
152
|
## License
|
|
104
153
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export * from './types.ts';
|
|
2
2
|
export { type LauncherBridge } from './bridge.ts';
|
|
3
3
|
export { TiltManagerSDK } from './tiltManagerSDK.ts';
|
|
4
|
+
export { TiltClient, queryTilt, watchTilt, type TiltResource, type TiltStatus, type TiltWatchEvent, type TiltWatchCallback, type TiltClientOptions, } from './tiltClient.ts';
|
package/dist/index.js
CHANGED
|
@@ -766,6 +766,269 @@ ${err.message}` }));
|
|
|
766
766
|
this.emitStatus();
|
|
767
767
|
}
|
|
768
768
|
}
|
|
769
|
+
|
|
770
|
+
// src/tiltClient.ts
|
|
771
|
+
function parseEndpoint(endpoint) {
|
|
772
|
+
if (!endpoint)
|
|
773
|
+
return {};
|
|
774
|
+
try {
|
|
775
|
+
const url = new URL(endpoint);
|
|
776
|
+
return {
|
|
777
|
+
port: Number(url.port || (url.protocol === "https:" ? 443 : 80)),
|
|
778
|
+
path: url.pathname || "/"
|
|
779
|
+
};
|
|
780
|
+
} catch {
|
|
781
|
+
return {};
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function parseUIResource(item, tiltPort) {
|
|
785
|
+
const endpointUrl = item.status?.endpointLinks?.[0]?.url;
|
|
786
|
+
let endpoint;
|
|
787
|
+
if (endpointUrl) {
|
|
788
|
+
try {
|
|
789
|
+
endpoint = new URL(endpointUrl).toString();
|
|
790
|
+
} catch {
|
|
791
|
+
try {
|
|
792
|
+
endpoint = new URL(endpointUrl, `http://localhost:${tiltPort}`).toString();
|
|
793
|
+
} catch {
|
|
794
|
+
endpoint = undefined;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const parsed = parseEndpoint(endpoint);
|
|
799
|
+
const labels = item.metadata?.labels ? Object.keys(item.metadata.labels) : [];
|
|
800
|
+
const disableState = (item.status?.disableStatus?.state ?? "").toLowerCase();
|
|
801
|
+
const isDisabled = disableState === "disabled" || disableState === "pending";
|
|
802
|
+
const runtimeStatus = isDisabled ? "disabled" : item.status?.runtimeStatus ?? "unknown";
|
|
803
|
+
const resourceKind = runtimeStatus === "not_applicable" || runtimeStatus === "disabled" ? "cmd" : runtimeStatus === "ok" || runtimeStatus === "pending" || runtimeStatus === "error" ? "serve" : "unknown";
|
|
804
|
+
const buildHistory = item.status?.buildHistory;
|
|
805
|
+
const lastBuild = buildHistory?.[0];
|
|
806
|
+
const lastBuildDuration = lastBuild?.startTime && lastBuild?.finishTime ? (new Date(lastBuild.finishTime).getTime() - new Date(lastBuild.startTime).getTime()) / 1000 : undefined;
|
|
807
|
+
const waiting = item.status?.waiting;
|
|
808
|
+
const rawConditions = item.status?.conditions;
|
|
809
|
+
const conditions = rawConditions?.map((c) => ({
|
|
810
|
+
type: c.type ?? "",
|
|
811
|
+
status: c.status ?? "",
|
|
812
|
+
...c.lastTransitionTime != null ? { lastTransitionTime: c.lastTransitionTime } : {}
|
|
813
|
+
}));
|
|
814
|
+
const rawPid = item.status?.localResourceInfo?.pid;
|
|
815
|
+
return {
|
|
816
|
+
name: item.metadata?.name ?? "unknown",
|
|
817
|
+
label: item.metadata?.name ?? "unknown",
|
|
818
|
+
category: labels[0] ?? "services",
|
|
819
|
+
type: item.status?.specs?.[0]?.type ?? "unknown",
|
|
820
|
+
endpoint,
|
|
821
|
+
port: parsed.port,
|
|
822
|
+
resourceKind,
|
|
823
|
+
runtimeStatus,
|
|
824
|
+
isDisabled,
|
|
825
|
+
updateStatus: item.status?.updateStatus,
|
|
826
|
+
waitingReason: waiting?.reason,
|
|
827
|
+
waitingOn: waiting?.on?.map((ref) => ref.name ?? "").filter(Boolean),
|
|
828
|
+
lastDeployTime: item.status?.lastDeployTime,
|
|
829
|
+
lastBuildDuration,
|
|
830
|
+
lastBuildError: lastBuild?.error,
|
|
831
|
+
hasPendingChanges: item.status?.hasPendingChanges,
|
|
832
|
+
triggerMode: item.status?.triggerMode,
|
|
833
|
+
queued: item.status?.queued,
|
|
834
|
+
pid: rawPid ? Number(rawPid) : undefined,
|
|
835
|
+
conditions
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function buildStatus(resources) {
|
|
839
|
+
const errors = resources.filter((r) => r.runtimeStatus === "error");
|
|
840
|
+
const healthy = resources.filter((r) => r.runtimeStatus === "ok");
|
|
841
|
+
const pending = resources.filter((r) => r.runtimeStatus === "pending");
|
|
842
|
+
const active = resources.filter((r) => !r.isDisabled && r.runtimeStatus !== "not_applicable");
|
|
843
|
+
return {
|
|
844
|
+
resources,
|
|
845
|
+
errors,
|
|
846
|
+
healthy,
|
|
847
|
+
pending,
|
|
848
|
+
allHealthy: active.length > 0 && errors.length === 0 && pending.length === 0
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
class TiltClient {
|
|
853
|
+
baseUrl;
|
|
854
|
+
port;
|
|
855
|
+
timeoutMs;
|
|
856
|
+
ws = null;
|
|
857
|
+
watchCallback = null;
|
|
858
|
+
reconnectTimer = null;
|
|
859
|
+
closed = false;
|
|
860
|
+
constructor(port, options) {
|
|
861
|
+
const host = options?.host ?? "127.0.0.1";
|
|
862
|
+
this.port = port;
|
|
863
|
+
this.baseUrl = `http://${host}:${port}`;
|
|
864
|
+
this.timeoutMs = options?.timeoutMs ?? 5000;
|
|
865
|
+
}
|
|
866
|
+
async isReachable() {
|
|
867
|
+
try {
|
|
868
|
+
const resp = await fetch(`${this.baseUrl}/api/websocket_token`, {
|
|
869
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
870
|
+
});
|
|
871
|
+
return resp.ok;
|
|
872
|
+
} catch {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async getResources() {
|
|
877
|
+
const resp = await fetch(`${this.baseUrl}/api/v1alpha1/uiresources`, {
|
|
878
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
879
|
+
});
|
|
880
|
+
if (!resp.ok) {
|
|
881
|
+
throw new Error(`Tilt API error: ${resp.status} ${resp.statusText}`);
|
|
882
|
+
}
|
|
883
|
+
const data = await resp.json();
|
|
884
|
+
return (data.items ?? []).filter((item) => item.metadata?.name && item.metadata.name !== "(Tiltfile)").map((item) => parseUIResource(item, this.port));
|
|
885
|
+
}
|
|
886
|
+
async getStatus() {
|
|
887
|
+
const resources = await this.getResources();
|
|
888
|
+
return buildStatus(resources);
|
|
889
|
+
}
|
|
890
|
+
async getResource(name) {
|
|
891
|
+
const resources = await this.getResources();
|
|
892
|
+
return resources.find((r) => r.name === name) ?? null;
|
|
893
|
+
}
|
|
894
|
+
async triggerResource(name) {
|
|
895
|
+
const resp = await fetch(`${this.baseUrl}/api/trigger`, {
|
|
896
|
+
method: "POST",
|
|
897
|
+
headers: { "Content-Type": "application/json" },
|
|
898
|
+
body: JSON.stringify({ manifest_names: [name], build_reason: 16 }),
|
|
899
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
900
|
+
});
|
|
901
|
+
if (!resp.ok) {
|
|
902
|
+
throw new Error(`Tilt trigger error: ${resp.status} ${resp.statusText}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async watch(callback) {
|
|
906
|
+
this.watchCallback = callback;
|
|
907
|
+
await this.connectWS();
|
|
908
|
+
return () => {
|
|
909
|
+
this.watchCallback = null;
|
|
910
|
+
this.disconnectWS();
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
close() {
|
|
914
|
+
this.closed = true;
|
|
915
|
+
this.watchCallback = null;
|
|
916
|
+
this.disconnectWS();
|
|
917
|
+
}
|
|
918
|
+
async connectWS() {
|
|
919
|
+
if (this.ws)
|
|
920
|
+
return;
|
|
921
|
+
const tokenResp = await fetch(`${this.baseUrl}/api/websocket_token`, {
|
|
922
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
923
|
+
});
|
|
924
|
+
if (!tokenResp.ok)
|
|
925
|
+
throw new Error("Could not get WebSocket token from Tilt");
|
|
926
|
+
const token = (await tokenResp.text()).trim();
|
|
927
|
+
const wsUrl = `ws://127.0.0.1:${this.port}/ws/view?token=${token}`;
|
|
928
|
+
const ws = new WebSocket(wsUrl);
|
|
929
|
+
ws.onmessage = (event) => {
|
|
930
|
+
try {
|
|
931
|
+
const data = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString());
|
|
932
|
+
this.handleWSMessage(data);
|
|
933
|
+
} catch {}
|
|
934
|
+
};
|
|
935
|
+
ws.onerror = () => {};
|
|
936
|
+
ws.onclose = () => {
|
|
937
|
+
this.ws = null;
|
|
938
|
+
if (this.watchCallback && !this.closed) {
|
|
939
|
+
this.reconnectTimer = setTimeout(() => {
|
|
940
|
+
this.reconnectTimer = null;
|
|
941
|
+
this.connectWS();
|
|
942
|
+
}, 3000);
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
this.ws = ws;
|
|
946
|
+
}
|
|
947
|
+
disconnectWS() {
|
|
948
|
+
if (this.reconnectTimer) {
|
|
949
|
+
clearTimeout(this.reconnectTimer);
|
|
950
|
+
this.reconnectTimer = null;
|
|
951
|
+
}
|
|
952
|
+
if (this.ws) {
|
|
953
|
+
try {
|
|
954
|
+
this.ws.close();
|
|
955
|
+
} catch {}
|
|
956
|
+
this.ws = null;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
handleWSMessage(data) {
|
|
960
|
+
if (!this.watchCallback)
|
|
961
|
+
return;
|
|
962
|
+
const resources = [];
|
|
963
|
+
const logs = [];
|
|
964
|
+
if (data.uiResources && Array.isArray(data.uiResources)) {
|
|
965
|
+
for (const item of data.uiResources) {
|
|
966
|
+
if (item.metadata?.name && item.metadata.name !== "(Tiltfile)") {
|
|
967
|
+
resources.push(parseUIResource(item, this.port));
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (data.logList?.segments && Array.isArray(data.logList.segments)) {
|
|
972
|
+
const spans = data.logList.spans;
|
|
973
|
+
for (const seg of data.logList.segments) {
|
|
974
|
+
const text = seg.text?.trimEnd();
|
|
975
|
+
if (!text)
|
|
976
|
+
continue;
|
|
977
|
+
const resourceName = seg.spanId && spans?.[seg.spanId]?.manifestName;
|
|
978
|
+
logs.push({
|
|
979
|
+
resourceName: resourceName && resourceName !== "(Tiltfile)" ? resourceName : undefined,
|
|
980
|
+
text
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (resources.length > 0 || logs.length > 0) {
|
|
985
|
+
this.watchCallback({ resources, logs });
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
async function queryTilt(port, options) {
|
|
990
|
+
const client = new TiltClient(port, options);
|
|
991
|
+
try {
|
|
992
|
+
return await client.getStatus();
|
|
993
|
+
} finally {
|
|
994
|
+
client.close();
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
async function watchTilt(port, callback, options) {
|
|
998
|
+
const client = new TiltClient(port, options);
|
|
999
|
+
const unsub = await client.watch(callback);
|
|
1000
|
+
return () => {
|
|
1001
|
+
unsub();
|
|
1002
|
+
client.close();
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
function tiltResourceToCached(r) {
|
|
1006
|
+
return {
|
|
1007
|
+
name: r.name,
|
|
1008
|
+
label: r.label,
|
|
1009
|
+
category: r.category,
|
|
1010
|
+
type: r.type,
|
|
1011
|
+
endpoint: r.endpoint,
|
|
1012
|
+
port: r.port,
|
|
1013
|
+
runtimeStatus: r.runtimeStatus,
|
|
1014
|
+
isDisabled: r.isDisabled,
|
|
1015
|
+
resourceKind: r.resourceKind,
|
|
1016
|
+
updateStatus: r.updateStatus,
|
|
1017
|
+
waitingReason: r.waitingReason,
|
|
1018
|
+
waitingOn: r.waitingOn,
|
|
1019
|
+
lastDeployTime: r.lastDeployTime,
|
|
1020
|
+
lastBuildDuration: r.lastBuildDuration,
|
|
1021
|
+
lastBuildError: r.lastBuildError,
|
|
1022
|
+
hasPendingChanges: r.hasPendingChanges,
|
|
1023
|
+
triggerMode: r.triggerMode,
|
|
1024
|
+
queued: r.queued,
|
|
1025
|
+
pid: r.pid,
|
|
1026
|
+
conditions: r.conditions
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
769
1029
|
export {
|
|
770
|
-
|
|
1030
|
+
watchTilt,
|
|
1031
|
+
queryTilt,
|
|
1032
|
+
TiltManagerSDK,
|
|
1033
|
+
TiltClient
|
|
771
1034
|
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone Tilt API client.
|
|
3
|
+
*
|
|
4
|
+
* Connects directly to a running Tilt instance via its HTTP/WebSocket API.
|
|
5
|
+
* No config file, no process management — just point at a port and query.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { TiltClient } from '@tilt-launcher/sdk';
|
|
10
|
+
*
|
|
11
|
+
* const tilt = new TiltClient(10350);
|
|
12
|
+
* const resources = await tilt.getResources();
|
|
13
|
+
* const failing = resources.filter(r => r.runtimeStatus === 'error');
|
|
14
|
+
* tilt.close();
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import type { CachedResource } from './types.ts';
|
|
18
|
+
export interface TiltResource {
|
|
19
|
+
/** Resource name as defined in Tiltfile */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Display label */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Tilt category grouping */
|
|
24
|
+
category: string;
|
|
25
|
+
/** Resource type from spec (e.g. "local", "docker_compose") */
|
|
26
|
+
type: string;
|
|
27
|
+
/** Public endpoint URL if exposed */
|
|
28
|
+
endpoint?: string | undefined;
|
|
29
|
+
/** Port number extracted from endpoint */
|
|
30
|
+
port?: number | undefined;
|
|
31
|
+
/** Kind of resource: long-running service, one-shot command, or unknown */
|
|
32
|
+
resourceKind: 'serve' | 'cmd' | 'unknown';
|
|
33
|
+
/** Tilt runtime status: ok, pending, error, not_applicable, disabled */
|
|
34
|
+
runtimeStatus: string;
|
|
35
|
+
/** Whether the resource is disabled */
|
|
36
|
+
isDisabled: boolean;
|
|
37
|
+
/** Update status: ok, pending, error, not_applicable */
|
|
38
|
+
updateStatus?: string | undefined;
|
|
39
|
+
/** If waiting, why */
|
|
40
|
+
waitingReason?: string | undefined;
|
|
41
|
+
/** Resources this one is waiting on */
|
|
42
|
+
waitingOn?: string[] | undefined;
|
|
43
|
+
/** ISO timestamp of last deploy */
|
|
44
|
+
lastDeployTime?: string | undefined;
|
|
45
|
+
/** Duration of last build in seconds */
|
|
46
|
+
lastBuildDuration?: number | undefined;
|
|
47
|
+
/** Error message from last build, if any */
|
|
48
|
+
lastBuildError?: string | undefined;
|
|
49
|
+
/** Whether there are pending file changes */
|
|
50
|
+
hasPendingChanges?: boolean | undefined;
|
|
51
|
+
/** Trigger mode (0 = auto, 1 = manual) */
|
|
52
|
+
triggerMode?: number | undefined;
|
|
53
|
+
/** Whether a trigger is queued */
|
|
54
|
+
queued?: boolean | undefined;
|
|
55
|
+
/** PID of local resource process */
|
|
56
|
+
pid?: number | undefined;
|
|
57
|
+
/** Tilt conditions array */
|
|
58
|
+
conditions?: Array<{
|
|
59
|
+
type: string;
|
|
60
|
+
status: string;
|
|
61
|
+
lastTransitionTime?: string;
|
|
62
|
+
}> | undefined;
|
|
63
|
+
}
|
|
64
|
+
export interface TiltStatus {
|
|
65
|
+
/** All resources discovered in this Tilt instance */
|
|
66
|
+
resources: TiltResource[];
|
|
67
|
+
/** Resources currently in error state */
|
|
68
|
+
errors: TiltResource[];
|
|
69
|
+
/** Resources currently running OK */
|
|
70
|
+
healthy: TiltResource[];
|
|
71
|
+
/** Resources pending/building */
|
|
72
|
+
pending: TiltResource[];
|
|
73
|
+
/** Whether all non-disabled resources are healthy */
|
|
74
|
+
allHealthy: boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface TiltWatchEvent {
|
|
77
|
+
/** Updated resource statuses */
|
|
78
|
+
resources: TiltResource[];
|
|
79
|
+
/** Log lines from this update (spanId-tagged) */
|
|
80
|
+
logs: Array<{
|
|
81
|
+
resourceName?: string | undefined;
|
|
82
|
+
text: string;
|
|
83
|
+
}>;
|
|
84
|
+
}
|
|
85
|
+
export type TiltWatchCallback = (event: TiltWatchEvent) => void;
|
|
86
|
+
export interface TiltClientOptions {
|
|
87
|
+
/** Hostname of the Tilt instance. Default: '127.0.0.1' */
|
|
88
|
+
host?: string;
|
|
89
|
+
/** Timeout for HTTP requests in ms. Default: 5000 */
|
|
90
|
+
timeoutMs?: number;
|
|
91
|
+
}
|
|
92
|
+
export declare class TiltClient {
|
|
93
|
+
private readonly baseUrl;
|
|
94
|
+
private readonly port;
|
|
95
|
+
private readonly timeoutMs;
|
|
96
|
+
private ws;
|
|
97
|
+
private watchCallback;
|
|
98
|
+
private reconnectTimer;
|
|
99
|
+
private closed;
|
|
100
|
+
constructor(port: number, options?: TiltClientOptions);
|
|
101
|
+
/** Check if Tilt is reachable at this port. */
|
|
102
|
+
isReachable(): Promise<boolean>;
|
|
103
|
+
/** Fetch all resources from this Tilt instance. */
|
|
104
|
+
getResources(): Promise<TiltResource[]>;
|
|
105
|
+
/** Fetch resources and return a structured status summary. */
|
|
106
|
+
getStatus(): Promise<TiltStatus>;
|
|
107
|
+
/** Get a single resource by name. */
|
|
108
|
+
getResource(name: string): Promise<TiltResource | null>;
|
|
109
|
+
/** Trigger a resource update (equivalent to pressing the trigger button in Tilt UI). */
|
|
110
|
+
triggerResource(name: string): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Watch for resource updates via WebSocket.
|
|
113
|
+
* Returns an unsubscribe function.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* const stop = await tilt.watch((event) => {
|
|
118
|
+
* for (const r of event.resources) {
|
|
119
|
+
* if (r.runtimeStatus === 'error') console.error(`${r.name} failed!`);
|
|
120
|
+
* }
|
|
121
|
+
* });
|
|
122
|
+
* // Later:
|
|
123
|
+
* stop();
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
watch(callback: TiltWatchCallback): Promise<() => void>;
|
|
127
|
+
/** Close all connections. */
|
|
128
|
+
close(): void;
|
|
129
|
+
private connectWS;
|
|
130
|
+
private disconnectWS;
|
|
131
|
+
private handleWSMessage;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* One-shot query: get the current status of all resources from a Tilt instance.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* import { queryTilt } from '@tilt-launcher/sdk';
|
|
139
|
+
*
|
|
140
|
+
* const status = await queryTilt(10350);
|
|
141
|
+
* if (!status.allHealthy) {
|
|
142
|
+
* for (const err of status.errors) {
|
|
143
|
+
* console.error(`${err.name}: ${err.lastBuildError ?? err.runtimeStatus}`);
|
|
144
|
+
* }
|
|
145
|
+
* process.exit(1);
|
|
146
|
+
* }
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export declare function queryTilt(port: number, options?: TiltClientOptions): Promise<TiltStatus>;
|
|
150
|
+
/**
|
|
151
|
+
* Stream resource updates from a Tilt instance.
|
|
152
|
+
* Returns an unsubscribe function.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* import { watchTilt } from '@tilt-launcher/sdk';
|
|
157
|
+
*
|
|
158
|
+
* const stop = await watchTilt(10350, (event) => {
|
|
159
|
+
* console.log(`${event.resources.length} resources updated`);
|
|
160
|
+
* });
|
|
161
|
+
*
|
|
162
|
+
* // Stop watching after 30s
|
|
163
|
+
* setTimeout(stop, 30_000);
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export declare function watchTilt(port: number, callback: TiltWatchCallback, options?: TiltClientOptions): Promise<() => void>;
|
|
167
|
+
/** @internal Convert TiltResource to CachedResource for backward compatibility */
|
|
168
|
+
export declare function tiltResourceToCached(r: TiltResource): CachedResource;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// src/tiltClient.ts
|
|
2
|
+
function parseEndpoint(endpoint) {
|
|
3
|
+
if (!endpoint)
|
|
4
|
+
return {};
|
|
5
|
+
try {
|
|
6
|
+
const url = new URL(endpoint);
|
|
7
|
+
return {
|
|
8
|
+
port: Number(url.port || (url.protocol === "https:" ? 443 : 80)),
|
|
9
|
+
path: url.pathname || "/"
|
|
10
|
+
};
|
|
11
|
+
} catch {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function parseUIResource(item, tiltPort) {
|
|
16
|
+
const endpointUrl = item.status?.endpointLinks?.[0]?.url;
|
|
17
|
+
let endpoint;
|
|
18
|
+
if (endpointUrl) {
|
|
19
|
+
try {
|
|
20
|
+
endpoint = new URL(endpointUrl).toString();
|
|
21
|
+
} catch {
|
|
22
|
+
try {
|
|
23
|
+
endpoint = new URL(endpointUrl, `http://localhost:${tiltPort}`).toString();
|
|
24
|
+
} catch {
|
|
25
|
+
endpoint = undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const parsed = parseEndpoint(endpoint);
|
|
30
|
+
const labels = item.metadata?.labels ? Object.keys(item.metadata.labels) : [];
|
|
31
|
+
const disableState = (item.status?.disableStatus?.state ?? "").toLowerCase();
|
|
32
|
+
const isDisabled = disableState === "disabled" || disableState === "pending";
|
|
33
|
+
const runtimeStatus = isDisabled ? "disabled" : item.status?.runtimeStatus ?? "unknown";
|
|
34
|
+
const resourceKind = runtimeStatus === "not_applicable" || runtimeStatus === "disabled" ? "cmd" : runtimeStatus === "ok" || runtimeStatus === "pending" || runtimeStatus === "error" ? "serve" : "unknown";
|
|
35
|
+
const buildHistory = item.status?.buildHistory;
|
|
36
|
+
const lastBuild = buildHistory?.[0];
|
|
37
|
+
const lastBuildDuration = lastBuild?.startTime && lastBuild?.finishTime ? (new Date(lastBuild.finishTime).getTime() - new Date(lastBuild.startTime).getTime()) / 1000 : undefined;
|
|
38
|
+
const waiting = item.status?.waiting;
|
|
39
|
+
const rawConditions = item.status?.conditions;
|
|
40
|
+
const conditions = rawConditions?.map((c) => ({
|
|
41
|
+
type: c.type ?? "",
|
|
42
|
+
status: c.status ?? "",
|
|
43
|
+
...c.lastTransitionTime != null ? { lastTransitionTime: c.lastTransitionTime } : {}
|
|
44
|
+
}));
|
|
45
|
+
const rawPid = item.status?.localResourceInfo?.pid;
|
|
46
|
+
return {
|
|
47
|
+
name: item.metadata?.name ?? "unknown",
|
|
48
|
+
label: item.metadata?.name ?? "unknown",
|
|
49
|
+
category: labels[0] ?? "services",
|
|
50
|
+
type: item.status?.specs?.[0]?.type ?? "unknown",
|
|
51
|
+
endpoint,
|
|
52
|
+
port: parsed.port,
|
|
53
|
+
resourceKind,
|
|
54
|
+
runtimeStatus,
|
|
55
|
+
isDisabled,
|
|
56
|
+
updateStatus: item.status?.updateStatus,
|
|
57
|
+
waitingReason: waiting?.reason,
|
|
58
|
+
waitingOn: waiting?.on?.map((ref) => ref.name ?? "").filter(Boolean),
|
|
59
|
+
lastDeployTime: item.status?.lastDeployTime,
|
|
60
|
+
lastBuildDuration,
|
|
61
|
+
lastBuildError: lastBuild?.error,
|
|
62
|
+
hasPendingChanges: item.status?.hasPendingChanges,
|
|
63
|
+
triggerMode: item.status?.triggerMode,
|
|
64
|
+
queued: item.status?.queued,
|
|
65
|
+
pid: rawPid ? Number(rawPid) : undefined,
|
|
66
|
+
conditions
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function buildStatus(resources) {
|
|
70
|
+
const errors = resources.filter((r) => r.runtimeStatus === "error");
|
|
71
|
+
const healthy = resources.filter((r) => r.runtimeStatus === "ok");
|
|
72
|
+
const pending = resources.filter((r) => r.runtimeStatus === "pending");
|
|
73
|
+
const active = resources.filter((r) => !r.isDisabled && r.runtimeStatus !== "not_applicable");
|
|
74
|
+
return {
|
|
75
|
+
resources,
|
|
76
|
+
errors,
|
|
77
|
+
healthy,
|
|
78
|
+
pending,
|
|
79
|
+
allHealthy: active.length > 0 && errors.length === 0 && pending.length === 0
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class TiltClient {
|
|
84
|
+
baseUrl;
|
|
85
|
+
port;
|
|
86
|
+
timeoutMs;
|
|
87
|
+
ws = null;
|
|
88
|
+
watchCallback = null;
|
|
89
|
+
reconnectTimer = null;
|
|
90
|
+
closed = false;
|
|
91
|
+
constructor(port, options) {
|
|
92
|
+
const host = options?.host ?? "127.0.0.1";
|
|
93
|
+
this.port = port;
|
|
94
|
+
this.baseUrl = `http://${host}:${port}`;
|
|
95
|
+
this.timeoutMs = options?.timeoutMs ?? 5000;
|
|
96
|
+
}
|
|
97
|
+
async isReachable() {
|
|
98
|
+
try {
|
|
99
|
+
const resp = await fetch(`${this.baseUrl}/api/websocket_token`, {
|
|
100
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
101
|
+
});
|
|
102
|
+
return resp.ok;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async getResources() {
|
|
108
|
+
const resp = await fetch(`${this.baseUrl}/api/v1alpha1/uiresources`, {
|
|
109
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
110
|
+
});
|
|
111
|
+
if (!resp.ok) {
|
|
112
|
+
throw new Error(`Tilt API error: ${resp.status} ${resp.statusText}`);
|
|
113
|
+
}
|
|
114
|
+
const data = await resp.json();
|
|
115
|
+
return (data.items ?? []).filter((item) => item.metadata?.name && item.metadata.name !== "(Tiltfile)").map((item) => parseUIResource(item, this.port));
|
|
116
|
+
}
|
|
117
|
+
async getStatus() {
|
|
118
|
+
const resources = await this.getResources();
|
|
119
|
+
return buildStatus(resources);
|
|
120
|
+
}
|
|
121
|
+
async getResource(name) {
|
|
122
|
+
const resources = await this.getResources();
|
|
123
|
+
return resources.find((r) => r.name === name) ?? null;
|
|
124
|
+
}
|
|
125
|
+
async triggerResource(name) {
|
|
126
|
+
const resp = await fetch(`${this.baseUrl}/api/trigger`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
body: JSON.stringify({ manifest_names: [name], build_reason: 16 }),
|
|
130
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
131
|
+
});
|
|
132
|
+
if (!resp.ok) {
|
|
133
|
+
throw new Error(`Tilt trigger error: ${resp.status} ${resp.statusText}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async watch(callback) {
|
|
137
|
+
this.watchCallback = callback;
|
|
138
|
+
await this.connectWS();
|
|
139
|
+
return () => {
|
|
140
|
+
this.watchCallback = null;
|
|
141
|
+
this.disconnectWS();
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
close() {
|
|
145
|
+
this.closed = true;
|
|
146
|
+
this.watchCallback = null;
|
|
147
|
+
this.disconnectWS();
|
|
148
|
+
}
|
|
149
|
+
async connectWS() {
|
|
150
|
+
if (this.ws)
|
|
151
|
+
return;
|
|
152
|
+
const tokenResp = await fetch(`${this.baseUrl}/api/websocket_token`, {
|
|
153
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
154
|
+
});
|
|
155
|
+
if (!tokenResp.ok)
|
|
156
|
+
throw new Error("Could not get WebSocket token from Tilt");
|
|
157
|
+
const token = (await tokenResp.text()).trim();
|
|
158
|
+
const wsUrl = `ws://127.0.0.1:${this.port}/ws/view?token=${token}`;
|
|
159
|
+
const ws = new WebSocket(wsUrl);
|
|
160
|
+
ws.onmessage = (event) => {
|
|
161
|
+
try {
|
|
162
|
+
const data = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString());
|
|
163
|
+
this.handleWSMessage(data);
|
|
164
|
+
} catch {}
|
|
165
|
+
};
|
|
166
|
+
ws.onerror = () => {};
|
|
167
|
+
ws.onclose = () => {
|
|
168
|
+
this.ws = null;
|
|
169
|
+
if (this.watchCallback && !this.closed) {
|
|
170
|
+
this.reconnectTimer = setTimeout(() => {
|
|
171
|
+
this.reconnectTimer = null;
|
|
172
|
+
this.connectWS();
|
|
173
|
+
}, 3000);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
this.ws = ws;
|
|
177
|
+
}
|
|
178
|
+
disconnectWS() {
|
|
179
|
+
if (this.reconnectTimer) {
|
|
180
|
+
clearTimeout(this.reconnectTimer);
|
|
181
|
+
this.reconnectTimer = null;
|
|
182
|
+
}
|
|
183
|
+
if (this.ws) {
|
|
184
|
+
try {
|
|
185
|
+
this.ws.close();
|
|
186
|
+
} catch {}
|
|
187
|
+
this.ws = null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
handleWSMessage(data) {
|
|
191
|
+
if (!this.watchCallback)
|
|
192
|
+
return;
|
|
193
|
+
const resources = [];
|
|
194
|
+
const logs = [];
|
|
195
|
+
if (data.uiResources && Array.isArray(data.uiResources)) {
|
|
196
|
+
for (const item of data.uiResources) {
|
|
197
|
+
if (item.metadata?.name && item.metadata.name !== "(Tiltfile)") {
|
|
198
|
+
resources.push(parseUIResource(item, this.port));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (data.logList?.segments && Array.isArray(data.logList.segments)) {
|
|
203
|
+
const spans = data.logList.spans;
|
|
204
|
+
for (const seg of data.logList.segments) {
|
|
205
|
+
const text = seg.text?.trimEnd();
|
|
206
|
+
if (!text)
|
|
207
|
+
continue;
|
|
208
|
+
const resourceName = seg.spanId && spans?.[seg.spanId]?.manifestName;
|
|
209
|
+
logs.push({
|
|
210
|
+
resourceName: resourceName && resourceName !== "(Tiltfile)" ? resourceName : undefined,
|
|
211
|
+
text
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (resources.length > 0 || logs.length > 0) {
|
|
216
|
+
this.watchCallback({ resources, logs });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function queryTilt(port, options) {
|
|
221
|
+
const client = new TiltClient(port, options);
|
|
222
|
+
try {
|
|
223
|
+
return await client.getStatus();
|
|
224
|
+
} finally {
|
|
225
|
+
client.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function watchTilt(port, callback, options) {
|
|
229
|
+
const client = new TiltClient(port, options);
|
|
230
|
+
const unsub = await client.watch(callback);
|
|
231
|
+
return () => {
|
|
232
|
+
unsub();
|
|
233
|
+
client.close();
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function tiltResourceToCached(r) {
|
|
237
|
+
return {
|
|
238
|
+
name: r.name,
|
|
239
|
+
label: r.label,
|
|
240
|
+
category: r.category,
|
|
241
|
+
type: r.type,
|
|
242
|
+
endpoint: r.endpoint,
|
|
243
|
+
port: r.port,
|
|
244
|
+
runtimeStatus: r.runtimeStatus,
|
|
245
|
+
isDisabled: r.isDisabled,
|
|
246
|
+
resourceKind: r.resourceKind,
|
|
247
|
+
updateStatus: r.updateStatus,
|
|
248
|
+
waitingReason: r.waitingReason,
|
|
249
|
+
waitingOn: r.waitingOn,
|
|
250
|
+
lastDeployTime: r.lastDeployTime,
|
|
251
|
+
lastBuildDuration: r.lastBuildDuration,
|
|
252
|
+
lastBuildError: r.lastBuildError,
|
|
253
|
+
hasPendingChanges: r.hasPendingChanges,
|
|
254
|
+
triggerMode: r.triggerMode,
|
|
255
|
+
queued: r.queued,
|
|
256
|
+
pid: r.pid,
|
|
257
|
+
conditions: r.conditions
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
export {
|
|
261
|
+
watchTilt,
|
|
262
|
+
tiltResourceToCached,
|
|
263
|
+
queryTilt,
|
|
264
|
+
TiltClient
|
|
265
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tilt-launcher/sdk",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "SDK for managing Tilt dev environments — start, stop, monitor, and control resources",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"README.md"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"build": "bun build src/index.ts src/types.ts src/bridge.ts src/tiltManagerSDK.ts --outdir dist --target node && tsc -p tsconfig.build.json",
|
|
22
|
+
"build": "bun build src/index.ts src/types.ts src/bridge.ts src/tiltManagerSDK.ts src/tiltClient.ts --outdir dist --target node && tsc -p tsconfig.build.json",
|
|
23
23
|
"check": "tsc --noEmit -p tsconfig.json",
|
|
24
24
|
"prepublishOnly": "bun run check && bun run build"
|
|
25
25
|
},
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
export * from './types.ts';
|
|
2
2
|
export { type LauncherBridge } from './bridge.ts';
|
|
3
3
|
export { TiltManagerSDK } from './tiltManagerSDK.ts';
|
|
4
|
+
export {
|
|
5
|
+
TiltClient,
|
|
6
|
+
queryTilt,
|
|
7
|
+
watchTilt,
|
|
8
|
+
type TiltResource,
|
|
9
|
+
type TiltStatus,
|
|
10
|
+
type TiltWatchEvent,
|
|
11
|
+
type TiltWatchCallback,
|
|
12
|
+
type TiltClientOptions,
|
|
13
|
+
} from './tiltClient.ts';
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone Tilt API client.
|
|
3
|
+
*
|
|
4
|
+
* Connects directly to a running Tilt instance via its HTTP/WebSocket API.
|
|
5
|
+
* No config file, no process management — just point at a port and query.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { TiltClient } from '@tilt-launcher/sdk';
|
|
10
|
+
*
|
|
11
|
+
* const tilt = new TiltClient(10350);
|
|
12
|
+
* const resources = await tilt.getResources();
|
|
13
|
+
* const failing = resources.filter(r => r.runtimeStatus === 'error');
|
|
14
|
+
* tilt.close();
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { CachedResource } from './types.ts';
|
|
19
|
+
|
|
20
|
+
// ── Types ────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface TiltResource {
|
|
23
|
+
/** Resource name as defined in Tiltfile */
|
|
24
|
+
name: string;
|
|
25
|
+
/** Display label */
|
|
26
|
+
label: string;
|
|
27
|
+
/** Tilt category grouping */
|
|
28
|
+
category: string;
|
|
29
|
+
/** Resource type from spec (e.g. "local", "docker_compose") */
|
|
30
|
+
type: string;
|
|
31
|
+
/** Public endpoint URL if exposed */
|
|
32
|
+
endpoint?: string | undefined;
|
|
33
|
+
/** Port number extracted from endpoint */
|
|
34
|
+
port?: number | undefined;
|
|
35
|
+
/** Kind of resource: long-running service, one-shot command, or unknown */
|
|
36
|
+
resourceKind: 'serve' | 'cmd' | 'unknown';
|
|
37
|
+
/** Tilt runtime status: ok, pending, error, not_applicable, disabled */
|
|
38
|
+
runtimeStatus: string;
|
|
39
|
+
/** Whether the resource is disabled */
|
|
40
|
+
isDisabled: boolean;
|
|
41
|
+
/** Update status: ok, pending, error, not_applicable */
|
|
42
|
+
updateStatus?: string | undefined;
|
|
43
|
+
/** If waiting, why */
|
|
44
|
+
waitingReason?: string | undefined;
|
|
45
|
+
/** Resources this one is waiting on */
|
|
46
|
+
waitingOn?: string[] | undefined;
|
|
47
|
+
/** ISO timestamp of last deploy */
|
|
48
|
+
lastDeployTime?: string | undefined;
|
|
49
|
+
/** Duration of last build in seconds */
|
|
50
|
+
lastBuildDuration?: number | undefined;
|
|
51
|
+
/** Error message from last build, if any */
|
|
52
|
+
lastBuildError?: string | undefined;
|
|
53
|
+
/** Whether there are pending file changes */
|
|
54
|
+
hasPendingChanges?: boolean | undefined;
|
|
55
|
+
/** Trigger mode (0 = auto, 1 = manual) */
|
|
56
|
+
triggerMode?: number | undefined;
|
|
57
|
+
/** Whether a trigger is queued */
|
|
58
|
+
queued?: boolean | undefined;
|
|
59
|
+
/** PID of local resource process */
|
|
60
|
+
pid?: number | undefined;
|
|
61
|
+
/** Tilt conditions array */
|
|
62
|
+
conditions?: Array<{ type: string; status: string; lastTransitionTime?: string }> | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface TiltStatus {
|
|
66
|
+
/** All resources discovered in this Tilt instance */
|
|
67
|
+
resources: TiltResource[];
|
|
68
|
+
/** Resources currently in error state */
|
|
69
|
+
errors: TiltResource[];
|
|
70
|
+
/** Resources currently running OK */
|
|
71
|
+
healthy: TiltResource[];
|
|
72
|
+
/** Resources pending/building */
|
|
73
|
+
pending: TiltResource[];
|
|
74
|
+
/** Whether all non-disabled resources are healthy */
|
|
75
|
+
allHealthy: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TiltWatchEvent {
|
|
79
|
+
/** Updated resource statuses */
|
|
80
|
+
resources: TiltResource[];
|
|
81
|
+
/** Log lines from this update (spanId-tagged) */
|
|
82
|
+
logs: Array<{ resourceName?: string | undefined; text: string }>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type TiltWatchCallback = (event: TiltWatchEvent) => void;
|
|
86
|
+
|
|
87
|
+
// ── Internal helpers ─────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function parseEndpoint(endpoint?: string): { port?: number; path?: string } {
|
|
90
|
+
if (!endpoint) return {};
|
|
91
|
+
try {
|
|
92
|
+
const url = new URL(endpoint);
|
|
93
|
+
return {
|
|
94
|
+
port: Number(url.port || (url.protocol === 'https:' ? 443 : 80)),
|
|
95
|
+
path: url.pathname || '/',
|
|
96
|
+
};
|
|
97
|
+
} catch {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
function parseUIResource(item: any, tiltPort: number): TiltResource {
|
|
104
|
+
const endpointUrl = item.status?.endpointLinks?.[0]?.url as string | undefined;
|
|
105
|
+
let endpoint: string | undefined;
|
|
106
|
+
if (endpointUrl) {
|
|
107
|
+
try {
|
|
108
|
+
endpoint = new URL(endpointUrl).toString();
|
|
109
|
+
} catch {
|
|
110
|
+
try {
|
|
111
|
+
endpoint = new URL(endpointUrl, `http://localhost:${tiltPort}`).toString();
|
|
112
|
+
} catch {
|
|
113
|
+
endpoint = undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const parsed = parseEndpoint(endpoint);
|
|
119
|
+
const labels = item.metadata?.labels ? Object.keys(item.metadata.labels) : [];
|
|
120
|
+
const disableState = (item.status?.disableStatus?.state ?? '').toLowerCase();
|
|
121
|
+
const isDisabled = disableState === 'disabled' || disableState === 'pending';
|
|
122
|
+
const runtimeStatus = isDisabled ? 'disabled' : (item.status?.runtimeStatus ?? 'unknown');
|
|
123
|
+
const resourceKind =
|
|
124
|
+
runtimeStatus === 'not_applicable' || runtimeStatus === 'disabled'
|
|
125
|
+
? ('cmd' as const)
|
|
126
|
+
: runtimeStatus === 'ok' || runtimeStatus === 'pending' || runtimeStatus === 'error'
|
|
127
|
+
? ('serve' as const)
|
|
128
|
+
: ('unknown' as const);
|
|
129
|
+
|
|
130
|
+
// Build history
|
|
131
|
+
const buildHistory = item.status?.buildHistory as
|
|
132
|
+
| Array<{ startTime?: string; finishTime?: string; error?: string }>
|
|
133
|
+
| undefined;
|
|
134
|
+
const lastBuild = buildHistory?.[0];
|
|
135
|
+
const lastBuildDuration =
|
|
136
|
+
lastBuild?.startTime && lastBuild?.finishTime
|
|
137
|
+
? (new Date(lastBuild.finishTime).getTime() - new Date(lastBuild.startTime).getTime()) / 1000
|
|
138
|
+
: undefined;
|
|
139
|
+
|
|
140
|
+
// Waiting
|
|
141
|
+
const waiting = item.status?.waiting as { reason?: string; on?: Array<{ name?: string }> } | undefined;
|
|
142
|
+
|
|
143
|
+
// Conditions
|
|
144
|
+
const rawConditions = item.status?.conditions as
|
|
145
|
+
| Array<{ type?: string; status?: string; lastTransitionTime?: string }>
|
|
146
|
+
| undefined;
|
|
147
|
+
const conditions = rawConditions?.map((c) => ({
|
|
148
|
+
type: c.type ?? '',
|
|
149
|
+
status: c.status ?? '',
|
|
150
|
+
...(c.lastTransitionTime != null ? { lastTransitionTime: c.lastTransitionTime } : {}),
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
const rawPid = item.status?.localResourceInfo?.pid;
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
name: item.metadata?.name ?? 'unknown',
|
|
157
|
+
label: item.metadata?.name ?? 'unknown',
|
|
158
|
+
category: labels[0] ?? 'services',
|
|
159
|
+
type: item.status?.specs?.[0]?.type ?? 'unknown',
|
|
160
|
+
endpoint,
|
|
161
|
+
port: parsed.port,
|
|
162
|
+
resourceKind,
|
|
163
|
+
runtimeStatus,
|
|
164
|
+
isDisabled,
|
|
165
|
+
updateStatus: item.status?.updateStatus,
|
|
166
|
+
waitingReason: waiting?.reason,
|
|
167
|
+
waitingOn: waiting?.on?.map((ref: { name?: string }) => ref.name ?? '').filter(Boolean),
|
|
168
|
+
lastDeployTime: item.status?.lastDeployTime,
|
|
169
|
+
lastBuildDuration,
|
|
170
|
+
lastBuildError: lastBuild?.error,
|
|
171
|
+
hasPendingChanges: item.status?.hasPendingChanges,
|
|
172
|
+
triggerMode: item.status?.triggerMode,
|
|
173
|
+
queued: item.status?.queued,
|
|
174
|
+
pid: rawPid ? Number(rawPid) : undefined,
|
|
175
|
+
conditions,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function buildStatus(resources: TiltResource[]): TiltStatus {
|
|
180
|
+
const errors = resources.filter((r) => r.runtimeStatus === 'error');
|
|
181
|
+
const healthy = resources.filter((r) => r.runtimeStatus === 'ok');
|
|
182
|
+
const pending = resources.filter((r) => r.runtimeStatus === 'pending');
|
|
183
|
+
const active = resources.filter((r) => !r.isDisabled && r.runtimeStatus !== 'not_applicable');
|
|
184
|
+
return {
|
|
185
|
+
resources,
|
|
186
|
+
errors,
|
|
187
|
+
healthy,
|
|
188
|
+
pending,
|
|
189
|
+
allHealthy: active.length > 0 && errors.length === 0 && pending.length === 0,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── TiltClient ───────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
export interface TiltClientOptions {
|
|
196
|
+
/** Hostname of the Tilt instance. Default: '127.0.0.1' */
|
|
197
|
+
host?: string;
|
|
198
|
+
/** Timeout for HTTP requests in ms. Default: 5000 */
|
|
199
|
+
timeoutMs?: number;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export class TiltClient {
|
|
203
|
+
private readonly baseUrl: string;
|
|
204
|
+
private readonly port: number;
|
|
205
|
+
private readonly timeoutMs: number;
|
|
206
|
+
private ws: WebSocket | null = null;
|
|
207
|
+
private watchCallback: TiltWatchCallback | null = null;
|
|
208
|
+
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
209
|
+
private closed = false;
|
|
210
|
+
|
|
211
|
+
constructor(port: number, options?: TiltClientOptions) {
|
|
212
|
+
const host = options?.host ?? '127.0.0.1';
|
|
213
|
+
this.port = port;
|
|
214
|
+
this.baseUrl = `http://${host}:${port}`;
|
|
215
|
+
this.timeoutMs = options?.timeoutMs ?? 5000;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Check if Tilt is reachable at this port. */
|
|
219
|
+
async isReachable(): Promise<boolean> {
|
|
220
|
+
try {
|
|
221
|
+
const resp = await fetch(`${this.baseUrl}/api/websocket_token`, {
|
|
222
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
223
|
+
});
|
|
224
|
+
return resp.ok;
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Fetch all resources from this Tilt instance. */
|
|
231
|
+
async getResources(): Promise<TiltResource[]> {
|
|
232
|
+
const resp = await fetch(`${this.baseUrl}/api/v1alpha1/uiresources`, {
|
|
233
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
234
|
+
});
|
|
235
|
+
if (!resp.ok) {
|
|
236
|
+
throw new Error(`Tilt API error: ${resp.status} ${resp.statusText}`);
|
|
237
|
+
}
|
|
238
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
239
|
+
const data = (await resp.json()) as { items?: any[] };
|
|
240
|
+
return (data.items ?? [])
|
|
241
|
+
.filter((item: { metadata?: { name?: string } }) => item.metadata?.name && item.metadata.name !== '(Tiltfile)')
|
|
242
|
+
.map((item: unknown) => parseUIResource(item, this.port));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Fetch resources and return a structured status summary. */
|
|
246
|
+
async getStatus(): Promise<TiltStatus> {
|
|
247
|
+
const resources = await this.getResources();
|
|
248
|
+
return buildStatus(resources);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Get a single resource by name. */
|
|
252
|
+
async getResource(name: string): Promise<TiltResource | null> {
|
|
253
|
+
const resources = await this.getResources();
|
|
254
|
+
return resources.find((r) => r.name === name) ?? null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Trigger a resource update (equivalent to pressing the trigger button in Tilt UI). */
|
|
258
|
+
async triggerResource(name: string): Promise<void> {
|
|
259
|
+
const resp = await fetch(`${this.baseUrl}/api/trigger`, {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
headers: { 'Content-Type': 'application/json' },
|
|
262
|
+
body: JSON.stringify({ manifest_names: [name], build_reason: 16 /* manual */ }),
|
|
263
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
264
|
+
});
|
|
265
|
+
if (!resp.ok) {
|
|
266
|
+
throw new Error(`Tilt trigger error: ${resp.status} ${resp.statusText}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Watch for resource updates via WebSocket.
|
|
272
|
+
* Returns an unsubscribe function.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* const stop = await tilt.watch((event) => {
|
|
277
|
+
* for (const r of event.resources) {
|
|
278
|
+
* if (r.runtimeStatus === 'error') console.error(`${r.name} failed!`);
|
|
279
|
+
* }
|
|
280
|
+
* });
|
|
281
|
+
* // Later:
|
|
282
|
+
* stop();
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
async watch(callback: TiltWatchCallback): Promise<() => void> {
|
|
286
|
+
this.watchCallback = callback;
|
|
287
|
+
await this.connectWS();
|
|
288
|
+
return () => {
|
|
289
|
+
this.watchCallback = null;
|
|
290
|
+
this.disconnectWS();
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Close all connections. */
|
|
295
|
+
close(): void {
|
|
296
|
+
this.closed = true;
|
|
297
|
+
this.watchCallback = null;
|
|
298
|
+
this.disconnectWS();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── WebSocket internals ──────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
private async connectWS(): Promise<void> {
|
|
304
|
+
if (this.ws) return;
|
|
305
|
+
|
|
306
|
+
const tokenResp = await fetch(`${this.baseUrl}/api/websocket_token`, {
|
|
307
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
308
|
+
});
|
|
309
|
+
if (!tokenResp.ok) throw new Error('Could not get WebSocket token from Tilt');
|
|
310
|
+
const token = (await tokenResp.text()).trim();
|
|
311
|
+
|
|
312
|
+
const wsUrl = `ws://127.0.0.1:${this.port}/ws/view?token=${token}`;
|
|
313
|
+
const ws = new WebSocket(wsUrl);
|
|
314
|
+
|
|
315
|
+
ws.onmessage = (event: MessageEvent) => {
|
|
316
|
+
try {
|
|
317
|
+
const data = JSON.parse(typeof event.data === 'string' ? event.data : event.data.toString());
|
|
318
|
+
this.handleWSMessage(data);
|
|
319
|
+
} catch {
|
|
320
|
+
/* ignore malformed */
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
ws.onerror = () => {
|
|
325
|
+
/* triggers onclose */
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
ws.onclose = () => {
|
|
329
|
+
this.ws = null;
|
|
330
|
+
if (this.watchCallback && !this.closed) {
|
|
331
|
+
this.reconnectTimer = setTimeout(() => {
|
|
332
|
+
this.reconnectTimer = null;
|
|
333
|
+
void this.connectWS();
|
|
334
|
+
}, 3000);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
this.ws = ws;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private disconnectWS(): void {
|
|
342
|
+
if (this.reconnectTimer) {
|
|
343
|
+
clearTimeout(this.reconnectTimer);
|
|
344
|
+
this.reconnectTimer = null;
|
|
345
|
+
}
|
|
346
|
+
if (this.ws) {
|
|
347
|
+
try {
|
|
348
|
+
this.ws.close();
|
|
349
|
+
} catch {
|
|
350
|
+
/* already closed */
|
|
351
|
+
}
|
|
352
|
+
this.ws = null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
357
|
+
private handleWSMessage(data: any): void {
|
|
358
|
+
if (!this.watchCallback) return;
|
|
359
|
+
|
|
360
|
+
const resources: TiltResource[] = [];
|
|
361
|
+
const logs: Array<{ resourceName?: string | undefined; text: string }> = [];
|
|
362
|
+
|
|
363
|
+
// Parse resources
|
|
364
|
+
if (data.uiResources && Array.isArray(data.uiResources)) {
|
|
365
|
+
for (const item of data.uiResources) {
|
|
366
|
+
if (item.metadata?.name && item.metadata.name !== '(Tiltfile)') {
|
|
367
|
+
resources.push(parseUIResource(item, this.port));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Parse logs
|
|
373
|
+
if (data.logList?.segments && Array.isArray(data.logList.segments)) {
|
|
374
|
+
const spans = data.logList.spans as Record<string, { manifestName?: string }> | undefined;
|
|
375
|
+
for (const seg of data.logList.segments as Array<{ spanId?: string; text?: string }>) {
|
|
376
|
+
const text = seg.text?.trimEnd();
|
|
377
|
+
if (!text) continue;
|
|
378
|
+
const resourceName = seg.spanId && spans?.[seg.spanId]?.manifestName;
|
|
379
|
+
logs.push({
|
|
380
|
+
resourceName: resourceName && resourceName !== '(Tiltfile)' ? resourceName : undefined,
|
|
381
|
+
text,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (resources.length > 0 || logs.length > 0) {
|
|
387
|
+
this.watchCallback({ resources, logs });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ── Convenience functions ────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* One-shot query: get the current status of all resources from a Tilt instance.
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```ts
|
|
399
|
+
* import { queryTilt } from '@tilt-launcher/sdk';
|
|
400
|
+
*
|
|
401
|
+
* const status = await queryTilt(10350);
|
|
402
|
+
* if (!status.allHealthy) {
|
|
403
|
+
* for (const err of status.errors) {
|
|
404
|
+
* console.error(`${err.name}: ${err.lastBuildError ?? err.runtimeStatus}`);
|
|
405
|
+
* }
|
|
406
|
+
* process.exit(1);
|
|
407
|
+
* }
|
|
408
|
+
* ```
|
|
409
|
+
*/
|
|
410
|
+
export async function queryTilt(port: number, options?: TiltClientOptions): Promise<TiltStatus> {
|
|
411
|
+
const client = new TiltClient(port, options);
|
|
412
|
+
try {
|
|
413
|
+
return await client.getStatus();
|
|
414
|
+
} finally {
|
|
415
|
+
client.close();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Stream resource updates from a Tilt instance.
|
|
421
|
+
* Returns an unsubscribe function.
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* ```ts
|
|
425
|
+
* import { watchTilt } from '@tilt-launcher/sdk';
|
|
426
|
+
*
|
|
427
|
+
* const stop = await watchTilt(10350, (event) => {
|
|
428
|
+
* console.log(`${event.resources.length} resources updated`);
|
|
429
|
+
* });
|
|
430
|
+
*
|
|
431
|
+
* // Stop watching after 30s
|
|
432
|
+
* setTimeout(stop, 30_000);
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
export async function watchTilt(
|
|
436
|
+
port: number,
|
|
437
|
+
callback: TiltWatchCallback,
|
|
438
|
+
options?: TiltClientOptions,
|
|
439
|
+
): Promise<() => void> {
|
|
440
|
+
const client = new TiltClient(port, options);
|
|
441
|
+
const unsub = await client.watch(callback);
|
|
442
|
+
return () => {
|
|
443
|
+
unsub();
|
|
444
|
+
client.close();
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ── Adapter: TiltResource → CachedResource (for TiltManagerSDK compat) ──
|
|
449
|
+
|
|
450
|
+
/** @internal Convert TiltResource to CachedResource for backward compatibility */
|
|
451
|
+
export function tiltResourceToCached(r: TiltResource): CachedResource {
|
|
452
|
+
return {
|
|
453
|
+
name: r.name,
|
|
454
|
+
label: r.label,
|
|
455
|
+
category: r.category,
|
|
456
|
+
type: r.type,
|
|
457
|
+
endpoint: r.endpoint,
|
|
458
|
+
port: r.port,
|
|
459
|
+
runtimeStatus: r.runtimeStatus,
|
|
460
|
+
isDisabled: r.isDisabled,
|
|
461
|
+
resourceKind: r.resourceKind,
|
|
462
|
+
updateStatus: r.updateStatus,
|
|
463
|
+
waitingReason: r.waitingReason,
|
|
464
|
+
waitingOn: r.waitingOn,
|
|
465
|
+
lastDeployTime: r.lastDeployTime,
|
|
466
|
+
lastBuildDuration: r.lastBuildDuration,
|
|
467
|
+
lastBuildError: r.lastBuildError,
|
|
468
|
+
hasPendingChanges: r.hasPendingChanges,
|
|
469
|
+
triggerMode: r.triggerMode,
|
|
470
|
+
queued: r.queued,
|
|
471
|
+
pid: r.pid,
|
|
472
|
+
conditions: r.conditions,
|
|
473
|
+
};
|
|
474
|
+
}
|