diff --git a/README.md b/README.md
index b778a2e..56de1cb 100644
--- a/README.md
+++ b/README.md
@@ -45,9 +45,9 @@ Releases are managed through GitHub Actions. To create a new release:
3. Click on the "Release" workflow
4. Click "Run workflow"
5. Select the version bump type:
- - `patch`: for backwards-compatible bug fixes (0.0.x)
- - `minor`: for backwards-compatible new features (0.x.0)
- - `major`: for breaking changes (x.0.0)
+ - `patch`: for backwards-compatible changes (0.0.x)
+ - `minor`: for breaking changes (0.x.0)
+ - `major`: for major breaking changes (x.0.0)
- `prerelease`: for pre-release versions (0.0.0-pre.timestamp)
6. Click "Run workflow" to start the release process
diff --git a/src/lib/contracts/ContractDisplay.tsx b/src/lib/contracts/ContractDisplay.tsx
index 05e78b7..61ddfc4 100644
--- a/src/lib/contracts/ContractDisplay.tsx
+++ b/src/lib/contracts/ContractDisplay.tsx
@@ -170,8 +170,8 @@ export function ContractList(props: { contracts: Contract[] }) {
- # Place a buy order to get started
- sf buy
+ # Buy a node to get started
+ sf nodes create
);
diff --git a/src/lib/orders/OrderDisplay.tsx b/src/lib/orders/OrderDisplay.tsx
index 71011cc..33e59a2 100644
--- a/src/lib/orders/OrderDisplay.tsx
+++ b/src/lib/orders/OrderDisplay.tsx
@@ -189,7 +189,7 @@ export function OrderDisplay(props: {
# Place an order to buy compute
- sf buy
+ sf nodes create
);
diff --git a/src/lib/zones.tsx b/src/lib/zones.tsx
index bbaf3b4..5b1785e 100644
--- a/src/lib/zones.tsx
+++ b/src/lib/zones.tsx
@@ -1,7 +1,11 @@
import * as console from "node:console";
import type { Command } from "@commander-js/extra-typings";
-import chalk from "chalk";
-import Table from "cli-table3";
+import {
+ differenceInDays,
+ differenceInHours,
+ differenceInMinutes,
+} from "date-fns";
+import dayjs from "dayjs";
import { Box, render, Text } from "ink";
import { apiClient } from "../apiClient.ts";
import { isLoggedIn } from "../helpers/config.ts";
@@ -15,41 +19,131 @@ import { isFeatureEnabled } from "./posthog.ts";
type ZoneInfo = components["schemas"]["node-api_ZoneInfo"];
-// Delivery type conversion similar to InstanceTypeMetadata pattern
-const DeliveryTypeMetadata: Record = {
- K8s: { displayName: "Kubernetes" },
- K8sNamespace: { displayName: "Kubernetes" },
- VM: { displayName: "Virtual Machine" },
-} as const;
+type CapacityMetrics = {
+ availableNow: number;
+ availableWithin1Day: { count: number; startTimestamp: number | null };
+ availableWithin1Week: { count: number; startTimestamp: number | null };
+};
-function formatDeliveryType(deliveryType: string): string {
- return DeliveryTypeMetadata[deliveryType]?.displayName || deliveryType;
-}
+function getZoneCapacityMetrics(zone: ZoneInfo): CapacityMetrics {
+ if (zone.available_capacity.length === 0) {
+ return {
+ availableNow: 0,
+ availableWithin1Day: { count: 0, startTimestamp: null },
+ availableWithin1Week: { count: 0, startTimestamp: null },
+ };
+ }
+
+ const now = dayjs();
+ const nowTimestamp = now.unix();
+ const oneDayTimestamp = now.add(1, "day").unix();
+ const oneWeekTimestamp = now.add(1, "week").unix();
+
+ // Sort capacity windows by start_timestamp
+ const sortedCapacity = [...zone.available_capacity].sort(
+ (a, b) => a.start_timestamp - b.start_timestamp,
+ );
+
+ let availableNow = 0;
+ let maxWithin1Day = 0;
+ let maxWithin1DayStart: number | null = null;
+ let maxWithin1Week = 0;
+ let maxWithin1WeekStart: number | null = null;
+
+ // Iterate through each capacity window
+ for (const win of sortedCapacity) {
+ // Skip windows that have already ended
+ if (win.end_timestamp <= nowTimestamp) {
+ continue;
+ }
+
+ // Early exit: windows are sorted, so if this one starts after 1 week, all subsequent ones will too
+ if (win.start_timestamp > oneWeekTimestamp) {
+ break;
+ }
+
+ const quantity = win.quantity;
-function getCurrentAvailableCapacity(zone: ZoneInfo): number {
- const oldestShape = zone.available_capacity
- ?.sort((a, b) => a.start_timestamp - b.start_timestamp)
- .at(0);
- if (
- oldestShape?.start_timestamp &&
- oldestShape.start_timestamp >= Math.floor(Date.now() / 1000)
- ) {
- return 0;
- }
- return oldestShape?.quantity ?? 0;
+ // Check if window contains "now"
+ if (
+ nowTimestamp >= win.start_timestamp &&
+ nowTimestamp < win.end_timestamp
+ ) {
+ availableNow = quantity;
+ }
+
+ // Check if window overlaps with "within 1 day" period
+ if (
+ win.start_timestamp < oneDayTimestamp &&
+ win.end_timestamp > nowTimestamp
+ ) {
+ if (quantity > maxWithin1Day) {
+ maxWithin1Day = quantity;
+ maxWithin1DayStart = Math.max(win.start_timestamp, nowTimestamp);
+ }
+ }
+
+ // Check if window overlaps with "within 1 week" period
+ if (
+ win.start_timestamp < oneWeekTimestamp &&
+ win.end_timestamp > nowTimestamp
+ ) {
+ if (quantity > maxWithin1Week) {
+ maxWithin1Week = quantity;
+ maxWithin1WeekStart = Math.max(win.start_timestamp, nowTimestamp);
+ }
+ }
+ }
+
+ return {
+ availableNow,
+ availableWithin1Day: {
+ count: maxWithin1Day,
+ startTimestamp: maxWithin1DayStart,
+ },
+ availableWithin1Week: {
+ count: maxWithin1Week,
+ startTimestamp: maxWithin1WeekStart,
+ },
+ };
}
// Region conversion to short slugs
const RegionMetadata: Record = {
- NorthAmerica: { slug: "North America" },
- AsiaPacific: { slug: "Asia" },
- EuropeMiddleEastAfrica: { slug: "EMEA" },
+ NorthAmerica: { slug: "north america" },
+ AsiaPacific: { slug: "asia" },
+ EuropeMiddleEastAfrica: { slug: "emea" },
} as const;
function formatRegion(region: string): string {
return RegionMetadata[region]?.slug || region;
}
+// Bright colors not used elsewhere in this component
+const REGION_COLORS = [
+ "green",
+ "yellow",
+ "blueBright",
+ "magentaBright",
+ "cyanBright",
+] as const;
+
+// Simple deterministic hash for consistent color assignment
+function hashString(str: string): number {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ hash = (hash << 5) - hash + char;
+ hash = hash & hash; // Convert to 32bit integer
+ }
+ return Math.abs(hash);
+}
+
+function getRegionColor(region: string): (typeof REGION_COLORS)[number] {
+ const hash = hashString(region);
+ return REGION_COLORS[hash % REGION_COLORS.length];
+}
+
export async function registerZones(program: Command) {
const isEnabled = await isFeatureEnabled("zones");
if (!isEnabled) return;
@@ -61,30 +155,28 @@ export async function registerZones(program: Command) {
"after",
`
Examples:
- \x1b[2m# List all zones\x1b[0m
+ \x1b[2m# List zones with availability\x1b[0m
$ sf zones ls
+ \x1b[2m# List all zones, including those with no availability\x1b[0m
+ $ sf zones ls --all
+
\x1b[2m# List zones with JSON output\x1b[0m
$ sf zones ls --json
-
-Note: This is an early access feature (v0) that may change at any time.
`,
);
zones
.command("list")
.alias("ls")
.description("List all zones")
+ .option("--all", "Show all zones, including those with no availability")
.option("--json", "Output in JSON format")
.action(async (options) => {
await listZonesAction(options);
});
}
-async function listZonesAction(options: { json?: boolean }) {
- console.error(
- "\x1b[33mNote: This is an early access feature (v0) and may change at any time.\x1b[0m\n",
- );
-
+async function listZonesAction(options: { all?: boolean; json?: boolean }) {
const loggedIn = await isLoggedIn();
if (!loggedIn) {
logLoginMessageAndQuit();
@@ -125,81 +217,411 @@ async function listZonesAction(options: { json?: boolean }) {
return;
}
- displayZonesTable(filteredZones);
+ await displayZonesTable(filteredZones, { showAll: options.all });
+}
+
+function hasAvailability(zone: ZoneInfo): boolean {
+ const metrics = getZoneCapacityMetrics(zone);
+ return (
+ metrics.availableNow > 0 ||
+ metrics.availableWithin1Day.count > 0 ||
+ metrics.availableWithin1Week.count > 0
+ );
+}
+
+function formatShortDistance(date: Date): string {
+ const now = new Date();
+ const minutes = differenceInMinutes(date, now);
+ const hours = differenceInHours(date, now);
+ const days = differenceInDays(date, now);
+
+ if (days >= 7) {
+ const weeks = Math.floor(days / 7);
+ return `${weeks}w`;
+ }
+ if (days >= 1) {
+ return `${days}d`;
+ }
+ if (hours >= 1) {
+ return `${hours}h`;
+ }
+ return `${minutes}m`;
}
-function displayZonesTable(zones: ZoneInfo[]) {
+function AvailabilityDisplay({
+ count,
+ startTimestamp,
+ isNow = false,
+ padWidth = 1,
+}: {
+ count: number;
+ startTimestamp: number | null;
+ isNow?: boolean;
+ padWidth?: number;
+}) {
+ const countStr = String(count).padStart(padWidth);
+
+ if (isNow && count === 0) {
+ return (
+
+ sold out
+
+ );
+ }
+
+ if (isNow) {
+ return (
+ <>
+ {countStr}
+ {" now "}
+ >
+ );
+ }
+
+ if (count === 0 || startTimestamp === null) {
+ return (
+ <>
+
+ {countStr}
+
+ {" now "}
+ >
+ );
+ }
+
+ const startDate = new Date(startTimestamp * 1000);
+ const now = new Date();
+ const minutesUntil = differenceInMinutes(startDate, now);
+
+ // If available now or within 1 minute, show "now"
+ if (minutesUntil <= 1) {
+ return (
+ <>
+ {countStr}
+ {" now "}
+ >
+ );
+ }
+
+ const distance = formatShortDistance(startDate).padEnd(3);
+ return (
+ <>
+ {countStr}
+ {` in ${distance}`}
+ >
+ );
+}
+
+// Column widths
+const COL = {
+ gpu: 6,
+ region: 15,
+ now: 11,
+ soonest: 11,
+ max: 11,
+};
+
+function ZonesTableDisplay({
+ zones,
+ hiddenCount,
+ firstAvailableZone,
+ firstAvailableZoneStartTime,
+}: {
+ zones: ZoneInfo[];
+ hiddenCount: number;
+ firstAvailableZone: string | null;
+ firstAvailableZoneStartTime: string | null;
+}) {
+ // Calculate dynamic zone column width based on longest zone name
+ const zoneWidth = Math.max(
+ 6, // minimum width for "zone" header + padding
+ ...zones.map((z) => z.name.length + 2), // +2 for padding
+ );
+
+ // Pre-calculate metrics for all zones to determine padding widths
+ const allMetrics = zones.map((z) => getZoneCapacityMetrics(z));
+
+ // Calculate max values for each column to determine padding
+ const maxNow = Math.max(1, ...allMetrics.map((m) => m.availableNow));
+ const maxToday = Math.max(
+ 1,
+ ...allMetrics.map((m) => m.availableWithin1Day.count),
+ );
+ const maxWeek = Math.max(
+ 1,
+ ...allMetrics.map((m) => m.availableWithin1Week.count),
+ );
+
+ // Calculate pad widths (number of digits needed)
+ const nowPadWidth = String(maxNow).length;
+ const todayPadWidth = String(maxToday).length;
+ const weekPadWidth = String(maxWeek).length;
+
+ return (
+
+
+ {/* Header row */}
+
+
+ zone
+ (slug)
+
+
+ gpu
+
+
+ region
+
+
+ nodes
+ now
+
+ ┊
+
+ soonest
+
+ ┊
+
+ max
+
+
+
+ {/* Body rows */}
+ {zones.map((zone, idx) => {
+ const metrics = allMetrics[idx];
+ const allSoldOut =
+ metrics.availableNow === 0 &&
+ metrics.availableWithin1Day.count === 0 &&
+ metrics.availableWithin1Week.count === 0;
+
+ // Total width of node columns: now(11) + separator(1) + soonest(11) + separator(1) + max(11) = 35
+ const nodeColsWidth = COL.now + 1 + COL.soonest + 1 + COL.max;
+ const soldOutText = "sold out";
+ const dashCount = Math.floor(
+ (nodeColsWidth - soldOutText.length - 2) / 2,
+ );
+ const soldOutDisplay = `${"-".repeat(dashCount)} ${soldOutText} ${"-".repeat(dashCount)}`;
+
+ return (
+
+
+ {zone.name}
+
+
+ {zone.hardware_type}
+
+
+
+ {formatRegion(zone.region)}
+
+
+ {allSoldOut ? (
+
+ {soldOutDisplay}
+
+ ) : (
+ <>
+
+
+
+ ┊
+
+
+
+ ┊
+
+
+
+ >
+ )}
+
+ );
+ })}
+
+ {/* Footer */}
+ {hiddenCount > 0 && (
+
+ {hiddenCount} sold-out zones hidden. Use
+ sf zones ls
+ --all
+ to show.
+
+ )}
+
+
+ {/* Examples */}
+ {firstAvailableZone && (
+
+ Use zone names when launching nodes.
+
+ Examples:
+
+ {" "}sf nodes create --zone{" "}
+ {firstAvailableZone}
+ {firstAvailableZoneStartTime && (
+ <>
+ {" "}
+ -s {firstAvailableZoneStartTime}
+ >
+ )}
+
+
+
+ )}
+
+ );
+}
+
+async function displayZonesTable(
+ zones: ZoneInfo[],
+ options: { showAll?: boolean } = {},
+) {
if (zones.length === 0) {
- render();
+ const { waitUntilExit } = render();
+ await waitUntilExit();
return;
}
- // Sort zones so available ones come first, then alphabetically by name
- const sortedZones = [...zones].sort((a, b) => {
- // Available zones first (true comes before false)
- const aAvailable = getCurrentAvailableCapacity(a) > 0;
- const bAvailable = getCurrentAvailableCapacity(b) > 0;
- if (aAvailable !== bAvailable) {
- return bAvailable ? 1 : -1;
+ // Separate zones with and without availability
+ const zonesWithAvailability = zones.filter(hasAvailability);
+ const zonesWithoutAvailability = zones.filter((z) => !hasAvailability(z));
+
+ if (zonesWithAvailability.length === 0) {
+ const { waitUntilExit } = render();
+ await waitUntilExit();
+ return;
+ }
+
+ // Determine which zones to display
+ const zonesToDisplay = options.showAll ? zones : zonesWithAvailability;
+
+ // Sort zones by availability: now > today > 1 week, then alphabetically by name
+ const sortedZones = [...zonesToDisplay].sort((a, b) => {
+ const aMetrics = getZoneCapacityMetrics(a);
+ const bMetrics = getZoneCapacityMetrics(b);
+
+ // Sort by availableNow (higher first)
+ if (aMetrics.availableNow !== bMetrics.availableNow) {
+ return bMetrics.availableNow - aMetrics.availableNow;
}
- // Then sort by name alphabetically
+
+ // Break ties with availableWithin1Day (higher first)
+ if (
+ aMetrics.availableWithin1Day.count !== bMetrics.availableWithin1Day.count
+ ) {
+ return (
+ bMetrics.availableWithin1Day.count - aMetrics.availableWithin1Day.count
+ );
+ }
+
+ // Break ties with availableWithin1Week (higher first)
+ if (
+ aMetrics.availableWithin1Week.count !==
+ bMetrics.availableWithin1Week.count
+ ) {
+ return (
+ bMetrics.availableWithin1Week.count -
+ aMetrics.availableWithin1Week.count
+ );
+ }
+
+ // Finally sort by name alphabetically
return a.name.localeCompare(b.name);
});
- const table = new Table({
- head: [
- chalk.cyan("Zone"),
- chalk.cyan("Delivery Type"),
- chalk.cyan("Available Nodes"),
- chalk.cyan("GPU Type"),
- chalk.cyan("Interconnect"),
- chalk.cyan("Region"),
- ],
- style: {
- head: [],
- border: ["gray"],
- },
- });
+ const hiddenCount = options.showAll ? 0 : zonesWithoutAvailability.length;
+ const firstAvailableZone =
+ zonesWithAvailability.length > 0 ? zonesWithAvailability[0].name : null;
- sortedZones.forEach((zone) => {
- const available = getCurrentAvailableCapacity(zone);
- const availableNodesText =
- available > 0
- ? chalk.green(available.toString())
- : chalk.red(available.toString());
-
- table.push([
- zone.name,
- formatDeliveryType(zone.delivery_type),
- availableNodesText,
- zone.hardware_type,
- zone.interconnect_type || "None",
- formatRegion(zone.region),
- ]);
- });
+ // Calculate earliest start time for first available zone
+ let firstAvailableZoneStartTime: string | null = null;
+ if (zonesWithAvailability.length > 0) {
+ const metrics = getZoneCapacityMetrics(zonesWithAvailability[0]);
+ if (metrics.availableNow > 0) {
+ firstAvailableZoneStartTime = "now";
+ } else if (metrics.availableWithin1Day.startTimestamp) {
+ firstAvailableZoneStartTime = dayjs
+ .unix(metrics.availableWithin1Day.startTimestamp)
+ .toISOString();
+ } else if (metrics.availableWithin1Week.startTimestamp) {
+ firstAvailableZoneStartTime = dayjs
+ .unix(metrics.availableWithin1Week.startTimestamp)
+ .toISOString();
+ }
+ }
- const availableZones = sortedZones.filter(
- (zone) => getCurrentAvailableCapacity(zone) > 0,
- );
- const availableZoneName = availableZones?.[0]?.name ?? "alamo";
- console.log(table.toString());
- console.log(
- `\n${chalk.gray("Use zone names when placing orders or configuring nodes.")}\n`,
- );
- console.log(chalk.gray("Examples:"));
- console.log(` sf buy --zone ${chalk.green(availableZoneName)}`);
- console.log(
- ` sf scale create -n 16 --zone ${chalk.green(availableZoneName)}`,
+ const { waitUntilExit } = render(
+ ,
);
+ await waitUntilExit();
}
function EmptyZonesDisplay() {
return (
- No zones found.
+ No zones with availability found.
# Check back later for available zones
sf zones ls
+
+ # To show all zones, including those with no availability
+
+ sf zones ls --all
);