@synteq/cli 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/dist/api/client.d.ts +28 -37
  2. package/dist/api/client.js +47 -49
  3. package/dist/api/client.js.map +1 -1
  4. package/dist/api/types.d.ts +34 -23
  5. package/dist/commands/api.js +4 -4
  6. package/dist/commands/api.js.map +1 -1
  7. package/dist/commands/catalog.js +1 -1
  8. package/dist/commands/catalog.js.map +1 -1
  9. package/dist/commands/config/set-api-url.js +37 -2
  10. package/dist/commands/config/set-api-url.js.map +1 -1
  11. package/dist/commands/gpu/activity.d.ts +1 -0
  12. package/dist/commands/gpu/activity.js +6 -0
  13. package/dist/commands/gpu/activity.js.map +1 -1
  14. package/dist/commands/gpu/bandwidth.d.ts +1 -0
  15. package/dist/commands/gpu/bandwidth.js +4 -0
  16. package/dist/commands/gpu/bandwidth.js.map +1 -1
  17. package/dist/commands/gpu/boot-order.d.ts +1 -0
  18. package/dist/commands/gpu/boot-order.js +5 -0
  19. package/dist/commands/gpu/boot-order.js.map +1 -1
  20. package/dist/commands/gpu/console.d.ts +1 -0
  21. package/dist/commands/gpu/console.js +5 -0
  22. package/dist/commands/gpu/console.js.map +1 -1
  23. package/dist/commands/gpu/details.d.ts +2 -0
  24. package/dist/commands/gpu/details.js +9 -0
  25. package/dist/commands/gpu/details.js.map +1 -1
  26. package/dist/commands/gpu/ip/add.d.ts +1 -0
  27. package/dist/commands/gpu/ip/add.js +6 -0
  28. package/dist/commands/gpu/ip/add.js.map +1 -1
  29. package/dist/commands/gpu/ip/list.d.ts +1 -0
  30. package/dist/commands/gpu/ip/list.js +4 -0
  31. package/dist/commands/gpu/ip/list.js.map +1 -1
  32. package/dist/commands/gpu/list.d.ts +1 -1
  33. package/dist/commands/gpu/list.js +2 -2
  34. package/dist/commands/gpu/list.js.map +1 -1
  35. package/dist/commands/gpu/order.js +2 -0
  36. package/dist/commands/gpu/order.js.map +1 -1
  37. package/dist/commands/gpu/power.d.ts +1 -0
  38. package/dist/commands/gpu/power.js +6 -0
  39. package/dist/commands/gpu/power.js.map +1 -1
  40. package/dist/commands/gpu/reinstall.d.ts +3 -0
  41. package/dist/commands/gpu/reinstall.js +7 -0
  42. package/dist/commands/gpu/reinstall.js.map +1 -1
  43. package/dist/commands/gpu/resize.d.ts +1 -0
  44. package/dist/commands/gpu/resize.js +6 -0
  45. package/dist/commands/gpu/resize.js.map +1 -1
  46. package/dist/commands/gpu/stats.d.ts +1 -0
  47. package/dist/commands/gpu/stats.js +5 -0
  48. package/dist/commands/gpu/stats.js.map +1 -1
  49. package/dist/commands/instant-metal/activity.d.ts +1 -0
  50. package/dist/commands/instant-metal/activity.js +6 -0
  51. package/dist/commands/instant-metal/activity.js.map +1 -1
  52. package/dist/commands/instant-metal/bandwidth.d.ts +1 -0
  53. package/dist/commands/instant-metal/bandwidth.js +4 -0
  54. package/dist/commands/instant-metal/bandwidth.js.map +1 -1
  55. package/dist/commands/instant-metal/boot-order.d.ts +1 -0
  56. package/dist/commands/instant-metal/boot-order.js +5 -0
  57. package/dist/commands/instant-metal/boot-order.js.map +1 -1
  58. package/dist/commands/instant-metal/console.d.ts +1 -0
  59. package/dist/commands/instant-metal/console.js +5 -0
  60. package/dist/commands/instant-metal/console.js.map +1 -1
  61. package/dist/commands/instant-metal/details.d.ts +2 -0
  62. package/dist/commands/instant-metal/details.js +9 -0
  63. package/dist/commands/instant-metal/details.js.map +1 -1
  64. package/dist/commands/instant-metal/ip/add.d.ts +1 -0
  65. package/dist/commands/instant-metal/ip/add.js +6 -0
  66. package/dist/commands/instant-metal/ip/add.js.map +1 -1
  67. package/dist/commands/instant-metal/ip/list.d.ts +1 -0
  68. package/dist/commands/instant-metal/ip/list.js +4 -0
  69. package/dist/commands/instant-metal/ip/list.js.map +1 -1
  70. package/dist/commands/instant-metal/list.d.ts +1 -1
  71. package/dist/commands/instant-metal/list.js +2 -2
  72. package/dist/commands/instant-metal/list.js.map +1 -1
  73. package/dist/commands/instant-metal/order.js +2 -0
  74. package/dist/commands/instant-metal/order.js.map +1 -1
  75. package/dist/commands/instant-metal/power.d.ts +1 -0
  76. package/dist/commands/instant-metal/power.js +6 -0
  77. package/dist/commands/instant-metal/power.js.map +1 -1
  78. package/dist/commands/instant-metal/reinstall.d.ts +3 -0
  79. package/dist/commands/instant-metal/reinstall.js +7 -0
  80. package/dist/commands/instant-metal/reinstall.js.map +1 -1
  81. package/dist/commands/instant-metal/resize.d.ts +1 -0
  82. package/dist/commands/instant-metal/resize.js +6 -0
  83. package/dist/commands/instant-metal/resize.js.map +1 -1
  84. package/dist/commands/instant-metal/stats.d.ts +1 -0
  85. package/dist/commands/instant-metal/stats.js +5 -0
  86. package/dist/commands/instant-metal/stats.js.map +1 -1
  87. package/dist/commands/order/gpu.js +2 -0
  88. package/dist/commands/order/gpu.js.map +1 -1
  89. package/dist/commands/order/instant-metal.js +2 -0
  90. package/dist/commands/order/instant-metal.js.map +1 -1
  91. package/dist/commands/order/storage.js +1 -1
  92. package/dist/commands/order/storage.js.map +1 -1
  93. package/dist/commands/order/vps.js +2 -0
  94. package/dist/commands/order/vps.js.map +1 -1
  95. package/dist/commands/os-images.js +1 -1
  96. package/dist/commands/os-images.js.map +1 -1
  97. package/dist/commands/resources.js +22 -21
  98. package/dist/commands/resources.js.map +1 -1
  99. package/dist/commands/storage/attach.d.ts +1 -1
  100. package/dist/commands/storage/attach.js +11 -11
  101. package/dist/commands/storage/attach.js.map +1 -1
  102. package/dist/commands/storage/detach.js +2 -2
  103. package/dist/commands/storage/detach.js.map +1 -1
  104. package/dist/commands/storage/list.js +1 -1
  105. package/dist/commands/storage/list.js.map +1 -1
  106. package/dist/commands/storage/order.d.ts +1 -1
  107. package/dist/commands/storage/order.js +10 -10
  108. package/dist/commands/storage/order.js.map +1 -1
  109. package/dist/commands/vds/activity.d.ts +1 -0
  110. package/dist/commands/vds/activity.js +6 -0
  111. package/dist/commands/vds/activity.js.map +1 -1
  112. package/dist/commands/vds/bandwidth.d.ts +1 -0
  113. package/dist/commands/vds/bandwidth.js +4 -0
  114. package/dist/commands/vds/bandwidth.js.map +1 -1
  115. package/dist/commands/vds/boot-order.d.ts +1 -0
  116. package/dist/commands/vds/boot-order.js +5 -0
  117. package/dist/commands/vds/boot-order.js.map +1 -1
  118. package/dist/commands/vds/console.d.ts +1 -0
  119. package/dist/commands/vds/console.js +5 -0
  120. package/dist/commands/vds/console.js.map +1 -1
  121. package/dist/commands/vds/details.d.ts +2 -0
  122. package/dist/commands/vds/details.js +9 -0
  123. package/dist/commands/vds/details.js.map +1 -1
  124. package/dist/commands/vds/ip/add.d.ts +1 -0
  125. package/dist/commands/vds/ip/add.js +6 -0
  126. package/dist/commands/vds/ip/add.js.map +1 -1
  127. package/dist/commands/vds/ip/list.d.ts +1 -0
  128. package/dist/commands/vds/ip/list.js +4 -0
  129. package/dist/commands/vds/ip/list.js.map +1 -1
  130. package/dist/commands/vds/list.d.ts +1 -1
  131. package/dist/commands/vds/list.js +2 -2
  132. package/dist/commands/vds/list.js.map +1 -1
  133. package/dist/commands/vds/order.d.ts +19 -2
  134. package/dist/commands/vds/order.js +210 -7
  135. package/dist/commands/vds/order.js.map +1 -1
  136. package/dist/commands/vds/power.d.ts +1 -0
  137. package/dist/commands/vds/power.js +6 -0
  138. package/dist/commands/vds/power.js.map +1 -1
  139. package/dist/commands/vds/reinstall.d.ts +3 -0
  140. package/dist/commands/vds/reinstall.js +7 -0
  141. package/dist/commands/vds/reinstall.js.map +1 -1
  142. package/dist/commands/vds/resize.d.ts +1 -0
  143. package/dist/commands/vds/resize.js +6 -0
  144. package/dist/commands/vds/resize.js.map +1 -1
  145. package/dist/commands/vds/stats.d.ts +1 -0
  146. package/dist/commands/vds/stats.js +5 -0
  147. package/dist/commands/vds/stats.js.map +1 -1
  148. package/dist/commands/vps/activity.js +4 -4
  149. package/dist/commands/vps/activity.js.map +1 -1
  150. package/dist/commands/vps/bandwidth.js +5 -5
  151. package/dist/commands/vps/bandwidth.js.map +1 -1
  152. package/dist/commands/vps/boot-order.js +5 -5
  153. package/dist/commands/vps/boot-order.js.map +1 -1
  154. package/dist/commands/vps/console.js +9 -9
  155. package/dist/commands/vps/console.js.map +1 -1
  156. package/dist/commands/vps/details.d.ts +4 -0
  157. package/dist/commands/vps/details.js +20 -9
  158. package/dist/commands/vps/details.js.map +1 -1
  159. package/dist/commands/vps/ip/add.js +10 -9
  160. package/dist/commands/vps/ip/add.js.map +1 -1
  161. package/dist/commands/vps/ip/list.js +6 -6
  162. package/dist/commands/vps/ip/list.js.map +1 -1
  163. package/dist/commands/vps/list.d.ts +3 -1
  164. package/dist/commands/vps/list.js +11 -7
  165. package/dist/commands/vps/list.js.map +1 -1
  166. package/dist/commands/vps/order.d.ts +3 -2
  167. package/dist/commands/vps/order.js +25 -16
  168. package/dist/commands/vps/order.js.map +1 -1
  169. package/dist/commands/vps/power.js +14 -13
  170. package/dist/commands/vps/power.js.map +1 -1
  171. package/dist/commands/vps/reinstall.d.ts +2 -0
  172. package/dist/commands/vps/reinstall.js +41 -14
  173. package/dist/commands/vps/reinstall.js.map +1 -1
  174. package/dist/commands/vps/resize.js +11 -10
  175. package/dist/commands/vps/resize.js.map +1 -1
  176. package/dist/commands/vps/stats.js +7 -7
  177. package/dist/commands/vps/stats.js.map +1 -1
  178. package/dist/help.js +0 -1
  179. package/dist/help.js.map +1 -1
  180. package/dist/tui/screens/Dashboard.js +1 -1
  181. package/dist/tui/screens/Dashboard.js.map +1 -1
  182. package/dist/tui/screens/Order.js +352 -45
  183. package/dist/tui/screens/Order.js.map +1 -1
  184. package/dist/tui/screens/Resources.js +31 -60
  185. package/dist/tui/screens/Resources.js.map +1 -1
  186. package/dist/ui/image-resolve.js +1 -1
  187. package/dist/ui/image-resolve.js.map +1 -1
  188. package/dist/ui/payment.d.ts +1 -1
  189. package/dist/ui/resource-detail.d.ts +3 -0
  190. package/dist/ui/{vps-detail.js → resource-detail.js} +11 -7
  191. package/dist/ui/resource-detail.js.map +1 -0
  192. package/dist/ui/resource-resolve.d.ts +4 -0
  193. package/dist/ui/resource-resolve.js +24 -0
  194. package/dist/ui/resource-resolve.js.map +1 -0
  195. package/dist/ui/wait.d.ts +1 -1
  196. package/dist/ui/wait.js +4 -4
  197. package/dist/ui/wait.js.map +1 -1
  198. package/package.json +1 -4
  199. package/dist/commands/bare-metal/index.d.ts +0 -4
  200. package/dist/commands/bare-metal/index.js +0 -5
  201. package/dist/commands/bare-metal/index.js.map +0 -1
  202. package/dist/commands/bare-metal/list.d.ts +0 -9
  203. package/dist/commands/bare-metal/list.js +0 -38
  204. package/dist/commands/bare-metal/list.js.map +0 -1
  205. package/dist/commands/bare-metal/order.d.ts +0 -6
  206. package/dist/commands/bare-metal/order.js +0 -27
  207. package/dist/commands/bare-metal/order.js.map +0 -1
  208. package/dist/ui/vps-detail.d.ts +0 -3
  209. package/dist/ui/vps-detail.js.map +0 -1
  210. package/dist/ui/vps-resolve.d.ts +0 -4
  211. package/dist/ui/vps-resolve.js +0 -24
  212. package/dist/ui/vps-resolve.js.map +0 -1
