diff --git a/.cursor/rules/cli.mdc b/.cursor/rules/cli.mdc index 93b33cb..2609372 100644 --- a/.cursor/rules/cli.mdc +++ b/.cursor/rules/cli.mdc @@ -5,4 +5,5 @@ globs: *.(ts|tsx) - This is a CLI program written in Deno using the `commander` library and `@commander-js/extra-typings`. - When using node globals like `console.log`, `setTimeout`, `process`, etc, import them from the corresponding explicit `node:console`, `node:timers`, and `node:process`, etc. -- Where possible, use the `openapi-typescript`/`openapi-fetch` client instead of writing raw fetch calls. \ No newline at end of file +- Where possible, use the `openapi-typescript`/`openapi-fetch` client instead of writing raw fetch calls. +- renders the content as uppercase so tests should assert on the uppercase text. \ No newline at end of file diff --git a/deno.lock b/deno.lock index 17cee55..f78c74a 100644 --- a/deno.lock +++ b/deno.lock @@ -4,6 +4,7 @@ "jsr:@std/assert@1": "1.0.9", "jsr:@std/internal@^1.0.5": "1.0.5", "npm:@commander-js/extra-typings@^12.1.0": "12.1.0_commander@12.1.0", + "npm:@inkjs/ui@2": "2.0.0_ink@5.1.0__@types+react@18.3.18__react@18.3.1_@types+react@18.3.18_react@18.3.1", "npm:@inquirer/prompts@^5.1.2": "5.5.0", "npm:@types/ms@~0.7.34": "0.7.34", "npm:@types/react@^18.3.12": "18.3.18", @@ -18,9 +19,11 @@ "npm:dotenv@^16.4.5": "16.4.7", "npm:ink-confirm-input@2": "2.0.0_ink@5.1.0__@types+react@18.3.18__react@18.3.1_react@18.3.1_@types+react@18.3.18", "npm:ink-spinner@5": "5.0.0_ink@5.1.0__@types+react@18.3.18__react@18.3.1_react@18.3.1_@types+react@18.3.18", + "npm:ink-testing-library@4": "4.0.0_@types+react@18.3.18", "npm:ink-text-input@6": "6.0.0_ink@5.1.0__@types+react@18.3.18__react@18.3.1_react@18.3.1_@types+react@18.3.18", "npm:ink@^5.0.1": "5.1.0_@types+react@18.3.18_react@18.3.1", "npm:inquirer@^10.1.2": "10.2.2", + "npm:little-date@1": "1.0.0", "npm:ms@^2.1.3": "2.1.3", "npm:node-fetch@^3.3.2": "3.3.2", "npm:openapi-fetch@~0.11.1": "0.11.3", @@ -54,6 +57,12 @@ "is-fullwidth-code-point@4.0.0" ] }, + "@babel/runtime@7.26.9": { + "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", + "dependencies": [ + "regenerator-runtime" + ] + }, "@colors/colors@1.5.0": { "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" }, @@ -63,6 +72,16 @@ "commander" ] }, + "@inkjs/ui@2.0.0_ink@5.1.0__@types+react@18.3.18__react@18.3.1_@types+react@18.3.18_react@18.3.1": { + "integrity": "sha512-5+8fJmwtF9UvikzLfph9sA+LS+l37Ij/szQltkuXLOAXwNkBX9innfzh4pLGXIB59vKEQUtc6D4qGvhD7h3pAg==", + "dependencies": [ + "chalk@5.4.1", + "cli-spinners@3.2.0", + "deepmerge", + "figures", + "ink@5.1.0_@types+react@18.3.18_react@18.3.1" + ] + }, "@inquirer/checkbox@2.5.0": { "integrity": "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==", "dependencies": [ @@ -371,6 +390,9 @@ "cli-spinners@2.9.2": { "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==" }, + "cli-spinners@3.2.0": { + "integrity": "sha512-pXftdQloMZzjCr3pCTIRniDcys6dDzgpgVhAHHk6TKBDbRuP1MkuetTF5KSv4YUutbOPa7+7ZrAJ2kVtbMqyXA==" + }, "cli-table3@0.6.5": { "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dependencies": [ @@ -437,9 +459,18 @@ "data-uri-to-buffer@4.0.1": { "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" }, + "date-fns@2.30.0": { + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": [ + "@babel/runtime" + ] + }, "dayjs@1.11.13": { "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" }, + "deepmerge@4.3.1": { + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" + }, "delayed-stream@1.0.0": { "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, @@ -479,6 +510,12 @@ "web-streams-polyfill" ] }, + "figures@6.1.0": { + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dependencies": [ + "is-unicode-supported@2.1.0" + ] + }, "follow-redirects@1.15.9": { "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" }, @@ -524,11 +561,17 @@ "ink-spinner@5.0.0_ink@5.1.0__@types+react@18.3.18__react@18.3.1_react@18.3.1_@types+react@18.3.18": { "integrity": "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA==", "dependencies": [ - "cli-spinners", + "cli-spinners@2.9.2", "ink@5.1.0_@types+react@18.3.18_react@18.3.1", "react@18.3.1" ] }, + "ink-testing-library@4.0.0_@types+react@18.3.18": { + "integrity": "sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==", + "dependencies": [ + "@types/react" + ] + }, "ink-text-input@3.3.0_ink@2.7.1__@types+react@18.3.18__react@16.14.0_react@16.14.0_@types+react@18.3.18": { "integrity": "sha512-gO4wrOf2ie3YuEARTIwGlw37lMjFn3Gk6CKIDrMlHb46WFMagZU7DplohjM24zynlqfnXA5UDEIfC2NBcvD8kg==", "dependencies": [ @@ -652,6 +695,12 @@ "js-tokens@4.0.0": { "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "little-date@1.0.0": { + "integrity": "sha512-41T/ktcwPzxC0OJ8E3wmaK0E1DL/QNR3n30kB9Dw6Ni6Eud24It8LZm70jK8lvDd+Mg+961fzKDcF6SQRL25cQ==", + "dependencies": [ + "date-fns" + ] + }, "lodash.throttle@4.1.1": { "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" }, @@ -746,7 +795,7 @@ "dependencies": [ "chalk@5.4.1", "cli-cursor@5.0.0", - "cli-spinners", + "cli-spinners@2.9.2", "is-interactive", "is-unicode-supported@2.1.0", "log-symbols", @@ -819,6 +868,9 @@ "loose-envify" ] }, + "regenerator-runtime@0.14.1": { + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "restore-cursor@2.0.0": { "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", "dependencies": [ @@ -1070,6 +1122,7 @@ "packageJson": { "dependencies": [ "npm:@commander-js/extra-typings@^12.1.0", + "npm:@inkjs/ui@2", "npm:@inquirer/prompts@^5.1.2", "npm:@types/ms@~0.7.34", "npm:@types/react@^18.3.12", @@ -1084,9 +1137,11 @@ "npm:dotenv@^16.4.5", "npm:ink-confirm-input@2", "npm:ink-spinner@5", + "npm:ink-testing-library@4", "npm:ink-text-input@6", "npm:ink@^5.0.1", "npm:inquirer@^10.1.2", + "npm:little-date@1", "npm:ms@^2.1.3", "npm:node-fetch@^3.3.2", "npm:openapi-fetch@~0.11.1", diff --git a/package.json b/package.json index d733fb1..6540102 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@commander-js/extra-typings": "^12.1.0", + "@inkjs/ui": "^2.0.0", "@inquirer/prompts": "^5.1.2", "@types/ms": "^0.7.34", "axios": "^1.7.2", @@ -22,8 +23,10 @@ "ink": "^5.0.1", "ink-confirm-input": "^2.0.0", "ink-spinner": "^5.0.0", + "ink-testing-library": "^4.0.0", "ink-text-input": "^6.0.0", "inquirer": "^10.1.2", + "little-date": "^1.0.0", "ms": "^2.1.3", "node-fetch": "^3.3.2", "openapi-fetch": "^0.11.1", @@ -45,5 +48,5 @@ "peerDependencies": { "typescript": "^5.6.2" }, - "version": "0.1.44" + "version": "0.1.46" } diff --git a/src/lib/clusters/clusters.tsx b/src/lib/clusters/clusters.tsx index 04792c7..bdba2c3 100644 --- a/src/lib/clusters/clusters.tsx +++ b/src/lib/clusters/clusters.tsx @@ -1,8 +1,10 @@ import type { Command } from "@commander-js/extra-typings"; -import * as console from "node:console"; -import { clearInterval, setInterval, setTimeout } from "node:timers"; +import { Badge } from "@inkjs/ui"; import { Box, render, Text, useApp } from "ink"; import Spinner from "ink-spinner"; +import * as console from "node:console"; +import { clearInterval, setInterval, setTimeout } from "node:timers"; +// biome-ignore lint/style/useImportType: import React, { useEffect, useState } from "react"; import yaml from "yaml"; import { apiClient } from "../../apiClient.ts"; @@ -14,6 +16,7 @@ import { KUBECONFIG_PATH, syncKubeconfig, } from "./kubeconfig.ts"; +import type { UserFacingCluster } from "./types.ts"; export function registerClusters(program: Command) { const clusters = program @@ -96,33 +99,79 @@ export function registerClusters(program: Command) { function ClusterDisplay({ clusters, }: { - clusters: Array<{ - name: string; - kubernetes_api_url: string; - kubernetes_namespace: string; - }>; + clusters: Array; }) { return ( - + {clusters.map((cluster, index) => ( - - - - - + ))} ); } +const ClusterRow = ({ cluster }: { cluster: UserFacingCluster }) => { + if (cluster.contract) { + return ; + } + + return ( + + + + + + ); +}; + +const COLUMN_WIDTH = 11; + +const ClusterRowWithContracts = ( + { cluster }: { + cluster: UserFacingCluster; + }, +) => { + if (!cluster.contract) { + return null; + } + + return ( + + + {cluster.contract.id} + + + + + + + + + + + ); +}; + async function listClustersAction({ returnJson, token, @@ -156,11 +205,11 @@ async function listClustersAction({ } else { render( ({ - name: cluster.name, - kubernetes_api_url: cluster.kubernetes_api_url || "", - kubernetes_namespace: cluster.kubernetes_namespace || "", - }))} + clusters={data.data.filter(( + cluster, + ): cluster is UserFacingCluster => + cluster.contract?.status === "active" || !cluster.contract + )} />, ); } diff --git a/src/lib/clusters/mock.ts b/src/lib/clusters/mock.ts new file mode 100644 index 0000000..e1752a3 --- /dev/null +++ b/src/lib/clusters/mock.ts @@ -0,0 +1,73 @@ +import { MOCK_CONTRACTS } from "../contracts/mock.ts"; +import type { UserFacingCluster } from "./types.ts"; + +export const MOCK_CLUSTERS: UserFacingCluster[] = [ + // 1. Cluster with upcoming single order contract + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-1.example.com", + name: "upcoming-single-cluster", + kubernetes_namespace: "default", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + contract: MOCK_CONTRACTS[0] as UserFacingCluster["contract"], // upcoming-single contract + }, + + // 2. Cluster with active multi-order contract + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-2.example.com", + name: "active-multi-cluster", + kubernetes_namespace: "prod", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + contract: MOCK_CONTRACTS[1] as UserFacingCluster["contract"], // active-multi contract + }, + + // 3. Cluster with upcoming multi-order contract + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-3.example.com", + name: "upcoming-multi-cluster", + kubernetes_namespace: "staging", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + contract: MOCK_CONTRACTS[2] as UserFacingCluster["contract"], // upcoming-multi contract + }, + + // 4. Cluster with expired contract + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-4.example.com", + name: "expired-cluster", + kubernetes_namespace: "default", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + contract: MOCK_CONTRACTS[3] as UserFacingCluster["contract"], // expired contract + }, + + // 5. Cluster with mixed state contract + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-5.example.com", + name: "mixed-states-cluster", + kubernetes_namespace: "mixed", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + contract: MOCK_CONTRACTS[4] as UserFacingCluster["contract"], // mixed-states contract + }, + + // 6. Cluster with colocated contract + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-6.example.com", + name: "colocated-cluster", + kubernetes_namespace: "colocated", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + contract: MOCK_CONTRACTS[5] as UserFacingCluster["contract"], // colocated contract + }, + + // 7. Cluster without contract (for testing non-contract clusters) + { + object: "kubernetes_cluster", + kubernetes_api_url: "https://k8s-api-7.example.com", + name: "no-contract-cluster", + kubernetes_namespace: "default", + kubernetes_ca_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", + }, +]; diff --git a/src/lib/clusters/types.ts b/src/lib/clusters/types.ts new file mode 100644 index 0000000..903629a --- /dev/null +++ b/src/lib/clusters/types.ts @@ -0,0 +1,20 @@ +export interface UserFacingCluster { + object: "kubernetes_cluster"; + kubernetes_api_url?: string; + name: string; + kubernetes_namespace: string; + kubernetes_ca_cert?: string; + contract?: { + object: "contract"; + status: "active"; + id: string; + created_at: string; + instance_type: string; + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + }; +} diff --git a/src/lib/contracts/ContractDisplay.tsx b/src/lib/contracts/ContractDisplay.tsx index dc71c09..a76b26d 100644 --- a/src/lib/contracts/ContractDisplay.tsx +++ b/src/lib/contracts/ContractDisplay.tsx @@ -1,13 +1,75 @@ +import { Badge } from "@inkjs/ui"; import { Box, Text } from "ink"; -import type { Contract } from "./types.ts"; -import { Row } from "../Row.tsx"; -import dayjs from "npm:dayjs@1.11.13"; +import { formatDateRange } from "little-date"; import ms from "ms"; -import React from "react"; +// biome-ignore lint/style/useImportType: +import * as React from "react"; +import { Row } from "../Row.tsx"; import { GPUS_PER_NODE } from "../constants.ts"; +import type { Contract } from "./types.ts"; + +interface IntervalData { + /** + * e.g. "1d" or "7w" + */ + durationString: string; + /** + * e.g. "Jan 1 → Jan 2" + */ + dateRangeLabel: string; + /** + * The number of GPUs in the interval. + */ + quantity: number; + instanceType: string; + start: Date; + end: Date; + state: "Upcoming" | "Active" | "Expired"; +} + +export function createIntervalData( + shape: Contract["shape"], + instanceType: string, +): IntervalData[] { + const now = new Date(); + + return shape.intervals.slice(0, -1).map((interval, index) => { + const start = new Date(interval); + const end = new Date(shape.intervals[index + 1]); + const duration = end.getTime() - start.getTime(); + const state = start > now ? "Upcoming" : end < now ? "Expired" : "Active"; + + return { + dateRangeLabel: formatDateRange(start, end, { separator: "→" }), + durationString: ms(duration), + quantity: shape.quantities[index], + instanceType, + start, + end, + state, + }; + }); +} + +export function IntervalDisplay({ data }: { data: IntervalData }) { + const isDimmed = data.state === "Expired"; + + return ( + + + {data.quantity * GPUS_PER_NODE} gpus + + + + {data.dateRangeLabel} + + ({data.durationString}) + [{data.state}] + + ); +} -const STARTED = "▶"; -const UPCOMING = "⏸"; +const COLUMN_WIDTH = 12; export function ContractDisplay(props: { contract: Contract }) { if (props.contract.status === "pending") { @@ -15,60 +77,71 @@ export function ContractDisplay(props: { contract: Contract }) { } const startsAt = new Date(props.contract.shape.intervals[0]); - const statusIcon = startsAt < new Date() ? STARTED : UPCOMING; + const endsAt = new Date( + props.contract.shape.intervals[props.contract.shape.intervals.length - 1], + ); + const now = new Date(); + let color: React.ComponentProps["color"] | undefined; + let statusIcon: React.ReactNode; + if (startsAt > now) { + statusIcon = Upcoming; + color = "green"; + } else if (endsAt < now) { + color = "gray"; + statusIcon = Expired; + } else { + color = "cyan"; + statusIcon = Active; + } + + const intervalData = createIntervalData( + props.contract.shape, + props.contract.instance_type, + ); return ( - {statusIcon} - {props.contract.id} + + {statusIcon} + + + {props.contract.id} + - - 0 - ? props.contract.colocate_with.join(", ") - : "-"} - /> - - - {props.contract.shape.intervals.slice(0, -1).map((interval) => { - const start = new Date(interval); - const next = new Date( - props.contract.shape.intervals[ - props.contract.shape.intervals.indexOf(interval) + 1 - ], - ); - - const duration = next.getTime() - start.getTime(); - const startString = dayjs(start).format("MMM D h:mm a").toLowerCase(); - const nextString = dayjs(next).format("MMM D h:mm a").toLowerCase(); - const durationString = ms(duration); + + + {props.contract.colocate_with.length > 0 && ( + + )} - const quantity = props.contract.shape.quantities[ - props.contract.shape.intervals.indexOf(interval) - ]; - - return ( - - - - {quantity * GPUS_PER_NODE} x {props.contract.instance_type} - {" "} - (gpus) - - - - - {startString} - - {nextString} + + {intervalData.map((data, index) => { + return ( + + {index === 0 && ( + + Orders + + )} + - ({durationString}) - - ); - })} + ); + })} + ); @@ -89,9 +162,12 @@ export function ContractList(props: { contracts: Contract[] }) { } return ( - + {props.contracts.map((contract) => ( - + ))} ); diff --git a/src/lib/contracts/mock.ts b/src/lib/contracts/mock.ts new file mode 100644 index 0000000..fbef5f9 --- /dev/null +++ b/src/lib/contracts/mock.ts @@ -0,0 +1,110 @@ +import type { Contract } from "./types.ts"; + +const now = new Date(); +const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000); +const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); +const nextMonth = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); +const twoMonths = new Date(now.getTime() + 60 * 24 * 60 * 60 * 1000); +const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); +const lastWeek = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + +export const MOCK_CONTRACTS: Contract[] = [ + // 1. Upcoming contract with 1 order + { + object: "contract", + status: "active", + id: "upcoming-single", + created_at: now.toISOString(), + instance_type: "a100-80gb", + shape: { + intervals: [nextWeek.toISOString(), nextMonth.toISOString()], + quantities: [2], + }, + colocate_with: [], + }, + + // 2. Active contract with many orders + { + object: "contract", + status: "active", + id: "active-multi", + created_at: lastWeek.toISOString(), + instance_type: "a100-40gb", + shape: { + intervals: [ + lastWeek.toISOString(), + yesterday.toISOString(), + tomorrow.toISOString(), + nextWeek.toISOString(), + ], + quantities: [1, 3, 2], + }, + colocate_with: ["cluster-123"], + }, + + // 3. Upcoming contract with many orders + { + object: "contract", + status: "active", + id: "upcoming-multi", + created_at: now.toISOString(), + instance_type: "a100-80gb", + shape: { + intervals: [ + nextWeek.toISOString(), + nextMonth.toISOString(), + twoMonths.toISOString(), + ], + quantities: [1, 4], + }, + colocate_with: [], + }, + + // 4. Expired contract (for testing expired state) + { + object: "contract", + status: "active", + id: "expired", + created_at: lastWeek.toISOString(), + instance_type: "a100-40gb", + shape: { + intervals: [lastWeek.toISOString(), yesterday.toISOString()], + quantities: [2], + }, + colocate_with: [], + }, + + // 5. Mixed state contract (some intervals expired, some active, some upcoming) + { + object: "contract", + status: "active", + id: "mixed-states", + created_at: lastWeek.toISOString(), + instance_type: "h100", + shape: { + intervals: [ + lastWeek.toISOString(), + yesterday.toISOString(), + tomorrow.toISOString(), + nextWeek.toISOString(), + nextMonth.toISOString(), + ], + quantities: [1, 2, 3, 4], + }, + colocate_with: ["cluster-456"], + }, + + // 6. Contract with colocate_with + { + object: "contract", + status: "active", + id: "colocated", + created_at: now.toISOString(), + instance_type: "a100-80gb", + shape: { + intervals: [nextWeek.toISOString(), nextMonth.toISOString()], + quantities: [2], + }, + colocate_with: ["cluster-789", "cluster-012"], + }, +]; diff --git a/src/schema.ts b/src/schema.ts index 1657322..70322f9 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -46,13 +46,13 @@ export interface paths { get: operations["getV0GridsById"]; put?: never; post?: never; - delete?: never; + delete: operations["deleteV0GridsById"]; options?: never; head?: never; patch?: never; trace?: never; }; - "/v0/grids/{id}/grids/{id}/disable": { + "/v0/grids/{id}/disable": { parameters: { query?: never; header?: never; @@ -65,10 +65,10 @@ export interface paths { delete?: never; options?: never; head?: never; - patch: operations["patchV0GridsByIdGridsByIdDisable"]; + patch: operations["patchV0GridsByIdDisable"]; trace?: never; }; - "/v0/grids/{id}/grids/{id}/enable": { + "/v0/grids/{id}/enable": { parameters: { query?: never; header?: never; @@ -81,51 +81,83 @@ export interface paths { delete?: never; options?: never; head?: never; - patch: operations["patchV0GridsByIdGridsByIdEnable"]; + patch: operations["patchV0GridsByIdEnable"]; trace?: never; }; - "/v0/grids/{id}/grids/{id}": { + "/v0/procurements/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + get: operations["getV0ProcurementsById"]; + put: operations["putV0ProcurementsById"]; + post: operations["postV0ProcurementsById"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/procurements": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getV0Procurements"]; + put?: never; + post: operations["postV0Procurements"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/vm/logs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getV0VmLogs"]; put?: never; post?: never; - delete: operations["deleteV0GridsByIdGridsById"]; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v0/procurements/{id}": { + "/v0/vm/script": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0ProcurementsById"]; - put: operations["putV0ProcurementsById"]; - post: operations["postV0ProcurementsById"]; + get?: never; + put?: never; + post: operations["postV0VmScript"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v0/procurements": { + "/v0/vm/nodes": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0Procurements"]; + get: operations["getV0VmNodes"]; put?: never; - post: operations["postV0Procurements"]; + post?: never; delete?: never; options?: never; head?: never; @@ -721,7 +753,7 @@ export interface operations { }; }; }; - patchV0GridsByIdGridsByIdDisable: { + deleteV0GridsById: { parameters: { query?: never; header?: never; @@ -740,7 +772,7 @@ export interface operations { }; }; }; - patchV0GridsByIdGridsByIdEnable: { + patchV0GridsByIdDisable: { parameters: { query?: never; header?: never; @@ -759,7 +791,7 @@ export interface operations { }; }; }; - deleteV0GridsByIdGridsById: { + patchV0GridsByIdEnable: { parameters: { query?: never; header?: never; @@ -1290,6 +1322,69 @@ export interface operations { }; }; }; + getV0VmLogs: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + postV0VmScript: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + script: string; + }; + "multipart/form-data": { + script: string; + }; + "text/plain": { + script: string; + }; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getV0VmNodes: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; getV0Prices: { parameters: { query?: { @@ -1468,153 +1563,7 @@ export interface operations { headers: { [name: string]: unknown; }; - content: { - "application/json": { - data: { - /** @constant */ - object: "order"; - id: string; - side: "buy" | "sell"; - status: "pending" | "rejected" | "open" | "cancelled" | "filled" | "expired"; - /** @description The instance type. */ - instance_type: string; - /** @description The number of nodes. */ - quantity: number | string; - /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ - start_at: string; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description Price in cents (1 = $0.01) */ - price: number | string; - flags: { - /** @description If true, this will be a market order. */ - market?: boolean; - /** @description If true, this is a post-only order. */ - post_only?: boolean; - /** @description If true, this is an immediate-or-cancel order. */ - ioc?: boolean; - }; - executed: boolean; - executed_at?: string; - /** @description Execution price in cents (1 = $0.01) */ - execution_price?: number | string; - cancelled: boolean; - cancelled_at?: string; - colocate_with?: string[]; - created_at: string; - }[]; - has_more: boolean; - /** @constant */ - object: "list"; - }; - "multipart/form-data": { - data: { - /** @constant */ - object: "order"; - id: string; - side: "buy" | "sell"; - status: "pending" | "rejected" | "open" | "cancelled" | "filled" | "expired"; - /** @description The instance type. */ - instance_type: string; - /** @description The number of nodes. */ - quantity: number | string; - /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ - start_at: string; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description Price in cents (1 = $0.01) */ - price: number | string; - flags: { - /** @description If true, this will be a market order. */ - market?: boolean; - /** @description If true, this is a post-only order. */ - post_only?: boolean; - /** @description If true, this is an immediate-or-cancel order. */ - ioc?: boolean; - }; - executed: boolean; - executed_at?: string; - /** @description Execution price in cents (1 = $0.01) */ - execution_price?: number | string; - cancelled: boolean; - cancelled_at?: string; - colocate_with?: string[]; - created_at: string; - }[]; - has_more: boolean; - /** @constant */ - object: "list"; - }; - "text/plain": { - data: { - /** @constant */ - object: "order"; - id: string; - side: "buy" | "sell"; - status: "pending" | "rejected" | "open" | "cancelled" | "filled" | "expired"; - /** @description The instance type. */ - instance_type: string; - /** @description The number of nodes. */ - quantity: number | string; - /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ - start_at: string; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description Price in cents (1 = $0.01) */ - price: number | string; - flags: { - /** @description If true, this will be a market order. */ - market?: boolean; - /** @description If true, this is a post-only order. */ - post_only?: boolean; - /** @description If true, this is an immediate-or-cancel order. */ - ioc?: boolean; - }; - executed: boolean; - executed_at?: string; - /** @description Execution price in cents (1 = $0.01) */ - execution_price?: number | string; - cancelled: boolean; - cancelled_at?: string; - colocate_with?: string[]; - created_at: string; - }[]; - has_more: boolean; - /** @constant */ - object: "list"; - }; - }; - }; - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - object: "error"; - /** @constant */ - code: "internal_server"; - message: string; - details?: Record; - }; - "multipart/form-data": { - /** @constant */ - object: "error"; - /** @constant */ - code: "internal_server"; - message: string; - details?: Record; - }; - "text/plain": { - /** @constant */ - object: "error"; - /** @constant */ - code: "internal_server"; - message: string; - details?: Record; - }; - }; + content?: never; }; }; }; @@ -2369,6 +2318,30 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }[]; has_more: boolean; /** @constant */ @@ -2382,6 +2355,30 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }[]; has_more: boolean; /** @constant */ @@ -2395,6 +2392,30 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }[]; has_more: boolean; /** @constant */ @@ -2504,10 +2525,58 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }; encrypted_token?: string; nonce?: string; ephemeral_pubkey?: string; + contracts?: ({ + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + })[]; })[]; has_more: boolean; /** @constant */ @@ -2534,10 +2603,58 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }; encrypted_token?: string; nonce?: string; ephemeral_pubkey?: string; + contracts?: ({ + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + })[]; })[]; has_more: boolean; /** @constant */ @@ -2564,10 +2681,58 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }; encrypted_token?: string; nonce?: string; ephemeral_pubkey?: string; + contracts?: ({ + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + })[]; })[]; has_more: boolean; /** @constant */ @@ -2718,10 +2883,58 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }; encrypted_token?: string; nonce?: string; ephemeral_pubkey?: string; + contracts?: ({ + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + })[]; }; "multipart/form-data": { /** @constant */ @@ -2743,10 +2956,58 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }; encrypted_token?: string; nonce?: string; ephemeral_pubkey?: string; + contracts?: ({ + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + })[]; }; "text/plain": { /** @constant */ @@ -2768,10 +3029,58 @@ export interface operations { name: string; kubernetes_namespace: string; kubernetes_ca_cert?: string; + contract?: { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }; encrypted_token?: string; nonce?: string; ephemeral_pubkey?: string; + contracts?: ({ + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + })[]; }; }; };