smolmachines 1.25.1 → 1.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/generated/smolfleet.d.ts +227 -3
- package/dist/index.d.ts +1 -1
- package/dist/machine.d.ts +15 -2
- package/dist/machine.js +34 -2
- package/dist/transport.d.ts +20 -2
- package/dist/transport.js +109 -27
- package/dist/types.d.ts +32 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -321,6 +321,17 @@ await m.publishCacheDisk(); // v1
|
|
|
321
321
|
// Every machine created with cacheDisk: { cache: "deps" } now starts from v1.
|
|
322
322
|
```
|
|
323
323
|
|
|
324
|
+
One checkpoint can resume with many caches. Create the base machine with an
|
|
325
|
+
empty cache as a slot, which attaches it unmounted, and checkpoint it warm.
|
|
326
|
+
Each restore then mounts its own cache there, no larger than the slot and at
|
|
327
|
+
the slot's mount path:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
const base = await Machine.create({ image: "node:22", cacheDisk: { cache: "slot-20g", slot: true } });
|
|
331
|
+
const { id } = await base.checkpoint();
|
|
332
|
+
const a = await Machine.restoreCheckpoint(id, "project-a", undefined, { cacheDisk: { cache: "project-a" } });
|
|
333
|
+
```
|
|
334
|
+
|
|
324
335
|
## Building from source
|
|
325
336
|
|
|
326
337
|
This package's native core lives alongside it (Rust, `src/*.rs`) and links the
|
|
@@ -143,6 +143,26 @@ export interface paths {
|
|
|
143
143
|
patch?: never;
|
|
144
144
|
trace?: never;
|
|
145
145
|
};
|
|
146
|
+
"/v1/account/plan/checkout": {
|
|
147
|
+
parameters: {
|
|
148
|
+
query?: never;
|
|
149
|
+
header?: never;
|
|
150
|
+
path?: never;
|
|
151
|
+
cookie?: never;
|
|
152
|
+
};
|
|
153
|
+
get?: never;
|
|
154
|
+
put?: never;
|
|
155
|
+
/**
|
|
156
|
+
* Subscribe to a plan through Stripe Checkout.
|
|
157
|
+
* @description Returns a Stripe-hosted page that shows the plan and its monthly price, takes a card and collects the first month. Once Stripe confirms the payment the account moves onto the plan and keeps the card for later charges. For an account with no card on file; with one, change plan directly. Requires the `billing:write` scope.
|
|
158
|
+
*/
|
|
159
|
+
post: operations["account_plan_checkout"];
|
|
160
|
+
delete?: never;
|
|
161
|
+
options?: never;
|
|
162
|
+
head?: never;
|
|
163
|
+
patch?: never;
|
|
164
|
+
trace?: never;
|
|
165
|
+
};
|
|
146
166
|
"/v1/account/plans": {
|
|
147
167
|
parameters: {
|
|
148
168
|
query?: never;
|
|
@@ -439,6 +459,26 @@ export interface paths {
|
|
|
439
459
|
patch?: never;
|
|
440
460
|
trace?: never;
|
|
441
461
|
};
|
|
462
|
+
"/v1/billing/card": {
|
|
463
|
+
parameters: {
|
|
464
|
+
query?: never;
|
|
465
|
+
header?: never;
|
|
466
|
+
path?: never;
|
|
467
|
+
cookie?: never;
|
|
468
|
+
};
|
|
469
|
+
/**
|
|
470
|
+
* Get the card on file.
|
|
471
|
+
* @description Returns the brand, last four digits and expiry of the card the account's plan fee and auto top-ups are charged to. Requires the `usage:read` or `machine:read` scope.
|
|
472
|
+
*/
|
|
473
|
+
get: operations["billing_card_get"];
|
|
474
|
+
put?: never;
|
|
475
|
+
post?: never;
|
|
476
|
+
delete?: never;
|
|
477
|
+
options?: never;
|
|
478
|
+
head?: never;
|
|
479
|
+
patch?: never;
|
|
480
|
+
trace?: never;
|
|
481
|
+
};
|
|
442
482
|
"/v1/billing/checkout-session": {
|
|
443
483
|
parameters: {
|
|
444
484
|
query?: never;
|
|
@@ -2323,8 +2363,7 @@ export interface components {
|
|
|
2323
2363
|
scopes: string[];
|
|
2324
2364
|
};
|
|
2325
2365
|
/**
|
|
2326
|
-
* @description
|
|
2327
|
-
* Threshold/amount are the RESOLVED effective values (per-tenant override, else
|
|
2366
|
+
* @description Threshold/amount are the RESOLVED effective values (per-tenant override, else
|
|
2328
2367
|
* fleet default), so the UI can render exactly what will happen.
|
|
2329
2368
|
*/
|
|
2330
2369
|
AutoRechargeSettings: {
|
|
@@ -2388,6 +2427,21 @@ export interface components {
|
|
|
2388
2427
|
*/
|
|
2389
2428
|
version: number;
|
|
2390
2429
|
};
|
|
2430
|
+
/**
|
|
2431
|
+
* @description Opt-in saved-card auto-recharge settings (`GET`/`POST /v1/billing/auto-recharge`).
|
|
2432
|
+
* The card on file: what the account's plan fee and auto top-ups are charged
|
|
2433
|
+
* to, as Stripe describes it. Never the number itself.
|
|
2434
|
+
*/
|
|
2435
|
+
CardOnFile: {
|
|
2436
|
+
/** @description Card network, e.g. `visa`, `mastercard`, `amex`. */
|
|
2437
|
+
brand: string;
|
|
2438
|
+
/** Format: int32 */
|
|
2439
|
+
expMonth: number;
|
|
2440
|
+
/** Format: int32 */
|
|
2441
|
+
expYear: number;
|
|
2442
|
+
/** @description Last four digits. */
|
|
2443
|
+
last4: string;
|
|
2444
|
+
};
|
|
2391
2445
|
/**
|
|
2392
2446
|
* @description Ask to move this tenant onto a different plan. Only plans whose
|
|
2393
2447
|
* `self_serve` flag is set may be chosen; operator tiers are assigned by an
|
|
@@ -2982,7 +3036,11 @@ export interface components {
|
|
|
2982
3036
|
/** @description The cache disk a machine runs with. */
|
|
2983
3037
|
MachineCacheDisk: {
|
|
2984
3038
|
cacheDiskId: string;
|
|
3039
|
+
/** @description Put in a cache slot by a restore, which needs an engine that serves slots. */
|
|
3040
|
+
intoSlot?: boolean;
|
|
2985
3041
|
mountPath: string;
|
|
3042
|
+
/** @description Attached unmounted, as a slot a restore can put another cache in. */
|
|
3043
|
+
slot?: boolean;
|
|
2986
3044
|
/** Format: int32 */
|
|
2987
3045
|
version: number;
|
|
2988
3046
|
};
|
|
@@ -2998,6 +3056,12 @@ export interface components {
|
|
|
2998
3056
|
* left out.
|
|
2999
3057
|
*/
|
|
3000
3058
|
mountPath?: string | null;
|
|
3059
|
+
/**
|
|
3060
|
+
* @description Attach it as a slot: left unmounted, so a checkpoint of the machine can
|
|
3061
|
+
* be restored with a different cache mounted here (`cacheDisk` on restore).
|
|
3062
|
+
* The slot's size is this cache disk's size; its contents are never read.
|
|
3063
|
+
*/
|
|
3064
|
+
slot?: boolean;
|
|
3001
3065
|
/**
|
|
3002
3066
|
* Format: int32
|
|
3003
3067
|
* @description Version to start from; the latest when left out.
|
|
@@ -3729,6 +3793,7 @@ export interface components {
|
|
|
3729
3793
|
};
|
|
3730
3794
|
/** @description Create a stopped machine from a durable portable checkpoint. */
|
|
3731
3795
|
RestorePortableCheckpointRequest: {
|
|
3796
|
+
cacheDisk?: null | components["schemas"]["MachineCacheDiskSpec"];
|
|
3732
3797
|
name: string;
|
|
3733
3798
|
};
|
|
3734
3799
|
ScaleRequest: {
|
|
@@ -4268,7 +4333,7 @@ export interface operations {
|
|
|
4268
4333
|
"application/json": components["schemas"]["ErrorResponse"];
|
|
4269
4334
|
};
|
|
4270
4335
|
};
|
|
4271
|
-
/** @description The plan's monthly fee has no saved card to bill, or the card was declined; the plan does not move. */
|
|
4336
|
+
/** @description The plan's monthly fee has no saved card to bill (subscribe through /v1/account/plan/checkout instead), or the card was declined; the plan does not move. */
|
|
4272
4337
|
402: {
|
|
4273
4338
|
headers: {
|
|
4274
4339
|
[name: string]: unknown;
|
|
@@ -4333,6 +4398,91 @@ export interface operations {
|
|
|
4333
4398
|
};
|
|
4334
4399
|
};
|
|
4335
4400
|
};
|
|
4401
|
+
account_plan_checkout: {
|
|
4402
|
+
parameters: {
|
|
4403
|
+
query?: never;
|
|
4404
|
+
header?: never;
|
|
4405
|
+
path?: never;
|
|
4406
|
+
cookie?: never;
|
|
4407
|
+
};
|
|
4408
|
+
requestBody: {
|
|
4409
|
+
content: {
|
|
4410
|
+
"application/json": components["schemas"]["ChangePlanRequest"];
|
|
4411
|
+
};
|
|
4412
|
+
};
|
|
4413
|
+
responses: {
|
|
4414
|
+
/** @description Returns {"url": <stripe-hosted url>} for a Stripe Checkout in subscription mode; the browser comes back to the console's billing page. */
|
|
4415
|
+
200: {
|
|
4416
|
+
headers: {
|
|
4417
|
+
[name: string]: unknown;
|
|
4418
|
+
};
|
|
4419
|
+
content?: never;
|
|
4420
|
+
};
|
|
4421
|
+
/** @description Missing or invalid API key */
|
|
4422
|
+
401: {
|
|
4423
|
+
headers: {
|
|
4424
|
+
[name: string]: unknown;
|
|
4425
|
+
};
|
|
4426
|
+
content: {
|
|
4427
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
4428
|
+
};
|
|
4429
|
+
};
|
|
4430
|
+
/** @description Key lacks the required scope */
|
|
4431
|
+
403: {
|
|
4432
|
+
headers: {
|
|
4433
|
+
[name: string]: unknown;
|
|
4434
|
+
};
|
|
4435
|
+
content: {
|
|
4436
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
4437
|
+
};
|
|
4438
|
+
};
|
|
4439
|
+
/** @description The account is already on that plan, or already pays a plan fee */
|
|
4440
|
+
409: {
|
|
4441
|
+
headers: {
|
|
4442
|
+
[name: string]: unknown;
|
|
4443
|
+
};
|
|
4444
|
+
content: {
|
|
4445
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
4446
|
+
};
|
|
4447
|
+
};
|
|
4448
|
+
/** @description That plan is not self-serve, or has no monthly fee */
|
|
4449
|
+
422: {
|
|
4450
|
+
headers: {
|
|
4451
|
+
[name: string]: unknown;
|
|
4452
|
+
};
|
|
4453
|
+
content: {
|
|
4454
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
4455
|
+
};
|
|
4456
|
+
};
|
|
4457
|
+
/** @description Rate limited; retry after the Retry-After header */
|
|
4458
|
+
429: {
|
|
4459
|
+
headers: {
|
|
4460
|
+
[name: string]: unknown;
|
|
4461
|
+
};
|
|
4462
|
+
content: {
|
|
4463
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
4464
|
+
};
|
|
4465
|
+
};
|
|
4466
|
+
/** @description Internal error */
|
|
4467
|
+
500: {
|
|
4468
|
+
headers: {
|
|
4469
|
+
[name: string]: unknown;
|
|
4470
|
+
};
|
|
4471
|
+
content: {
|
|
4472
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
4473
|
+
};
|
|
4474
|
+
};
|
|
4475
|
+
/** @description Paid plans are not enabled on this deployment */
|
|
4476
|
+
503: {
|
|
4477
|
+
headers: {
|
|
4478
|
+
[name: string]: unknown;
|
|
4479
|
+
};
|
|
4480
|
+
content: {
|
|
4481
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
4482
|
+
};
|
|
4483
|
+
};
|
|
4484
|
+
};
|
|
4485
|
+
};
|
|
4336
4486
|
account_plans_list: {
|
|
4337
4487
|
parameters: {
|
|
4338
4488
|
query?: never;
|
|
@@ -5549,6 +5699,80 @@ export interface operations {
|
|
|
5549
5699
|
};
|
|
5550
5700
|
};
|
|
5551
5701
|
};
|
|
5702
|
+
billing_card_get: {
|
|
5703
|
+
parameters: {
|
|
5704
|
+
query?: never;
|
|
5705
|
+
header?: never;
|
|
5706
|
+
path?: never;
|
|
5707
|
+
cookie?: never;
|
|
5708
|
+
};
|
|
5709
|
+
requestBody?: never;
|
|
5710
|
+
responses: {
|
|
5711
|
+
/** @description The card on file */
|
|
5712
|
+
200: {
|
|
5713
|
+
headers: {
|
|
5714
|
+
[name: string]: unknown;
|
|
5715
|
+
};
|
|
5716
|
+
content: {
|
|
5717
|
+
"application/json": components["schemas"]["CardOnFile"];
|
|
5718
|
+
};
|
|
5719
|
+
};
|
|
5720
|
+
/** @description Missing or invalid API key */
|
|
5721
|
+
401: {
|
|
5722
|
+
headers: {
|
|
5723
|
+
[name: string]: unknown;
|
|
5724
|
+
};
|
|
5725
|
+
content: {
|
|
5726
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
5727
|
+
};
|
|
5728
|
+
};
|
|
5729
|
+
/** @description Key lacks the required scope */
|
|
5730
|
+
403: {
|
|
5731
|
+
headers: {
|
|
5732
|
+
[name: string]: unknown;
|
|
5733
|
+
};
|
|
5734
|
+
content: {
|
|
5735
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
5736
|
+
};
|
|
5737
|
+
};
|
|
5738
|
+
/** @description No card on file, or card payments are not enabled */
|
|
5739
|
+
404: {
|
|
5740
|
+
headers: {
|
|
5741
|
+
[name: string]: unknown;
|
|
5742
|
+
};
|
|
5743
|
+
content: {
|
|
5744
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
5745
|
+
};
|
|
5746
|
+
};
|
|
5747
|
+
/** @description Rate limited; retry after the Retry-After header */
|
|
5748
|
+
429: {
|
|
5749
|
+
headers: {
|
|
5750
|
+
[name: string]: unknown;
|
|
5751
|
+
};
|
|
5752
|
+
content: {
|
|
5753
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
5754
|
+
};
|
|
5755
|
+
};
|
|
5756
|
+
/** @description Internal error */
|
|
5757
|
+
500: {
|
|
5758
|
+
headers: {
|
|
5759
|
+
[name: string]: unknown;
|
|
5760
|
+
};
|
|
5761
|
+
content: {
|
|
5762
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
5763
|
+
};
|
|
5764
|
+
};
|
|
5765
|
+
/** @description The payment provider did not answer */
|
|
5766
|
+
502: {
|
|
5767
|
+
headers: {
|
|
5768
|
+
[name: string]: unknown;
|
|
5769
|
+
};
|
|
5770
|
+
content: {
|
|
5771
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
5772
|
+
};
|
|
5773
|
+
};
|
|
5774
|
+
};
|
|
5775
|
+
};
|
|
5552
5776
|
billing_checkout_session: {
|
|
5553
5777
|
parameters: {
|
|
5554
5778
|
query?: never;
|
package/dist/index.d.ts
CHANGED
|
@@ -22,4 +22,4 @@ export type { BuildRolloutJobOptions, EnsureRolloutExecutorOptions, RolloutBatch
|
|
|
22
22
|
export { SmolError, NotSupportedError, InvalidConfigError, ExecutionError } from './errors';
|
|
23
23
|
export type { CloudCheckpointInfo } from './transport';
|
|
24
24
|
export type { LocalAvailability, LocalUnavailableCode } from './availability';
|
|
25
|
-
export type { MachineConfig, CredentialSpec, CacheDiskRef, CacheDiskInfo, CacheDiskVersion, CreateCacheDiskOptions, PublishedCacheDisk, NetworkPolicy, RestoreCheckpointOptions, MachineState, ResourceSpec, MountSpec, PortSpec, BranchOptions, BranchBatchOptions, CheckpointOptions, ResizeOptions, MachineResources, ForkOptions, ForkBatchOptions, AssignOptions, ExecOptions, ExecResult, MachineUsageReport, ShareLink, PortableCheckpointInfo, MachineUsageTotals, MachineCostBreakdown, ImageInfo, MachineSummary, ListOptions, ExecEvent, ConnectOptions, EgressInterceptor, StartOptions, WaitReadyOptions, PortEndpoint, } from './types';
|
|
25
|
+
export type { MachineConfig, CredentialSpec, CacheDiskRef, CacheDiskInfo, CacheDiskVersion, CreateCacheDiskOptions, PublishedCacheDisk, NetworkPolicy, RestoreCheckpointOptions, MachineState, ResourceSpec, MountSpec, PortSpec, BranchOptions, BranchBatchOptions, CheckpointOptions, ResizeOptions, MachineResources, ForkOptions, ForkBatchOptions, AssignOptions, ExecOptions, ExecResult, MachineUsageReport, ShareLink, PortableCheckpointInfo, MachineUsageTotals, MachineCostBreakdown, ImageInfo, MachineSummary, ListOptions, ExecEvent, ConnectOptions, EgressInterceptor, StartOptions, ResumeOptions, WaitReadyOptions, PortEndpoint, } from './types';
|
package/dist/machine.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type LocalAvailability } from "./availability";
|
|
11
11
|
import { type CloudCheckpointInfo, type Transport } from "./transport";
|
|
12
|
-
import type { CacheDiskInfo, CreateCacheDiskOptions, PublishedCacheDisk, MachineResources, ResizeOptions, AssignOptions, BranchBatchOptions, BranchOptions, CheckpointOptions, ConnectOptions, ExecEvent, ExecOptions, ExecResult, ForkBatchOptions, ForkOptions, ImageInfo, ListOptions, MachineConfig, MachineSummary, NetworkPolicy, RestoreCheckpointOptions, StartOptions, MachineUsageReport, ShareLink, PortableCheckpointInfo, PortEndpoint, PortSpec, WaitReadyOptions } from "./types";
|
|
12
|
+
import type { CacheDiskInfo, CreateCacheDiskOptions, PublishedCacheDisk, MachineResources, ResizeOptions, AssignOptions, BranchBatchOptions, BranchOptions, CheckpointOptions, ConnectOptions, ExecEvent, ExecOptions, ExecResult, ForkBatchOptions, ForkOptions, ImageInfo, ListOptions, MachineConfig, MachineSummary, NetworkPolicy, RestoreCheckpointOptions, StartOptions, ResumeOptions, MachineUsageReport, ShareLink, PortableCheckpointInfo, PortEndpoint, PortSpec, WaitReadyOptions } from "./types";
|
|
13
13
|
export declare class Machine {
|
|
14
14
|
private readonly transport;
|
|
15
15
|
private constructor();
|
|
@@ -50,6 +50,10 @@ export declare class Machine {
|
|
|
50
50
|
* @param options `labels`: keep machines carrying every one of these
|
|
51
51
|
*/
|
|
52
52
|
static list(conn?: ConnectOptions, options?: ListOptions): Promise<MachineSummary[]>;
|
|
53
|
+
/** Check that a cloud connection works: the key is accepted and the base URL
|
|
54
|
+
* answers with a machine list. Throws otherwise, including when a proxy or
|
|
55
|
+
* a wrong URL answers with something that is not a machine list. */
|
|
56
|
+
static probe(conn?: ConnectOptions): Promise<void>;
|
|
53
57
|
/** Upload a `.checkpoint` file taken on this computer to the cloud, so it
|
|
54
58
|
* can be restored there by the returned id. Restoring the file with a cloud
|
|
55
59
|
* target uploads it for you. */
|
|
@@ -85,6 +89,15 @@ export declare class Machine {
|
|
|
85
89
|
* process launched; it does not mean the guest or workload is ready. Use
|
|
86
90
|
* `ready()` or `waitUntilReady()` before doing work. */
|
|
87
91
|
state(): Promise<string>;
|
|
92
|
+
/** The machine's full cloud record, every field the control plane returns
|
|
93
|
+
* (env, readiness, ports, …). Cloud only. */
|
|
94
|
+
info(): Promise<Record<string, unknown>>;
|
|
95
|
+
/** The last `tail` lines of the machine's console log. Cloud only. */
|
|
96
|
+
logs(tail?: number): Promise<string>;
|
|
97
|
+
/** Export this stopped machine as a `.smolmachine` in your registry
|
|
98
|
+
* namespace, re-deployable anywhere. Distinct from a portable checkpoint,
|
|
99
|
+
* which also keeps live memory. Cloud only. */
|
|
100
|
+
exportArtifact(): Promise<Record<string, unknown>>;
|
|
88
101
|
/** Whether the machine is READY to do work. `state()` becoming "started"
|
|
89
102
|
* means only that the VM process launched — the guest is still booting and
|
|
90
103
|
* is NOT yet usable. `ready` becomes true once the in-VM agent is reachable
|
|
@@ -140,7 +153,7 @@ export declare class Machine {
|
|
|
140
153
|
/** Save RAM and disk durably, then stop at that execution boundary. */
|
|
141
154
|
pause(): Promise<void>;
|
|
142
155
|
/** Resume saved execution in this machine. */
|
|
143
|
-
resume(): Promise<void>;
|
|
156
|
+
resume(options?: ResumeOptions): Promise<void>;
|
|
144
157
|
/** Boot a stopped machine with its disk state, not its previous RAM.
|
|
145
158
|
* The local handle reuses its interceptor binding or accepts a fresh one. */
|
|
146
159
|
start(options?: StartOptions): Promise<void>;
|
package/dist/machine.js
CHANGED
|
@@ -31,6 +31,7 @@ const transport_1 = require("./transport");
|
|
|
31
31
|
function makeExecResult(r) {
|
|
32
32
|
const success = r.exitCode === 0;
|
|
33
33
|
return {
|
|
34
|
+
...(r.pid !== undefined ? { pid: r.pid } : {}),
|
|
34
35
|
exitCode: r.exitCode,
|
|
35
36
|
stdout: r.stdout,
|
|
36
37
|
stderr: r.stderr,
|
|
@@ -102,6 +103,14 @@ class Machine {
|
|
|
102
103
|
static async list(conn = {}, options = {}) {
|
|
103
104
|
return (0, transport_1.listMachines)(conn, options);
|
|
104
105
|
}
|
|
106
|
+
/** Check that a cloud connection works: the key is accepted and the base URL
|
|
107
|
+
* answers with a machine list. Throws otherwise, including when a proxy or
|
|
108
|
+
* a wrong URL answers with something that is not a machine list. */
|
|
109
|
+
static async probe(conn = {}) {
|
|
110
|
+
if (conn.target === "local")
|
|
111
|
+
throw new errors_1.NotSupportedError("probe is cloud-only.");
|
|
112
|
+
await (0, transport_1.listMachines)({ ...conn, target: "cloud" });
|
|
113
|
+
}
|
|
105
114
|
/** Upload a `.checkpoint` file taken on this computer to the cloud, so it
|
|
106
115
|
* can be restored there by the returned id. Restoring the file with a cloud
|
|
107
116
|
* target uploads it for you. */
|
|
@@ -167,6 +176,27 @@ class Machine {
|
|
|
167
176
|
state() {
|
|
168
177
|
return this.transport.state();
|
|
169
178
|
}
|
|
179
|
+
/** The machine's full cloud record, every field the control plane returns
|
|
180
|
+
* (env, readiness, ports, …). Cloud only. */
|
|
181
|
+
info() {
|
|
182
|
+
if (!this.transport.info)
|
|
183
|
+
throw new errors_1.NotSupportedError("info is cloud-only.");
|
|
184
|
+
return this.transport.info();
|
|
185
|
+
}
|
|
186
|
+
/** The last `tail` lines of the machine's console log. Cloud only. */
|
|
187
|
+
logs(tail = 100) {
|
|
188
|
+
if (!this.transport.logs)
|
|
189
|
+
throw new errors_1.NotSupportedError("logs is cloud-only.");
|
|
190
|
+
return this.transport.logs(tail);
|
|
191
|
+
}
|
|
192
|
+
/** Export this stopped machine as a `.smolmachine` in your registry
|
|
193
|
+
* namespace, re-deployable anywhere. Distinct from a portable checkpoint,
|
|
194
|
+
* which also keeps live memory. Cloud only. */
|
|
195
|
+
exportArtifact() {
|
|
196
|
+
if (!this.transport.exportArtifact)
|
|
197
|
+
throw new errors_1.NotSupportedError("exportArtifact is cloud-only.");
|
|
198
|
+
return this.transport.exportArtifact();
|
|
199
|
+
}
|
|
170
200
|
/** Whether the machine is READY to do work. `state()` becoming "started"
|
|
171
201
|
* means only that the VM process launched — the guest is still booting and
|
|
172
202
|
* is NOT yet usable. `ready` becomes true once the in-VM agent is reachable
|
|
@@ -262,11 +292,13 @@ class Machine {
|
|
|
262
292
|
/** Save RAM and disk durably, then stop at that execution boundary. */
|
|
263
293
|
pause() { return this.transport.pause(); }
|
|
264
294
|
/** Resume saved execution in this machine. */
|
|
265
|
-
resume(
|
|
295
|
+
resume(options = {}) {
|
|
296
|
+
return this.transport.resume(options.waitUntilReady ?? true);
|
|
297
|
+
}
|
|
266
298
|
/** Boot a stopped machine with its disk state, not its previous RAM.
|
|
267
299
|
* The local handle reuses its interceptor binding or accepts a fresh one. */
|
|
268
300
|
start(options = {}) {
|
|
269
|
-
return this.transport.start(options.egressInterceptor);
|
|
301
|
+
return this.transport.start(options.egressInterceptor, options.waitUntilReady ?? true);
|
|
270
302
|
}
|
|
271
303
|
delete(opts) {
|
|
272
304
|
if (opts?.includeUsage)
|
package/dist/transport.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { type NativeMachineConfig } from "./native";
|
|
|
10
10
|
import type { CheckpointOptions, CacheDiskInfo, CreateCacheDiskOptions, PublishedCacheDisk, MachineResources, ResizeOptions, ConnectOptions, NetworkPolicy, RestoreCheckpointOptions, ExecEvent, ExecOptions, EgressInterceptor, AssignOptions, ForkBatchOptions, ForkOptions, ImageInfo, ListOptions, MachineConfig, MachineSummary, MachineUsageReport, ShareLink, PortableCheckpointInfo, PortEndpoint, PortSpec, WaitReadyOptions } from "./types";
|
|
11
11
|
/** Raw exec result (the ergonomic wrapper is added in machine.ts). */
|
|
12
12
|
export interface RawExec {
|
|
13
|
+
/** The detached process's pid, for a background exec. */
|
|
14
|
+
pid?: number;
|
|
13
15
|
exitCode: number;
|
|
14
16
|
stdout: string;
|
|
15
17
|
stderr: string;
|
|
@@ -23,6 +25,12 @@ export interface RawExec {
|
|
|
23
25
|
}
|
|
24
26
|
export interface Transport {
|
|
25
27
|
readonly name: string;
|
|
28
|
+
/** Cloud only: the machine's full record as the control plane returns it. */
|
|
29
|
+
info?(): Promise<Record<string, unknown>>;
|
|
30
|
+
/** Cloud only: the last `tail` lines of the machine's console log. */
|
|
31
|
+
logs?(tail: number): Promise<string>;
|
|
32
|
+
/** Cloud only: export the stopped machine as a `.smolmachine` in the registry. */
|
|
33
|
+
exportArtifact?(): Promise<Record<string, unknown>>;
|
|
26
34
|
state(): Promise<string>;
|
|
27
35
|
ready(): Promise<boolean>;
|
|
28
36
|
readyAt(): Promise<string | null>;
|
|
@@ -41,8 +49,8 @@ export interface Transport {
|
|
|
41
49
|
setNetworkPolicy(policy: NetworkPolicy): Promise<void>;
|
|
42
50
|
stop(): Promise<void>;
|
|
43
51
|
pause(): Promise<void>;
|
|
44
|
-
resume(): Promise<void>;
|
|
45
|
-
start(interceptor?: EgressInterceptor): Promise<void>;
|
|
52
|
+
resume(waitUntilReady?: boolean): Promise<void>;
|
|
53
|
+
start(interceptor?: EgressInterceptor, waitUntilReady?: boolean): Promise<void>;
|
|
46
54
|
delete(): Promise<void>;
|
|
47
55
|
/** Cloud only: delete and return the settled usage + cost in one call. */
|
|
48
56
|
deleteWithUsage(): Promise<MachineUsageReport>;
|
|
@@ -110,7 +118,17 @@ export declare function toNativeConfig(name: string, config: MachineConfig): Nat
|
|
|
110
118
|
interface CloudConn {
|
|
111
119
|
baseUrl: string;
|
|
112
120
|
apiKey: string;
|
|
121
|
+
/** The caller's `fetch` (ConnectOptions.fetch), when it supplied one. */
|
|
122
|
+
fetch?: typeof fetch;
|
|
123
|
+
readResponseBytes?: (response: Response) => Promise<Uint8Array>;
|
|
113
124
|
}
|
|
125
|
+
/** The largest file the cloud's file routes accept. */
|
|
126
|
+
export declare const CLOUD_MAX_FILE_BYTES: number;
|
|
127
|
+
/** How long moving `bytes` may take (ms): the request timeout plus the time
|
|
128
|
+
* `bytes` needs at the floor rate. */
|
|
129
|
+
export declare function transferTimeoutMs(bytes: number): number;
|
|
130
|
+
/** Refuse a file the cloud would refuse, before sending any of it. */
|
|
131
|
+
export declare function checkCloudFileSize(path: string, bytes: number): void;
|
|
114
132
|
/** The sentence a person should read from a failed response's body. The
|
|
115
133
|
* control plane answers errors as RFC 9457 problem+json, whose `detail` is the
|
|
116
134
|
* sentence; older builds sent `{"error": …}` or plain text. A JSON object
|
package/dist/transport.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Cloud-only/local-only capability gaps surface as `NotSupportedError`.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.CLOUD_MAX_FILE_BYTES = void 0;
|
|
11
12
|
exports.cliConfigApiKey = cliConfigApiKey;
|
|
12
13
|
exports.resolveNetwork = resolveNetwork;
|
|
13
14
|
exports.networkMode = networkMode;
|
|
@@ -15,6 +16,8 @@ exports.cloudNetwork = cloudNetwork;
|
|
|
15
16
|
exports.egressPolicy = egressPolicy;
|
|
16
17
|
exports.resolveBranchable = resolveBranchable;
|
|
17
18
|
exports.toNativeConfig = toNativeConfig;
|
|
19
|
+
exports.transferTimeoutMs = transferTimeoutMs;
|
|
20
|
+
exports.checkCloudFileSize = checkCloudFileSize;
|
|
18
21
|
exports.errorSentence = errorSentence;
|
|
19
22
|
exports.encodePath = encodePath;
|
|
20
23
|
exports.selectsCloud = selectsCloud;
|
|
@@ -526,7 +529,7 @@ class LocalTransport {
|
|
|
526
529
|
}
|
|
527
530
|
liveLocal.delete(this);
|
|
528
531
|
}
|
|
529
|
-
async resume() {
|
|
532
|
+
async resume(waitUntilReady = true) {
|
|
530
533
|
try {
|
|
531
534
|
await this.inner.resume();
|
|
532
535
|
}
|
|
@@ -535,9 +538,10 @@ class LocalTransport {
|
|
|
535
538
|
}
|
|
536
539
|
if (this.cleanupOnExit)
|
|
537
540
|
liveLocal.add(this);
|
|
538
|
-
|
|
541
|
+
if (waitUntilReady)
|
|
542
|
+
await this.waitUntilReady();
|
|
539
543
|
}
|
|
540
|
-
async start(interceptor) {
|
|
544
|
+
async start(interceptor, waitUntilReady = true) {
|
|
541
545
|
try {
|
|
542
546
|
const binding = interceptor ?? this.interceptor;
|
|
543
547
|
if (binding)
|
|
@@ -552,7 +556,8 @@ class LocalTransport {
|
|
|
552
556
|
}
|
|
553
557
|
if (this.cleanupOnExit)
|
|
554
558
|
liveLocal.add(this);
|
|
555
|
-
|
|
559
|
+
if (waitUntilReady)
|
|
560
|
+
await this.waitUntilReady();
|
|
556
561
|
}
|
|
557
562
|
async delete() {
|
|
558
563
|
liveLocal.delete(this);
|
|
@@ -685,9 +690,35 @@ class LocalTransport {
|
|
|
685
690
|
throw new errors_1.NotSupportedError("getLease() is a cloud-only lease feature.");
|
|
686
691
|
}
|
|
687
692
|
}
|
|
693
|
+
/** A cloud connection carrying the caller's transport hooks. */
|
|
694
|
+
function cloudConnOf(conn, baseUrl, apiKey) {
|
|
695
|
+
return {
|
|
696
|
+
baseUrl,
|
|
697
|
+
apiKey,
|
|
698
|
+
...(conn.fetch ? { fetch: conn.fetch } : {}),
|
|
699
|
+
...(conn.readResponseBytes ? { readResponseBytes: conn.readResponseBytes } : {}),
|
|
700
|
+
};
|
|
701
|
+
}
|
|
688
702
|
const DEFAULT_CLOUD_URL = "https://api.smolmachines.com";
|
|
689
703
|
/** Default per-request timeout for cloud calls (ms). Override via opts.timeoutMs. */
|
|
690
704
|
const CLOUD_TIMEOUT_MS = 30_000;
|
|
705
|
+
// A file transfer is given time for the slowest link it should survive: a
|
|
706
|
+
// large file over a slow uplink must not fail at CLOUD_TIMEOUT_MS while it is
|
|
707
|
+
// still moving. Same floor as the Rust and Python SDKs.
|
|
708
|
+
const CLOUD_TRANSFER_FLOOR_BYTES_PER_S = 256 * 1024;
|
|
709
|
+
/** The largest file the cloud's file routes accept. */
|
|
710
|
+
exports.CLOUD_MAX_FILE_BYTES = 100 * 1024 * 1024;
|
|
711
|
+
/** How long moving `bytes` may take (ms): the request timeout plus the time
|
|
712
|
+
* `bytes` needs at the floor rate. */
|
|
713
|
+
function transferTimeoutMs(bytes) {
|
|
714
|
+
return CLOUD_TIMEOUT_MS + Math.ceil(bytes / CLOUD_TRANSFER_FLOOR_BYTES_PER_S) * 1_000;
|
|
715
|
+
}
|
|
716
|
+
/** Refuse a file the cloud would refuse, before sending any of it. */
|
|
717
|
+
function checkCloudFileSize(path, bytes) {
|
|
718
|
+
if (bytes > exports.CLOUD_MAX_FILE_BYTES) {
|
|
719
|
+
throw new errors_1.SmolError("SMOLVM_ERROR", `${path} is ${(bytes / (1024 * 1024)).toFixed(1)} MiB; cloud machines accept files up to ${exports.CLOUD_MAX_FILE_BYTES / (1024 * 1024)} MiB`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
691
722
|
// Starting can include a cold image pull. Keep ordinary API calls short, but
|
|
692
723
|
// give this explicitly long-running operation the same bounded window already
|
|
693
724
|
// used by checkpoint restore.
|
|
@@ -750,7 +781,7 @@ async function cloudFetch(conn, method, path, opts = {}) {
|
|
|
750
781
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
751
782
|
let res;
|
|
752
783
|
try {
|
|
753
|
-
res = await fetch(`${conn.baseUrl}${path}`, {
|
|
784
|
+
res = await (conn.fetch ?? fetch)(`${conn.baseUrl}${path}`, {
|
|
754
785
|
method,
|
|
755
786
|
headers,
|
|
756
787
|
body: body ?? null,
|
|
@@ -763,6 +794,9 @@ async function cloudFetch(conn, method, path, opts = {}) {
|
|
|
763
794
|
if (e.name === "AbortError") {
|
|
764
795
|
throw new errors_1.SmolError("TIMEOUT", `cloud ${method} ${path} timed out after ${opts.timeoutMs ?? CLOUD_TIMEOUT_MS}ms`);
|
|
765
796
|
}
|
|
797
|
+
// A caller's own fetch owns its failure modes (retries, typed errors).
|
|
798
|
+
if (conn.fetch)
|
|
799
|
+
throw e;
|
|
766
800
|
throw new errors_1.SmolError("CONNECTION", `cloud request failed: ${e.message}`);
|
|
767
801
|
}
|
|
768
802
|
finally {
|
|
@@ -781,8 +815,15 @@ async function cloudFetch(conn, method, path, opts = {}) {
|
|
|
781
815
|
? "UNAUTHORIZED"
|
|
782
816
|
: "SMOLVM_ERROR", `cloud ${method} ${path} → ${res.status}${text ? `: ${text}` : ""}${rid ? ` [request id: ${rid}]` : ""}`);
|
|
783
817
|
}
|
|
784
|
-
if (opts.accept === "
|
|
818
|
+
if (opts.accept === "text")
|
|
819
|
+
return (await res.text());
|
|
820
|
+
if (opts.accept === "bytes") {
|
|
821
|
+
if (conn.readResponseBytes) {
|
|
822
|
+
const bytes = await conn.readResponseBytes(res);
|
|
823
|
+
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
824
|
+
}
|
|
785
825
|
return Buffer.from(await res.arrayBuffer());
|
|
826
|
+
}
|
|
786
827
|
if (res.status === 204)
|
|
787
828
|
return undefined;
|
|
788
829
|
const ct = res.headers.get("content-type") ?? "";
|
|
@@ -947,6 +988,7 @@ class CloudTransport {
|
|
|
947
988
|
cwd: opts?.workdir ?? null,
|
|
948
989
|
timeoutSeconds: opts?.timeout ?? null,
|
|
949
990
|
...(opts?.user !== undefined ? { user: opts.user } : {}),
|
|
991
|
+
...(opts?.background ? { background: true } : {}),
|
|
950
992
|
};
|
|
951
993
|
// The command may legitimately run far longer than the default cloud
|
|
952
994
|
// timeout, so size the request abort timeout off the request's own timeout
|
|
@@ -971,7 +1013,10 @@ class CloudTransport {
|
|
|
971
1013
|
// `stdoutB64`/`stderrB64` are byte-exact and untruncated; the generated
|
|
972
1014
|
// schema type may lag the server, so read them off the raw object.
|
|
973
1015
|
const raw = r;
|
|
1016
|
+
// A detached command reports its pid as `pid=<n>` on stdout.
|
|
1017
|
+
const pidMatch = opts?.background ? /^pid=(\d+)/m.exec(stdout) : null;
|
|
974
1018
|
return {
|
|
1019
|
+
...(pidMatch ? { pid: Number(pidMatch[1]) } : {}),
|
|
975
1020
|
exitCode: r.exitCode ?? 0,
|
|
976
1021
|
stdout,
|
|
977
1022
|
stderr,
|
|
@@ -1002,7 +1047,7 @@ class CloudTransport {
|
|
|
1002
1047
|
};
|
|
1003
1048
|
let res;
|
|
1004
1049
|
try {
|
|
1005
|
-
res = await fetch(`${this.conn.baseUrl}/v1/machines/${this.id}/exec/stream`, {
|
|
1050
|
+
res = await (this.conn.fetch ?? fetch)(`${this.conn.baseUrl}/v1/machines/${this.id}/exec/stream`, {
|
|
1006
1051
|
method: "POST",
|
|
1007
1052
|
headers: {
|
|
1008
1053
|
authorization: `Bearer ${this.conn.apiKey}`,
|
|
@@ -1107,11 +1152,14 @@ class CloudTransport {
|
|
|
1107
1152
|
async readFile(path) {
|
|
1108
1153
|
return cloudFetch(this.conn, "GET", `/v1/machines/${this.id}/files/${encodePath(path)}`, {
|
|
1109
1154
|
accept: "bytes",
|
|
1155
|
+
timeoutMs: transferTimeoutMs(exports.CLOUD_MAX_FILE_BYTES),
|
|
1110
1156
|
});
|
|
1111
1157
|
}
|
|
1112
1158
|
async writeFile(path, data, mode) {
|
|
1159
|
+
checkCloudFileSize(path, data.length);
|
|
1113
1160
|
await cloudFetch(this.conn, "PUT", `/v1/machines/${this.id}/files/${encodePath(path)}`, {
|
|
1114
1161
|
body: data,
|
|
1162
|
+
timeoutMs: transferTimeoutMs(data.length),
|
|
1115
1163
|
});
|
|
1116
1164
|
// The cloud /files PUT carries no file mode, so apply it with chmod when
|
|
1117
1165
|
// requested — e.g. writing an executable script the caller then runs.
|
|
@@ -1137,18 +1185,34 @@ class CloudTransport {
|
|
|
1137
1185
|
async pause() {
|
|
1138
1186
|
await cloudFetch(this.conn, "POST", `/v1/machines/${this.id}/pause`, { timeoutMs: CLOUD_START_TIMEOUT_MS });
|
|
1139
1187
|
}
|
|
1140
|
-
async resume() {
|
|
1188
|
+
async resume(waitUntilReady = true) {
|
|
1141
1189
|
await cloudFetch(this.conn, "POST", `/v1/machines/${this.id}/resume`, { timeoutMs: CLOUD_START_TIMEOUT_MS });
|
|
1142
|
-
|
|
1190
|
+
if (waitUntilReady)
|
|
1191
|
+
await waitForReady(this.conn, this.id);
|
|
1143
1192
|
}
|
|
1144
|
-
async start(interceptor) {
|
|
1193
|
+
async start(interceptor, waitUntilReady = true) {
|
|
1145
1194
|
if (interceptor)
|
|
1146
1195
|
throw new errors_1.NotSupportedError("egressInterceptor is local-only.");
|
|
1147
1196
|
// Resume a stopped machine, then wait for its agent so the handle is usable.
|
|
1148
1197
|
await cloudFetch(this.conn, "POST", `/v1/machines/${this.id}/start`, {
|
|
1149
1198
|
timeoutMs: CLOUD_START_TIMEOUT_MS,
|
|
1150
1199
|
});
|
|
1151
|
-
|
|
1200
|
+
if (waitUntilReady)
|
|
1201
|
+
await waitForReady(this.conn, this.id);
|
|
1202
|
+
}
|
|
1203
|
+
async info() {
|
|
1204
|
+
return cloudFetch(this.conn, "GET", `/v1/machines/${this.id}`);
|
|
1205
|
+
}
|
|
1206
|
+
async logs(tail) {
|
|
1207
|
+
return cloudFetch(this.conn, "GET", `/v1/machines/${this.id}/logs?tail=${tail}`, {
|
|
1208
|
+
accept: "text",
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
async exportArtifact() {
|
|
1212
|
+
return cloudFetch(this.conn, "POST", `/v1/machines/${this.id}/export`, {
|
|
1213
|
+
json: {},
|
|
1214
|
+
timeoutMs: CLOUD_START_TIMEOUT_MS,
|
|
1215
|
+
});
|
|
1152
1216
|
}
|
|
1153
1217
|
async delete() {
|
|
1154
1218
|
await cloudFetch(this.conn, "DELETE", `/v1/machines/${this.id}`);
|
|
@@ -1443,7 +1507,7 @@ async function makeTransport(config, conn) {
|
|
|
1443
1507
|
process.env.SMOL_CLOUD_URL ??
|
|
1444
1508
|
cliUrl ??
|
|
1445
1509
|
DEFAULT_CLOUD_URL).replace(/\/+$/, "");
|
|
1446
|
-
const cloudConn =
|
|
1510
|
+
const cloudConn = cloudConnOf(conn, baseUrl, key);
|
|
1447
1511
|
await storeCloudCredentials(cloudConn, config.credentials);
|
|
1448
1512
|
// smolfleet CreateMachineRequest (camelCase): source (tagged), nested
|
|
1449
1513
|
// resources, network {mode}, autoStopSeconds, ttlSeconds. Optional numeric
|
|
@@ -1451,6 +1515,7 @@ async function makeTransport(config, conn) {
|
|
|
1451
1515
|
// (exactOptionalPropertyTypes forbids passing `undefined` explicitly).
|
|
1452
1516
|
const createBody = {
|
|
1453
1517
|
name: config.name ?? null,
|
|
1518
|
+
...(config.labels && Object.keys(config.labels).length ? { labels: config.labels } : {}),
|
|
1454
1519
|
source: { type: "image", reference: config.image },
|
|
1455
1520
|
resources: {
|
|
1456
1521
|
...(config.resources?.cpus !== undefined
|
|
@@ -1487,15 +1552,7 @@ async function makeTransport(config, conn) {
|
|
|
1487
1552
|
...(config.credentials?.length
|
|
1488
1553
|
? { credentials: config.credentials.map((c) => c.name) }
|
|
1489
1554
|
: {}),
|
|
1490
|
-
...(config.cacheDisk
|
|
1491
|
-
? {
|
|
1492
|
-
cacheDisk: {
|
|
1493
|
-
cache: config.cacheDisk.cache,
|
|
1494
|
-
...(config.cacheDisk.version !== undefined ? { version: config.cacheDisk.version } : {}),
|
|
1495
|
-
...(config.cacheDisk.mountPath !== undefined ? { mountPath: config.cacheDisk.mountPath } : {}),
|
|
1496
|
-
},
|
|
1497
|
-
}
|
|
1498
|
-
: {}),
|
|
1555
|
+
...(config.cacheDisk ? { cacheDisk: cacheDiskBody(config.cacheDisk) } : {}),
|
|
1499
1556
|
autoStopSeconds: config.autoStopSeconds ?? null,
|
|
1500
1557
|
ttlSeconds: config.ttlSeconds ?? null,
|
|
1501
1558
|
// Forkable is a CREATE-time property: the control plane persists it and the
|
|
@@ -1624,15 +1681,28 @@ async function listMachines(conn, options = {}) {
|
|
|
1624
1681
|
process.env.SMOL_CLOUD_URL ??
|
|
1625
1682
|
cliUrl ??
|
|
1626
1683
|
DEFAULT_CLOUD_URL).replace(/\/+$/, "");
|
|
1627
|
-
|
|
1628
|
-
|
|
1684
|
+
// The control plane filters by label itself; the client-side filter below
|
|
1685
|
+
// stays as the backstop for an older control plane that ignores it.
|
|
1686
|
+
const labelQuery = Object.entries(options.labels ?? {})
|
|
1687
|
+
.map(([k, v]) => `label=${encodeURIComponent(`${k}=${v}`)}`)
|
|
1688
|
+
.join("&");
|
|
1689
|
+
const listed = await cloudFetch(cloudConnOf(conn, baseUrl, key), "GET", `/v1/machines${labelQuery ? `?${labelQuery}` : ""}`);
|
|
1690
|
+
// Anything but a machine list (a proxy's page, a wrong base URL) is an
|
|
1691
|
+
// error, not an account with no machines.
|
|
1692
|
+
const rows = Array.isArray(listed)
|
|
1693
|
+
? listed
|
|
1694
|
+
: Array.isArray(listed?.machines)
|
|
1695
|
+
? listed.machines
|
|
1696
|
+
: undefined;
|
|
1697
|
+
if (!rows) {
|
|
1698
|
+
throw new errors_1.SmolError("SMOLVM_ERROR", `cloud GET /v1/machines at ${baseUrl} did not return a machine list`);
|
|
1699
|
+
}
|
|
1629
1700
|
all = rows.map((m) => ({
|
|
1630
1701
|
name: m.name ?? m.id,
|
|
1631
1702
|
id: m.id,
|
|
1632
1703
|
state: m.state,
|
|
1633
1704
|
...(m.source?.type === "image" && { image: m.source.reference }),
|
|
1634
|
-
|
|
1635
|
-
labels: {},
|
|
1705
|
+
labels: m.labels ?? {},
|
|
1636
1706
|
persistent: !m.ephemeral,
|
|
1637
1707
|
// Remote machines never depend on a client process.
|
|
1638
1708
|
detached: true,
|
|
@@ -1694,7 +1764,7 @@ async function connectTransport(id, conn) {
|
|
|
1694
1764
|
process.env.SMOL_CLOUD_URL ??
|
|
1695
1765
|
cliUrl ??
|
|
1696
1766
|
DEFAULT_CLOUD_URL).replace(/\/+$/, "");
|
|
1697
|
-
const cloudConn =
|
|
1767
|
+
const cloudConn = cloudConnOf(conn, baseUrl, key);
|
|
1698
1768
|
// Resolve like the CLI does: try the id path first, and when that 404s,
|
|
1699
1769
|
// list machines and match by NAME. `machine.name` returns the human name,
|
|
1700
1770
|
// so `Machine.connect(other.name)` — the natural composition of this API —
|
|
@@ -1743,6 +1813,9 @@ async function restoreCheckpointTransport(checkpointId, name, conn, options) {
|
|
|
1743
1813
|
}
|
|
1744
1814
|
conn ??= { target: looksLikeLocalCheckpoint(checkpointId) ? "local" : "cloud" };
|
|
1745
1815
|
if (!selectsCloud(conn)) {
|
|
1816
|
+
if (options?.cacheDisk) {
|
|
1817
|
+
throw new errors_1.InvalidConfigError("cacheDisk is cloud-only: a local restore has no cache slot.");
|
|
1818
|
+
}
|
|
1746
1819
|
const path = (0, node_path_1.resolve)(checkpointId);
|
|
1747
1820
|
let restored;
|
|
1748
1821
|
try {
|
|
@@ -1775,6 +1848,7 @@ async function restoreCheckpointTransport(checkpointId, name, conn, options) {
|
|
|
1775
1848
|
...(options?.networkPolicy !== undefined
|
|
1776
1849
|
? { network: cloudNetwork(options.networkPolicy) }
|
|
1777
1850
|
: {}),
|
|
1851
|
+
...(options?.cacheDisk ? { cacheDisk: cacheDiskBody(options.cacheDisk) } : {}),
|
|
1778
1852
|
},
|
|
1779
1853
|
});
|
|
1780
1854
|
const id = created.id;
|
|
@@ -1790,6 +1864,14 @@ async function restoreCheckpointTransport(checkpointId, name, conn, options) {
|
|
|
1790
1864
|
}
|
|
1791
1865
|
return new CloudTransport(cloudConn, created.name ?? name, id);
|
|
1792
1866
|
}
|
|
1867
|
+
function cacheDiskBody(ref) {
|
|
1868
|
+
return {
|
|
1869
|
+
cache: ref.cache,
|
|
1870
|
+
...(ref.version !== undefined ? { version: ref.version } : {}),
|
|
1871
|
+
...(ref.mountPath !== undefined ? { mountPath: ref.mountPath } : {}),
|
|
1872
|
+
...(ref.slot ? { slot: true } : {}),
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1793
1875
|
/** The control plane and key a cloud operation uses: explicit options, then
|
|
1794
1876
|
* the environment, then the CLI session. */
|
|
1795
1877
|
function cloudConnFor(conn, operation) {
|
|
@@ -1803,7 +1885,7 @@ function cloudConnFor(conn, operation) {
|
|
|
1803
1885
|
process.env.SMOL_CLOUD_URL ??
|
|
1804
1886
|
cliUrl ??
|
|
1805
1887
|
DEFAULT_CLOUD_URL).replace(/\/+$/, "");
|
|
1806
|
-
return
|
|
1888
|
+
return cloudConnOf(conn, baseUrl, key);
|
|
1807
1889
|
}
|
|
1808
1890
|
/** Create a cache disk; its version 0 is an empty filesystem. Cloud only. */
|
|
1809
1891
|
function createCacheDisk(options, conn = {}) {
|
package/dist/types.d.ts
CHANGED
|
@@ -324,6 +324,12 @@ export interface RestoreCheckpointOptions {
|
|
|
324
324
|
* an earlier save point; do not run two machines from one checkpoint with
|
|
325
325
|
* it. Local target only. */
|
|
326
326
|
keepIdentity?: boolean;
|
|
327
|
+
/** Mount this cache in the slot the checkpoint's machine was created with
|
|
328
|
+
* (`cacheDisk: { cache, slot: true }`), in place of the cache it was
|
|
329
|
+
* captured with. It must be no larger than the slot and mount at the
|
|
330
|
+
* slot's path, so one base checkpoint resumes with each project's own
|
|
331
|
+
* cache. Cloud only. */
|
|
332
|
+
cacheDisk?: CacheDiskRef;
|
|
327
333
|
}
|
|
328
334
|
/** Per-call execution options. */
|
|
329
335
|
export interface ExecOptions {
|
|
@@ -354,6 +360,9 @@ export interface ExecOptions {
|
|
|
354
360
|
* Ignored (both families, as before) on older control planes and on the
|
|
355
361
|
* local target. */
|
|
356
362
|
output?: "text" | "b64" | "both";
|
|
363
|
+
/** Cloud only: start the command detached and return at once with its
|
|
364
|
+
* `pid`, leaving it running (a dev server, an agent) after the call ends. */
|
|
365
|
+
background?: boolean;
|
|
357
366
|
}
|
|
358
367
|
/** Result of a command execution. */
|
|
359
368
|
export interface ExecResult {
|
|
@@ -377,6 +386,8 @@ export interface ExecResult {
|
|
|
377
386
|
* the lossy `stdout`); on the local target it is the UTF-8 encoding of `stdout`.
|
|
378
387
|
* Prefer this over `stdout` for binary or >1 MiB output. */
|
|
379
388
|
stdoutBytes: Uint8Array;
|
|
389
|
+
/** The detached process's pid, for an exec run with `background: true`. */
|
|
390
|
+
pid?: number;
|
|
380
391
|
/** Byte-exact, untruncated stderr; see `stdoutBytes`. */
|
|
381
392
|
stderrBytes: Uint8Array;
|
|
382
393
|
/** True when exitCode === 0. */
|
|
@@ -509,6 +520,14 @@ export interface ConnectOptions {
|
|
|
509
520
|
baseUrl?: string;
|
|
510
521
|
/** Cloud API key, `smk_…` (cloud target only). */
|
|
511
522
|
apiKey?: string;
|
|
523
|
+
/** Cloud only: the `fetch` every cloud request goes through, for a proxy,
|
|
524
|
+
* retries, tracing, or a runtime without a global `fetch`. Its own errors
|
|
525
|
+
* reach the caller unchanged; timeouts and aborts are still reported as
|
|
526
|
+
* `TIMEOUT` and the abort reason. Default: the global `fetch`. */
|
|
527
|
+
fetch?: typeof fetch;
|
|
528
|
+
/** Cloud only: read a binary response body, for a `fetch` whose responses
|
|
529
|
+
* lack a working `arrayBuffer()`. */
|
|
530
|
+
readResponseBytes?: (response: Response) => Promise<Uint8Array>;
|
|
512
531
|
}
|
|
513
532
|
/** Per-launch trusted host service. Keep the token out of logs and persistent config. */
|
|
514
533
|
export interface EgressInterceptor {
|
|
@@ -519,6 +538,15 @@ export interface EgressInterceptor {
|
|
|
519
538
|
}
|
|
520
539
|
export interface StartOptions {
|
|
521
540
|
egressInterceptor?: EgressInterceptor;
|
|
541
|
+
/** Wait for the guest to be ready before returning (default true). Pass
|
|
542
|
+
* false to return once the start is accepted, e.g. when starting many
|
|
543
|
+
* machines and polling `ready()` yourself. */
|
|
544
|
+
waitUntilReady?: boolean;
|
|
545
|
+
}
|
|
546
|
+
/** Options for `Machine.resume`. */
|
|
547
|
+
export interface ResumeOptions {
|
|
548
|
+
/** Wait for the guest to be ready before returning (default true). */
|
|
549
|
+
waitUntilReady?: boolean;
|
|
522
550
|
}
|
|
523
551
|
/** Which cache disk a machine starts from. */
|
|
524
552
|
export interface CacheDiskRef {
|
|
@@ -528,6 +556,10 @@ export interface CacheDiskRef {
|
|
|
528
556
|
version?: number;
|
|
529
557
|
/** Absolute guest path to mount it at; the cache disk's own when omitted. */
|
|
530
558
|
mountPath?: string;
|
|
559
|
+
/** On create, attach the cache unmounted as a slot: restoring a checkpoint
|
|
560
|
+
* of the machine with `restoreCheckpoint(..., { cacheDisk })` mounts any
|
|
561
|
+
* cache no larger than this one there, at this one's mount path. */
|
|
562
|
+
slot?: boolean;
|
|
531
563
|
}
|
|
532
564
|
/** Options for creating a cache disk. */
|
|
533
565
|
export interface CreateCacheDiskOptions {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smolmachines",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.1",
|
|
4
4
|
"description": "Embed isolated microVM sandboxes directly in your code — no server to run.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "smol-machines",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"typescript": "^5.6.0"
|
|
64
64
|
},
|
|
65
65
|
"optionalDependencies": {
|
|
66
|
-
"smolmachines-darwin-arm64": "1.
|
|
67
|
-
"smolmachines-linux-x64-gnu": "1.
|
|
68
|
-
"smolmachines-linux-arm64-gnu": "1.
|
|
66
|
+
"smolmachines-darwin-arm64": "1.26.1",
|
|
67
|
+
"smolmachines-linux-x64-gnu": "1.26.1",
|
|
68
|
+
"smolmachines-linux-arm64-gnu": "1.26.1"
|
|
69
69
|
}
|
|
70
70
|
}
|