@@ -22,6 +22,8 @@ const emptyDraft = {
22
22
  projectId: null,
23
23
  planId: null,
24
24
  customSpec: {},
25
+ tierCategorySlug: null,
26
+ tierIndex: null,
25
27
  storageSizeGb: null,
26
28
  locationId: null,
27
29
  imageId: null,
@@ -37,8 +39,7 @@ const emptyDraft = {
37
39
  const PROGRESS_STEPS = [
38
40
  "kind",
39
41
  "project",
40
- "plan",
41
- "location",
42
+ "plan-location",
42
43
  "image",
43
44
  "hostname",
44
45
  "auth",
@@ -50,8 +51,11 @@ function stepsForKind(kind) {
50
51
  if (kind === "vps") {
51
52
  base.push("vps-spec", "image", "hostname", "auth", "payment", "summary");
52
53
  }
53
- else if (kind === "gpu" || kind === "dedicated") {
54
- base.push("plan", "location", "image", "hostname", "auth", "payment", "summary");
54
+ else if (kind === "gpu" || kind === "dedicated" || kind === "vds") {
55
+ // Split plan/tier picker + location picker into one step so we can probe
56
+ // per-location availability live (server-side /public/order/validate) and
57
+ // offer notify-me when the chosen location is out of stock.
58
+ base.push("plan-location", "image", "hostname", "auth", "payment", "summary");
55
59
  }
56
60
  else if (kind === "storage") {
57
61
  base.push("size", "location", "hostname", "payment", "summary");
@@ -85,7 +89,7 @@ export function Order() {
85
89
  (async () => {
86
90
  try {
87
91
  const [orderCatalog, storageCatalog, projects, methods, balance] = await Promise.all([
88
- api.vpsOrders.catalog().catch(() => null),
92
+ api.orders.catalog().catch(() => null),
89
93
  api.storage.catalog().catch(() => null),
90
94
  api.projects.list().catch(() => []),
91
95
  api.payments.methods().catch(() => []),
@@ -210,10 +214,18 @@ export function Order() {
210
214
  });
211
215
  }
212
216
  else {
217
+ // Sizing mode per kind:
218
+ // vps → custom_spec (custom_cpu_cores/ram/disk sliders)
219
+ // vds → tier_category_slug + tier_index (server resolves)
220
+ // gpu / dedicated → plan_id (preset catalog plan)
221
+ const isVps = draft.kind === "vps";
222
+ const isVds = draft.kind === "vds";
213
223
  const body = {
214
224
  project_id: draft.projectId,
215
- plan_id: draft.kind === "vps" ? undefined : draft.planId ?? undefined,
216
- custom_spec: draft.kind === "vps" ? buildCustomSpec(draft.customSpec) : undefined,
225
+ plan_id: isVps || isVds ? undefined : draft.planId ?? undefined,
226
+ custom_spec: isVps ? buildCustomSpec(draft.customSpec) : undefined,
227
+ tier_category_slug: isVds ? draft.tierCategorySlug ?? undefined : undefined,
228
+ tier_index: isVds ? draft.tierIndex ?? undefined : undefined,
217
229
  location_id: draft.locationId,
218
230
  image_id: draft.imageId,
219
231
  hostname: draft.hostname,
@@ -227,7 +239,7 @@ export function Order() {
227
239
  ? draft.sshKeyId
228
240
  : undefined,
229
241
  };
230
- const out = await api.vpsOrders.create(body);
242
+ const out = await api.orders.create(body);
231
243
  setResult({
232
244
  ok: true,
233
245
  orderId: out.order_id,
@@ -282,8 +294,8 @@ function StepRouter(props) {
282
294
  return React.createElement(KindStep, { draft: draft, setDraft: setDraft, goNext: goNext });
283
295
  case "project":
284
296
  return React.createElement(ProjectStep, { projects: cat.projects, draft: draft, setDraft: setDraft, goNext: goNext });
285
- case "plan":
286
- return (React.createElement(PlanStep, { catalog: cat.orderCatalog, draft: draft, setDraft: setDraft, goNext: goNext }));
297
+ case "plan-location":
298
+ return (React.createElement(PlanLocationStep, { catalog: cat.orderCatalog, userEmail: props.userEmail, draft: draft, setDraft: setDraft, goNext: goNext }));
287
299
  case "vps-spec":
288
300
  return (React.createElement(VpsSpecStep, { customConfig: cat.orderCatalog?.custom_config ?? null, locations: cat.orderCatalog?.locations ?? [], userEmail: props.userEmail, draft: draft, setDraft: setDraft, goNext: goNext }));
289
301
  case "size":
@@ -314,9 +326,10 @@ function StepRouter(props) {
314
326
  function KindStep({ draft, setDraft, goNext, }) {
315
327
  const items = [
316
328
  { value: "vps", label: "VPS", hint: "custom CPU/RAM/disk; shared or dedicated cores" },
329
+ { value: "vds", label: "VDS", hint: "tier-based performance servers (order via web for now)" },
317
330
  { value: "gpu", label: "GPU", hint: "preset GPU-bearing plans" },
318
- { value: "dedicated", label: "Dedicated", hint: "whole physical hosts" },
319
- { value: "storage", label: "Storage", hint: "block volume (attach to a VPS later)" },
331
+ { value: "dedicated", label: "Instant Metal", hint: "whole physical hosts" },
332
+ { value: "storage", label: "Storage", hint: "block volume (attach to a resource later)" },
320
333
  ];
321
334
  return (React.createElement(Panel, { title: "What are you ordering?", paddingX: 3, paddingY: 1, width: 80 },
322
335
  React.createElement(Menu, { items: items, initialIndex: Math.max(0, items.findIndex((i) => i.value === draft.kind)), onSelect: (v) => {
@@ -326,6 +339,8 @@ function KindStep({ draft, setDraft, goNext, }) {
326
339
  kind: v,
327
340
  planId: null,
328
341
  customSpec: {},
342
+ tierCategorySlug: null,
343
+ tierIndex: null,
329
344
  storageSizeGb: null,
330
345
  locationId: null,
331
346
  imageId: null,
@@ -355,33 +370,6 @@ function ProjectStep({ projects, draft, setDraft, goNext, }) {
355
370
  goNext();
356
371
  } })));
357
372
  }
358
- function PlanStep({ catalog, draft, setDraft, goNext, }) {
359
- if (!catalog) {
360
- return (React.createElement(Panel, { title: "Catalog unavailable", paddingX: 3, paddingY: 1, width: 80 },
361
- React.createElement(Text, { color: colors.error }, "Couldn't load the order catalog. Try again later.")));
362
- }
363
- const category = draft.kind === "gpu" ? "gpu_cloud" : "dedicated";
364
- const filtered = catalog.plans.filter((p) => p.category === category || p.category.replace("_", "-") === draft.kind);
365
- const plans = filtered.length ? filtered : catalog.plans;
366
- if (plans.length === 0) {
367
- return (React.createElement(Panel, { title: "No plans in catalog", paddingX: 3, paddingY: 1, width: 80 },
368
- React.createElement(Text, { color: colors.warning },
369
- "No ",
370
- draft.kind,
371
- " plans are currently offered. Try a different resource type.")));
372
- }
373
- const items = plans.map((p) => ({
374
- value: p.id,
375
- label: planRowLabel(p),
376
- hint: planRowHint(p),
377
- disabled: p.in_stock ? undefined : "out of stock",
378
- }));
379
- return (React.createElement(Panel, { title: `Pick a ${draft.kind === "gpu" ? "GPU" : "dedicated"} plan`, paddingX: 3, paddingY: 1, width: 80 },
380
- React.createElement(Menu, { items: items, initialIndex: Math.max(0, items.findIndex((i) => i.value === draft.planId)), onSelect: (v) => {
381
- setDraft((d) => ({ ...d, planId: v, locationId: null }));
382
- goNext();
383
- } })));
384
- }
385
373
  function planRowLabel(p) {
386
374
  return `${p.name} $${p.monthly_price}/mo`;
387
375
  }
@@ -392,10 +380,308 @@ function planRowHint(p) {
392
380
  const extra = p.gpu_model ? ` · ${p.gpu_model}` : "";
393
381
  return `${cores} ${ram} ${disk}${extra}`;
394
382
  }
383
+ // Combined plan/tier + location picker for VDS, GPU Cloud, and Instant Metal.
384
+ // Mirrors what the VPS custom-spec step does: probe /public/order/validate
385
+ // per-location so the user sees which locations can actually fit the pick,
386
+ // and offer marketing.notifyStock when the chosen location is out of stock.
387
+ //
388
+ // The VPS custom-spec flow lives in its own step (VpsSpecStep) because the
389
+ // left pane is a slider grid rather than a plan/tier list — different shape,
390
+ // same idea.
391
+ function PlanLocationStep({ catalog, userEmail, draft, setDraft, goNext, }) {
392
+ const api = useApi();
393
+ if (!catalog) {
394
+ return (React.createElement(Panel, { title: "Catalog unavailable", paddingX: 3, paddingY: 1, width: 80 },
395
+ React.createElement(Text, { color: colors.error }, "Couldn't load the order catalog. Try again later.")));
396
+ }
397
+ const isVds = draft.kind === "vds";
398
+ const isDedicated = draft.kind === "dedicated";
399
+ let picks = [];
400
+ let leftTitle = "Pick a plan";
401
+ const validateKind = isVds ? "vds" : isDedicated ? "instant_metal" : "gpu_cloud";
402
+ if (isVds) {
403
+ // Find an enabled VDS category with a non-empty ladder. Usually
404
+ // exactly one (vds_performance); we take the first enabled.
405
+ const vdsCats = (catalog.categories ?? []).filter((c) => c.slug.startsWith("vds") &&
406
+ c.slider_config?.enabled &&
407
+ (c.slider_config.tiers?.length ?? 0) > 0);
408
+ if (vdsCats.length === 0) {
409
+ return (React.createElement(Panel, { title: "No VDS tiers configured", paddingX: 3, paddingY: 1, width: 80 },
410
+ React.createElement(Text, { color: colors.warning }, "No VDS tier ladders are enabled on this deployment. Pick a different resource kind or order VDS from the web console.")));
411
+ }
412
+ const cat = vdsCats[0];
413
+ leftTitle = `Pick a VDS tier (${cat.name ?? cat.slug})`;
414
+ const tiers = cat.slider_config?.tiers ?? [];
415
+ picks = tiers.map((t, i) => ({
416
+ key: `${cat.slug}#${i}`,
417
+ label: `Tier ${i} $${t.monthly_price}/mo`,
418
+ hint: `${t.cpu}c · ${t.ram_gb}GB RAM · ${t.disk_gb}GB disk · ${t.bandwidth_tb}TB BW`,
419
+ cpu_cores: t.cpu,
420
+ ram_gb: t.ram_gb,
421
+ disk_gb: t.disk_gb,
422
+ monthly_price: t.monthly_price,
423
+ in_stock: true, // VDS ladders don't carry a coarse in-stock flag; rely on per-loc probe below.
424
+ tierCategorySlug: cat.slug,
425
+ tierIndex: i,
426
+ dedicated: false,
427
+ }));
428
+ }
429
+ else {
430
+ const category = isDedicated ? "dedicated" : "gpu_cloud";
431
+ const filtered = catalog.plans.filter((p) => p.category === category || p.category.replace("_", "-") === draft.kind);
432
+ const plans = filtered.length ? filtered : catalog.plans;
433
+ if (plans.length === 0) {
434
+ return (React.createElement(Panel, { title: "No plans in catalog", paddingX: 3, paddingY: 1, width: 80 },
435
+ React.createElement(Text, { color: colors.warning },
436
+ "No ",
437
+ draft.kind,
438
+ " plans are currently offered. Try a different resource type.")));
439
+ }
440
+ leftTitle = `Pick a ${isDedicated ? "Instant Metal" : "GPU"} plan`;
441
+ picks = plans.map((p) => ({
442
+ key: p.id,
443
+ label: planRowLabel(p),
444
+ hint: planRowHint(p),
445
+ cpu_cores: p.cpu_cores,
446
+ ram_gb: p.ram_gb,
447
+ disk_gb: p.disk_gb,
448
+ monthly_price: p.monthly_price,
449
+ in_stock: p.in_stock,
450
+ planId: p.id,
451
+ dedicated: isDedicated,
452
+ }));
453
+ }
454
+ const locations = catalog.locations;
455
+ const hasLocations = locations.length > 0;
456
+ // Initial cursor: last-picked selection if present, else the first
457
+ // in-stock (plan-level) row; falls back to 0.
458
+ const initialPickIdx = (() => {
459
+ if (isVds && draft.tierIndex != null && draft.tierCategorySlug) {
460
+ const idx = picks.findIndex((p) => p.tierCategorySlug === draft.tierCategorySlug &&
461
+ p.tierIndex === draft.tierIndex);
462
+ if (idx >= 0)
463
+ return idx;
464
+ }
465
+ if (!isVds && draft.planId) {
466
+ const idx = picks.findIndex((p) => p.planId === draft.planId);
467
+ if (idx >= 0)
468
+ return idx;
469
+ }
470
+ const firstInStock = picks.findIndex((p) => p.in_stock);
471
+ return firstInStock >= 0 ? firstInStock : 0;
472
+ })();
473
+ const [pickCursor, setPickCursor] = useState(initialPickIdx);
474
+ const [locCursor, setLocCursor] = useState(() => {
475
+ const idx = locations.findIndex((l) => l.id === draft.locationId);
476
+ return idx >= 0 ? idx : 0;
477
+ });
478
+ const [pane, setPane] = useState("pick");
479
+ const [notifyState, setNotifyState] = useState("idle");
480
+ // Availability probe keyed on (pickCursor, location). Cleared whenever
481
+ // the pick changes so a stale in-stock badge can't survive a tier switch.
482
+ const [stockByLoc, setStockByLoc] = useState({});
483
+ const currentPick = picks[pickCursor];
484
+ useEffect(() => {
485
+ // Reset probe results when the pick changes — a Tier 2 might fit where
486
+ // Tier 3 does not (and vice versa), so old flags would lie.
487
+ setStockByLoc({});
488
+ setNotifyState("idle");
489
+ }, [pickCursor]);
490
+ useEffect(() => {
491
+ if (!hasLocations || !currentPick)
492
+ return;
493
+ if (!currentPick.in_stock)
494
+ return; // Plan globally OOS — nothing to probe.
495
+ let cancelled = false;
496
+ // Small debounce so switching picks quickly doesn't fire N * K validate
497
+ // calls.
498
+ const t = setTimeout(() => {
499
+ for (const loc of locations) {
500
+ api.orders
501
+ .validate({
502
+ cpu_cores: currentPick.cpu_cores,
503
+ ram_gb: currentPick.ram_gb,
504
+ disk_gb: currentPick.disk_gb,
505
+ location_id: loc.id,
506
+ dedicated: currentPick.dedicated,
507
+ kind: validateKind,
508
+ })
509
+ .then((res) => {
510
+ if (cancelled)
511
+ return;
512
+ setStockByLoc((m) => ({ ...m, [loc.id]: res.available }));
513
+ })
514
+ .catch(() => {
515
+ // Swallow — the location just stays unmarked; the server will
516
+ // fail the order if the spec truly can't fit, and we've already
517
+ // let the user see the pick.
518
+ });
519
+ }
520
+ }, 250);
521
+ return () => {
522
+ cancelled = true;
523
+ clearTimeout(t);
524
+ };
525
+ // Re-run whenever the pick's specs, the location list, or the kind changes.
526
+ // eslint-disable-next-line react-hooks/exhaustive-deps
527
+ }, [
528
+ hasLocations,
529
+ currentPick?.cpu_cores,
530
+ currentPick?.ram_gb,
531
+ currentPick?.disk_gb,
532
+ currentPick?.dedicated,
533
+ validateKind,
534
+ locations,
535
+ ]);
536
+ const selLoc = hasLocations ? locations[locCursor] ?? null : null;
537
+ const planLevelOOS = Boolean(currentPick && !currentPick.in_stock);
538
+ const locStockKnown = selLoc ? stockByLoc[selLoc.id] !== undefined : false;
539
+ const selInStock = !hasLocations ||
540
+ (!planLevelOOS && (!locStockKnown || stockByLoc[selLoc?.id ?? ""] !== false));
541
+ const canContinue = Boolean(currentPick) && Boolean(selLoc) && selInStock;
542
+ const commit = () => {
543
+ if (!currentPick || !selLoc)
544
+ return;
545
+ setDraft((d) => ({
546
+ ...d,
547
+ planId: currentPick.planId ?? null,
548
+ tierCategorySlug: currentPick.tierCategorySlug ?? null,
549
+ tierIndex: currentPick.tierIndex ?? null,
550
+ locationId: selLoc.id,
551
+ }));
552
+ goNext();
553
+ };
554
+ const requestNotify = async () => {
555
+ if (!selLoc || !currentPick)
556
+ return;
557
+ if (notifyState === "sending" || notifyState === "sent")
558
+ return;
559
+ if (!userEmail) {
560
+ setNotifyState("error");
561
+ return;
562
+ }
563
+ setNotifyState("sending");
564
+ try {
565
+ await api.marketing.notifyStock({
566
+ email: userEmail,
567
+ location_id: selLoc.id,
568
+ plan_id: currentPick.planId ?? null,
569
+ note: `CLI order wizard — ${draft.kind}`,
570
+ config_json: {
571
+ cpu_cores: currentPick.cpu_cores,
572
+ ram_gb: currentPick.ram_gb,
573
+ disk_gb: currentPick.disk_gb,
574
+ dedicated: currentPick.dedicated,
575
+ kind: validateKind,
576
+ tier_category_slug: currentPick.tierCategorySlug ?? null,
577
+ tier_index: currentPick.tierIndex ?? null,
578
+ },
579
+ });
580
+ setNotifyState("sent");
581
+ }
582
+ catch {
583
+ setNotifyState("error");
584
+ }
585
+ };
586
+ useInput((input, key) => {
587
+ if (key.tab) {
588
+ if (hasLocations)
589
+ setPane((p) => (p === "pick" ? "loc" : "pick"));
590
+ return;
591
+ }
592
+ if (pane === "pick") {
593
+ if (key.upArrow) {
594
+ setPickCursor((c) => (c === 0 ? picks.length - 1 : c - 1));
595
+ }
596
+ else if (key.downArrow) {
597
+ setPickCursor((c) => (c === picks.length - 1 ? 0 : c + 1));
598
+ }
599
+ else if (key.return || key.rightArrow) {
600
+ if (hasLocations)
601
+ setPane("loc");
602
+ else
603
+ commit();
604
+ }
605
+ return;
606
+ }
607
+ // pane === "loc"
608
+ if (key.upArrow) {
609
+ setLocCursor((c) => (c === 0 ? locations.length - 1 : c - 1));
610
+ }
611
+ else if (key.downArrow) {
612
+ setLocCursor((c) => (c === locations.length - 1 ? 0 : c + 1));
613
+ }
614
+ else if (key.leftArrow) {
615
+ setPane("pick");
616
+ }
617
+ else if (key.return) {
618
+ if (canContinue)
619
+ commit();
620
+ else
621
+ void requestNotify();
622
+ }
623
+ });
624
+ const buttonLabel = canContinue
625
+ ? "Continue"
626
+ : notifyState === "sent"
627
+ ? "Requested ✓"
628
+ : notifyState === "sending"
629
+ ? "Sending…"
630
+ : "Notify me";
631
+ const pickPanel = (React.createElement(Panel, { title: leftTitle, paddingX: 2, paddingY: 1, width: hasLocations ? 50 : 80, borderColor: pane === "pick" ? colors.accent : undefined },
632
+ React.createElement(Box, { flexDirection: "column" },
633
+ picks.map((p, i) => {
634
+ const isCursor = i === pickCursor;
635
+ const disabled = !p.in_stock;
636
+ return (React.createElement(Box, { key: p.key },
637
+ React.createElement(Text, { color: isCursor
638
+ ? pane === "pick"
639
+ ? colors.accent
640
+ : colors.text
641
+ : colors.dim }, isCursor ? `${glyphs.chevronRight} ` : " "),
642
+ React.createElement(Text, { bold: isCursor, color: disabled
643
+ ? colors.dim
644
+ : isCursor && pane === "pick"
645
+ ? colors.accent
646
+ : colors.text }, p.label),
647
+ React.createElement(Text, { color: colors.dim }, ` ${p.hint}`),
648
+ disabled ? (React.createElement(Text, { color: colors.dim }, " out of stock")) : null));
649
+ }),
650
+ React.createElement(Box, { marginTop: 1 },
651
+ React.createElement(ActionButton, { keybind: "\u21B5", label: buttonLabel, onPress: () => (canContinue ? commit() : void requestNotify()), focused: pane === "pick" && !hasLocations })),
652
+ selLoc && !canContinue && currentPick ? (React.createElement(Box, null,
653
+ React.createElement(Text, { color: notifyState === "sent" ? colors.dim : colors.warning }, notifyState === "sent"
654
+ ? `We'll email you when ${selLoc.city ?? selLoc.name} has capacity for this ${isVds ? "tier" : "plan"}.`
655
+ : `${selLoc.city ?? selLoc.name} can't fit this ${isVds ? "tier" : "plan"} right now — we can email you when it can.`))) : null,
656
+ notifyState === "error" ? (React.createElement(Box, null,
657
+ React.createElement(Text, { color: colors.error }, "Couldn't save the notification \u2014 try again."))) : null)));
658
+ if (!hasLocations)
659
+ return pickPanel;
660
+ return (React.createElement(Box, { flexDirection: "row", width: 80 },
661
+ pickPanel,
662
+ React.createElement(Panel, { title: "Location", paddingX: 1, paddingY: 1, width: 30, borderColor: pane === "loc" ? colors.accent : undefined },
663
+ React.createElement(Box, { flexDirection: "column" }, locations.map((l, i) => {
664
+ const isCursor = i === locCursor;
665
+ const known = stockByLoc[l.id];
666
+ const oos = planLevelOOS || known === false;
667
+ const name = [l.city ?? l.name, l.country ?? ""]
668
+ .filter(Boolean)
669
+ .join(", ");
670
+ return (React.createElement(Box, { key: l.id, flexDirection: "column" },
671
+ React.createElement(Box, null,
672
+ React.createElement(Text, { color: isCursor
673
+ ? pane === "loc"
674
+ ? colors.accent
675
+ : colors.text
676
+ : colors.dim }, isCursor ? `${glyphs.chevronRight} ` : " "),
677
+ React.createElement(Text, { wrap: "truncate-end", bold: isCursor, dimColor: oos, color: oos ? colors.dim : isCursor ? colors.accent : colors.text }, name)),
678
+ oos ? (React.createElement(Text, { color: colors.dim }, " out of stock")) : known === undefined ? (React.createElement(Text, { color: colors.dim }, ` ${l.code ?? "checking…"}`)) : l.code ? (React.createElement(Text, { color: colors.dim }, ` ${l.code}`)) : null));
679
+ })))));
680
+ }
395
681
  function VpsSpecStep({ customConfig, locations, userEmail, draft, setDraft, goNext, }) {
396
682
  if (!customConfig?.enabled) {
397
683
  return (React.createElement(Panel, { title: "Custom VPS unavailable", paddingX: 3, paddingY: 1, width: 80 },
398
- React.createElement(Text, { color: colors.warning }, "Custom VPS spec is not currently offered. Pick GPU or Dedicated instead.")));
684
+ React.createElement(Text, { color: colors.warning }, "Custom VPS spec is not currently offered. Pick GPU or Instant Metal instead.")));
399
685
  }
400
686
  return (React.createElement(VpsSpecFieldsStep, { customConfig: customConfig, locations: locations, userEmail: userEmail, draft: draft, setDraft: setDraft, goNext: goNext }));
401
687
  }
@@ -433,8 +719,8 @@ function VpsSpecFieldsStep({ customConfig, locations, userEmail, draft, setDraft
433
719
  let cancelled = false;
434
720
  const t = setTimeout(() => {
435
721
  for (const loc of locations) {
436
- api.vpsOrders
437
- .customAvailability({
722
+ api.orders
723
+ .validate({
438
724
  cpu_cores: cpuN,
439
725
  ram_gb: ramN,
440
726
  disk_gb: diskN,
@@ -988,11 +1274,20 @@ function SummaryStep({ draft, cat, onSubmit, onEdit, }) {
988
1274
  const imageName = cat.orderCatalog?.images.find((i) => i.id === draft.imageId)?.display_name;
989
1275
  const plan = cat.orderCatalog?.plans.find((p) => p.id === draft.planId);
990
1276
  const chosenPm = cat.paymentMethods.find((m) => m.id === draft.paymentMethodId);
1277
+ // VDS: pull the picked tier's spec back out of the catalog for display.
1278
+ // The server owns the truth; we resolve here purely for the summary
1279
+ // preview so the user sees what they're about to buy.
1280
+ const vdsCat = draft.kind === "vds"
1281
+ ? (cat.orderCatalog?.categories ?? []).find((c) => c.slug === draft.tierCategorySlug)
1282
+ : null;
1283
+ const vdsTier = vdsCat?.slider_config?.tiers?.[draft.tierIndex ?? -1];
991
1284
  const specLine = draft.kind === "vps"
992
1285
  ? `${draft.customSpec.cpu_cores ?? "?"} vCPU · ${draft.customSpec.ram_gb ?? 0} GB RAM · ${draft.customSpec.disk_gb ?? "?"} GB disk${draft.customSpec.bandwidth_tb ? ` · ${draft.customSpec.bandwidth_tb} TB BW` : ""}${draft.customSpec._dedicated ? " · dedicated" : ""}`
993
- : plan
994
- ? `${plan.name} — ${plan.cpu_cores}c ${plan.ram_gb}G ${plan.disk_gb}G${plan.gpu_model ? ` · ${plan.gpu_model}` : ""}`
995
- : undefined;
1286
+ : draft.kind === "vds" && vdsTier
1287
+ ? `Tier ${draft.tierIndex} — ${vdsTier.cpu}c ${vdsTier.ram_gb}G ${vdsTier.disk_gb}G · ${vdsTier.bandwidth_tb}TB BW ($${vdsTier.monthly_price}/mo)`
1288
+ : plan
1289
+ ? `${plan.name} — ${plan.cpu_cores}c ${plan.ram_gb}G ${plan.disk_gb}G${plan.gpu_model ? ` · ${plan.gpu_model}` : ""}`
1290
+ : undefined;
996
1291
  const paymentLabel = (() => {
997
1292
  const parts = [];
998
1293
  if (draft.applyCredits && cat.creditBalance > 0) {
@@ -1080,6 +1375,8 @@ function tipForStep(step, draft, cancelConfirm) {
1080
1375
  return "↑↓ to choose, Enter to select. Esc to cancel.";
1081
1376
  case "vps-spec":
1082
1377
  return "Tab location/spec · ↑↓ move · Enter continue · Esc back.";
1378
+ case "plan-location":
1379
+ return "Tab pick/location · ↑↓ move · Enter continue (or Notify me if OOS) · Esc back.";
1083
1380
  case "hostname":
1084
1381
  return "Type a hostname (5–253 chars). Enter to confirm · Esc back.";
1085
1382
  case "auth":
@@ -1131,6 +1428,16 @@ function estimateMonthlyCost(draft, cat) {
1131
1428
  return { amount: plan.monthly_price, label: plan.name };
1132
1429
  }
1133
1430
  }
1431
+ if (draft.kind === "vds" && draft.tierCategorySlug && draft.tierIndex != null && cat.orderCatalog) {
1432
+ const vc = (cat.orderCatalog.categories ?? []).find((c) => c.slug === draft.tierCategorySlug);
1433
+ const t = vc?.slider_config?.tiers?.[draft.tierIndex];
1434
+ if (t) {
1435
+ return {
1436
+ amount: typeof t.monthly_price === "string" ? Number(t.monthly_price) : t.monthly_price,
1437
+ label: `VDS tier ${draft.tierIndex} · ${t.cpu}c ${t.ram_gb}GB ${t.disk_gb}GB`,
1438
+ };
1439
+ }
1440
+ }
1134
1441
  if (draft.kind === "vps" && cat.orderCatalog?.custom_config) {
1135
1442
  const cfg = cat.orderCatalog.custom_config;
1136
1443
  const baseCfg = draft.customSpec._dedicated ? cfg.dedicated : cfg;