@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.
- package/dist/api/client.d.ts +28 -37
- package/dist/api/client.js +47 -49
- package/dist/api/client.js.map +1 -1
- package/dist/api/types.d.ts +34 -23
- package/dist/commands/api.js +4 -4
- package/dist/commands/api.js.map +1 -1
- package/dist/commands/catalog.js +1 -1
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/config/set-api-url.js +37 -2
- package/dist/commands/config/set-api-url.js.map +1 -1
- package/dist/commands/gpu/activity.d.ts +1 -0
- package/dist/commands/gpu/activity.js +6 -0
- package/dist/commands/gpu/activity.js.map +1 -1
- package/dist/commands/gpu/bandwidth.d.ts +1 -0
- package/dist/commands/gpu/bandwidth.js +4 -0
- package/dist/commands/gpu/bandwidth.js.map +1 -1
- package/dist/commands/gpu/boot-order.d.ts +1 -0
- package/dist/commands/gpu/boot-order.js +5 -0
- package/dist/commands/gpu/boot-order.js.map +1 -1
- package/dist/commands/gpu/console.d.ts +1 -0
- package/dist/commands/gpu/console.js +5 -0
- package/dist/commands/gpu/console.js.map +1 -1
- package/dist/commands/gpu/details.d.ts +2 -0
- package/dist/commands/gpu/details.js +9 -0
- package/dist/commands/gpu/details.js.map +1 -1
- package/dist/commands/gpu/ip/add.d.ts +1 -0
- package/dist/commands/gpu/ip/add.js +6 -0
- package/dist/commands/gpu/ip/add.js.map +1 -1
- package/dist/commands/gpu/ip/list.d.ts +1 -0
- package/dist/commands/gpu/ip/list.js +4 -0
- package/dist/commands/gpu/ip/list.js.map +1 -1
- package/dist/commands/gpu/list.d.ts +1 -1
- package/dist/commands/gpu/list.js +2 -2
- package/dist/commands/gpu/list.js.map +1 -1
- package/dist/commands/gpu/order.js +2 -0
- package/dist/commands/gpu/order.js.map +1 -1
- package/dist/commands/gpu/power.d.ts +1 -0
- package/dist/commands/gpu/power.js +6 -0
- package/dist/commands/gpu/power.js.map +1 -1
- package/dist/commands/gpu/reinstall.d.ts +3 -0
- package/dist/commands/gpu/reinstall.js +7 -0
- package/dist/commands/gpu/reinstall.js.map +1 -1
- package/dist/commands/gpu/resize.d.ts +1 -0
- package/dist/commands/gpu/resize.js +6 -0
- package/dist/commands/gpu/resize.js.map +1 -1
- package/dist/commands/gpu/stats.d.ts +1 -0
- package/dist/commands/gpu/stats.js +5 -0
- package/dist/commands/gpu/stats.js.map +1 -1
- package/dist/commands/instant-metal/activity.d.ts +1 -0
- package/dist/commands/instant-metal/activity.js +6 -0
- package/dist/commands/instant-metal/activity.js.map +1 -1
- package/dist/commands/instant-metal/bandwidth.d.ts +1 -0
- package/dist/commands/instant-metal/bandwidth.js +4 -0
- package/dist/commands/instant-metal/bandwidth.js.map +1 -1
- package/dist/commands/instant-metal/boot-order.d.ts +1 -0
- package/dist/commands/instant-metal/boot-order.js +5 -0
- package/dist/commands/instant-metal/boot-order.js.map +1 -1
- package/dist/commands/instant-metal/console.d.ts +1 -0
- package/dist/commands/instant-metal/console.js +5 -0
- package/dist/commands/instant-metal/console.js.map +1 -1
- package/dist/commands/instant-metal/details.d.ts +2 -0
- package/dist/commands/instant-metal/details.js +9 -0
- package/dist/commands/instant-metal/details.js.map +1 -1
- package/dist/commands/instant-metal/ip/add.d.ts +1 -0
- package/dist/commands/instant-metal/ip/add.js +6 -0
- package/dist/commands/instant-metal/ip/add.js.map +1 -1
- package/dist/commands/instant-metal/ip/list.d.ts +1 -0
- package/dist/commands/instant-metal/ip/list.js +4 -0
- package/dist/commands/instant-metal/ip/list.js.map +1 -1
- package/dist/commands/instant-metal/list.d.ts +1 -1
- package/dist/commands/instant-metal/list.js +2 -2
- package/dist/commands/instant-metal/list.js.map +1 -1
- package/dist/commands/instant-metal/order.js +2 -0
- package/dist/commands/instant-metal/order.js.map +1 -1
- package/dist/commands/instant-metal/power.d.ts +1 -0
- package/dist/commands/instant-metal/power.js +6 -0
- package/dist/commands/instant-metal/power.js.map +1 -1
- package/dist/commands/instant-metal/reinstall.d.ts +3 -0
- package/dist/commands/instant-metal/reinstall.js +7 -0
- package/dist/commands/instant-metal/reinstall.js.map +1 -1
- package/dist/commands/instant-metal/resize.d.ts +1 -0
- package/dist/commands/instant-metal/resize.js +6 -0
- package/dist/commands/instant-metal/resize.js.map +1 -1
- package/dist/commands/instant-metal/stats.d.ts +1 -0
- package/dist/commands/instant-metal/stats.js +5 -0
- package/dist/commands/instant-metal/stats.js.map +1 -1
- package/dist/commands/order/gpu.js +2 -0
- package/dist/commands/order/gpu.js.map +1 -1
- package/dist/commands/order/instant-metal.js +2 -0
- package/dist/commands/order/instant-metal.js.map +1 -1
- package/dist/commands/order/storage.js +1 -1
- package/dist/commands/order/storage.js.map +1 -1
- package/dist/commands/order/vps.js +2 -0
- package/dist/commands/order/vps.js.map +1 -1
- package/dist/commands/os-images.js +1 -1
- package/dist/commands/os-images.js.map +1 -1
- package/dist/commands/resources.js +22 -21
- package/dist/commands/resources.js.map +1 -1
- package/dist/commands/storage/attach.d.ts +1 -1
- package/dist/commands/storage/attach.js +11 -11
- package/dist/commands/storage/attach.js.map +1 -1
- package/dist/commands/storage/detach.js +2 -2
- package/dist/commands/storage/detach.js.map +1 -1
- package/dist/commands/storage/list.js +1 -1
- package/dist/commands/storage/list.js.map +1 -1
- package/dist/commands/storage/order.d.ts +1 -1
- package/dist/commands/storage/order.js +10 -10
- package/dist/commands/storage/order.js.map +1 -1
- package/dist/commands/vds/activity.d.ts +1 -0
- package/dist/commands/vds/activity.js +6 -0
- package/dist/commands/vds/activity.js.map +1 -1
- package/dist/commands/vds/bandwidth.d.ts +1 -0
- package/dist/commands/vds/bandwidth.js +4 -0
- package/dist/commands/vds/bandwidth.js.map +1 -1
- package/dist/commands/vds/boot-order.d.ts +1 -0
- package/dist/commands/vds/boot-order.js +5 -0
- package/dist/commands/vds/boot-order.js.map +1 -1
- package/dist/commands/vds/console.d.ts +1 -0
- package/dist/commands/vds/console.js +5 -0
- package/dist/commands/vds/console.js.map +1 -1
- package/dist/commands/vds/details.d.ts +2 -0
- package/dist/commands/vds/details.js +9 -0
- package/dist/commands/vds/details.js.map +1 -1
- package/dist/commands/vds/ip/add.d.ts +1 -0
- package/dist/commands/vds/ip/add.js +6 -0
- package/dist/commands/vds/ip/add.js.map +1 -1
- package/dist/commands/vds/ip/list.d.ts +1 -0
- package/dist/commands/vds/ip/list.js +4 -0
- package/dist/commands/vds/ip/list.js.map +1 -1
- package/dist/commands/vds/list.d.ts +1 -1
- package/dist/commands/vds/list.js +2 -2
- package/dist/commands/vds/list.js.map +1 -1
- package/dist/commands/vds/order.d.ts +19 -2
- package/dist/commands/vds/order.js +210 -7
- package/dist/commands/vds/order.js.map +1 -1
- package/dist/commands/vds/power.d.ts +1 -0
- package/dist/commands/vds/power.js +6 -0
- package/dist/commands/vds/power.js.map +1 -1
- package/dist/commands/vds/reinstall.d.ts +3 -0
- package/dist/commands/vds/reinstall.js +7 -0
- package/dist/commands/vds/reinstall.js.map +1 -1
- package/dist/commands/vds/resize.d.ts +1 -0
- package/dist/commands/vds/resize.js +6 -0
- package/dist/commands/vds/resize.js.map +1 -1
- package/dist/commands/vds/stats.d.ts +1 -0
- package/dist/commands/vds/stats.js +5 -0
- package/dist/commands/vds/stats.js.map +1 -1
- package/dist/commands/vps/activity.js +4 -4
- package/dist/commands/vps/activity.js.map +1 -1
- package/dist/commands/vps/bandwidth.js +5 -5
- package/dist/commands/vps/bandwidth.js.map +1 -1
- package/dist/commands/vps/boot-order.js +5 -5
- package/dist/commands/vps/boot-order.js.map +1 -1
- package/dist/commands/vps/console.js +9 -9
- package/dist/commands/vps/console.js.map +1 -1
- package/dist/commands/vps/details.d.ts +4 -0
- package/dist/commands/vps/details.js +20 -9
- package/dist/commands/vps/details.js.map +1 -1
- package/dist/commands/vps/ip/add.js +10 -9
- package/dist/commands/vps/ip/add.js.map +1 -1
- package/dist/commands/vps/ip/list.js +6 -6
- package/dist/commands/vps/ip/list.js.map +1 -1
- package/dist/commands/vps/list.d.ts +3 -1
- package/dist/commands/vps/list.js +11 -7
- package/dist/commands/vps/list.js.map +1 -1
- package/dist/commands/vps/order.d.ts +3 -2
- package/dist/commands/vps/order.js +25 -16
- package/dist/commands/vps/order.js.map +1 -1
- package/dist/commands/vps/power.js +14 -13
- package/dist/commands/vps/power.js.map +1 -1
- package/dist/commands/vps/reinstall.d.ts +2 -0
- package/dist/commands/vps/reinstall.js +41 -14
- package/dist/commands/vps/reinstall.js.map +1 -1
- package/dist/commands/vps/resize.js +11 -10
- package/dist/commands/vps/resize.js.map +1 -1
- package/dist/commands/vps/stats.js +7 -7
- package/dist/commands/vps/stats.js.map +1 -1
- package/dist/help.js +0 -1
- package/dist/help.js.map +1 -1
- package/dist/tui/screens/Dashboard.js +1 -1
- package/dist/tui/screens/Dashboard.js.map +1 -1
- package/dist/tui/screens/Order.js +352 -45
- package/dist/tui/screens/Order.js.map +1 -1
- package/dist/tui/screens/Resources.js +31 -60
- package/dist/tui/screens/Resources.js.map +1 -1
- package/dist/ui/image-resolve.js +1 -1
- package/dist/ui/image-resolve.js.map +1 -1
- package/dist/ui/payment.d.ts +1 -1
- package/dist/ui/resource-detail.d.ts +3 -0
- package/dist/ui/{vps-detail.js → resource-detail.js} +11 -7
- package/dist/ui/resource-detail.js.map +1 -0
- package/dist/ui/resource-resolve.d.ts +4 -0
- package/dist/ui/resource-resolve.js +24 -0
- package/dist/ui/resource-resolve.js.map +1 -0
- package/dist/ui/wait.d.ts +1 -1
- package/dist/ui/wait.js +4 -4
- package/dist/ui/wait.js.map +1 -1
- package/package.json +1 -4
- package/dist/commands/bare-metal/index.d.ts +0 -4
- package/dist/commands/bare-metal/index.js +0 -5
- package/dist/commands/bare-metal/index.js.map +0 -1
- package/dist/commands/bare-metal/list.d.ts +0 -9
- package/dist/commands/bare-metal/list.js +0 -38
- package/dist/commands/bare-metal/list.js.map +0 -1
- package/dist/commands/bare-metal/order.d.ts +0 -6
- package/dist/commands/bare-metal/order.js +0 -27
- package/dist/commands/bare-metal/order.js.map +0 -1
- package/dist/ui/vps-detail.d.ts +0 -3
- package/dist/ui/vps-detail.js.map +0 -1
- package/dist/ui/vps-resolve.d.ts +0 -4
- package/dist/ui/vps-resolve.js +0 -24
- 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
|
-
|
|
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.
|
|
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:
|
|
216
|
-
custom_spec:
|
|
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.
|
|
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(
|
|
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: "
|
|
319
|
-
{ value: "storage", label: "Storage", hint: "block volume (attach to a
|
|
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
|
|
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.
|
|
437
|
-
.
|
|
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
|
-
:
|
|
994
|
-
?
|
|
995
|
-
:
|
|
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;
|