endia 0.0.0-stage → 0.0.2

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 (101) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +34 -2
  3. package/dist/archives/archive-file.d.ts +36 -0
  4. package/dist/archives/archive-file.js +71 -0
  5. package/dist/archives/build-context.d.ts +63 -0
  6. package/dist/archives/build-context.js +132 -0
  7. package/dist/archives/docker-ignore.d.ts +57 -0
  8. package/dist/archives/docker-ignore.js +250 -0
  9. package/dist/archives/fleet-archive.d.ts +33 -0
  10. package/dist/archives/fleet-archive.js +134 -0
  11. package/dist/compose-model.d.ts +32 -0
  12. package/dist/compose-model.js +39 -0
  13. package/dist/contract/archives.d.ts +14 -0
  14. package/dist/contract/archives.js +1 -0
  15. package/dist/contract/bases.d.ts +48 -0
  16. package/dist/contract/bases.js +1 -0
  17. package/dist/contract/clients.d.ts +29 -0
  18. package/dist/contract/clients.js +1 -0
  19. package/dist/contract/common.d.ts +30 -0
  20. package/dist/contract/common.js +1 -0
  21. package/dist/contract/compose.d.ts +27 -0
  22. package/dist/contract/compose.js +1 -0
  23. package/dist/contract/desktop.d.ts +269 -0
  24. package/dist/contract/desktop.js +1 -0
  25. package/dist/contract/execs.d.ts +61 -0
  26. package/dist/contract/execs.js +1 -0
  27. package/dist/contract/fleets.d.ts +136 -0
  28. package/dist/contract/fleets.js +1 -0
  29. package/dist/contract/images.d.ts +130 -0
  30. package/dist/contract/images.js +1 -0
  31. package/dist/contract/machines.d.ts +199 -0
  32. package/dist/contract/machines.js +1 -0
  33. package/dist/contract/operations.d.ts +28 -0
  34. package/dist/contract/operations.js +1 -0
  35. package/dist/contract/processes.d.ts +75 -0
  36. package/dist/contract/processes.js +1 -0
  37. package/dist/contract/purposes.d.ts +62 -0
  38. package/dist/contract/purposes.js +1 -0
  39. package/dist/contract/space.d.ts +114 -0
  40. package/dist/contract/space.js +1 -0
  41. package/dist/contract/stats.d.ts +58 -0
  42. package/dist/contract/stats.js +1 -0
  43. package/dist/contract/status.d.ts +151 -0
  44. package/dist/contract/status.js +1 -0
  45. package/dist/contract/volumes.d.ts +22 -0
  46. package/dist/contract/volumes.js +1 -0
  47. package/dist/eastindia-client.d.ts +118 -0
  48. package/dist/eastindia-client.js +142 -0
  49. package/dist/environment.d.ts +52 -0
  50. package/dist/environment.js +133 -0
  51. package/dist/errors.d.ts +81 -0
  52. package/dist/errors.js +108 -0
  53. package/dist/exec/exec-frame.d.ts +38 -0
  54. package/dist/exec/exec-frame.js +50 -0
  55. package/dist/exec/exec-session.d.ts +64 -0
  56. package/dist/exec/exec-session.js +117 -0
  57. package/dist/groups/archives.d.ts +78 -0
  58. package/dist/groups/archives.js +117 -0
  59. package/dist/groups/bases.d.ts +28 -0
  60. package/dist/groups/bases.js +39 -0
  61. package/dist/groups/certificates.d.ts +32 -0
  62. package/dist/groups/certificates.js +43 -0
  63. package/dist/groups/desktop.d.ts +143 -0
  64. package/dist/groups/desktop.js +182 -0
  65. package/dist/groups/execs.d.ts +110 -0
  66. package/dist/groups/execs.js +119 -0
  67. package/dist/groups/fleets.d.ts +206 -0
  68. package/dist/groups/fleets.js +255 -0
  69. package/dist/groups/images.d.ts +130 -0
  70. package/dist/groups/images.js +109 -0
  71. package/dist/groups/machines.d.ts +124 -0
  72. package/dist/groups/machines.js +127 -0
  73. package/dist/groups/operations.d.ts +127 -0
  74. package/dist/groups/operations.js +174 -0
  75. package/dist/groups/processes.d.ts +77 -0
  76. package/dist/groups/processes.js +75 -0
  77. package/dist/groups/stats.d.ts +29 -0
  78. package/dist/groups/stats.js +33 -0
  79. package/dist/groups/system.d.ts +33 -0
  80. package/dist/groups/system.js +40 -0
  81. package/dist/groups/volumes.d.ts +36 -0
  82. package/dist/groups/volumes.js +40 -0
  83. package/dist/index.d.ts +37 -0
  84. package/dist/index.js +20 -0
  85. package/dist/purposes.d.ts +17 -0
  86. package/dist/purposes.js +21 -0
  87. package/dist/routes.d.ts +71 -0
  88. package/dist/routes.js +73 -0
  89. package/dist/transport/api-connection.d.ts +185 -0
  90. package/dist/transport/api-connection.js +396 -0
  91. package/dist/transport/connection-failures.d.ts +14 -0
  92. package/dist/transport/connection-failures.js +57 -0
  93. package/dist/transport/json-lines.d.ts +10 -0
  94. package/dist/transport/json-lines.js +23 -0
  95. package/dist/transport/query-text.d.ts +49 -0
  96. package/dist/transport/query-text.js +62 -0
  97. package/dist/transport/service-address.d.ts +30 -0
  98. package/dist/transport/service-address.js +37 -0
  99. package/dist/transport/tls-files.d.ts +9 -0
  100. package/dist/transport/tls-files.js +1 -0
  101. package/package.json +44 -5
@@ -0,0 +1,119 @@
1
+ import { ExecSession } from "../exec/exec-session.js";
2
+ import { Routes } from "../routes.js";
3
+ import { jsonBody } from "../transport/api-connection.js";
4
+ import { QueryText } from "../transport/query-text.js";
5
+ /**
6
+ * The exec routes, which run a program in a running machine as `docker exec` does: create the exec, then start it attached over a
7
+ * WebSocket or detached. `run`, `spawn` and `start` do both steps; `create`, `attach`, `startDetached`, `inspect` and `resize` are the
8
+ * single routes.
9
+ */
10
+ export class ExecsClient {
11
+ connection;
12
+ /** @internal */
13
+ constructor(connection) {
14
+ this.connection = connection;
15
+ }
16
+ /**
17
+ * Runs a program to its end and collects its output: `endia exec`.
18
+ *
19
+ * @param machine - The machine name or a unique prefix of its ID.
20
+ * @param request - The program and its arguments in `cmd`, and how to run it.
21
+ * @param options - `input` is sent to the program's standard input.
22
+ * @returns The exit code in full and the output.
23
+ */
24
+ async run(machine, request, options = {}) {
25
+ const session = await this.spawn(machine, { ...request, attachStdin: options.input !== undefined }, options.signal);
26
+ session.stdin?.end(options.input);
27
+ const [stdout, stderr] = await Promise.all([collect(session.stdout), collect(session.stderr)]);
28
+ const exec = await session.wait(options.signal);
29
+ return { exitCode: exec.exitCode, startError: exec.startError, stdout, stderr, exec };
30
+ }
31
+ /**
32
+ * Starts a program with the caller attached to its streams: `endia exec` without collecting.
33
+ *
34
+ * @param machine - The machine name or a unique prefix of its ID.
35
+ * @param request - The program and its arguments in `cmd`, and how to run it. With `attachStdin`, the session's `stdin` is open.
36
+ * @param signal - Cancels the requests; the program keeps running once it has started.
37
+ * @returns The running program's streams; read both output streams.
38
+ */
39
+ async spawn(machine, request, signal) {
40
+ const created = await this.create(machine, request, signal);
41
+ return this.attach(created.id, { tty: request.tty ?? false, attachStdin: request.attachStdin ?? false }, signal);
42
+ }
43
+ /**
44
+ * Starts a program and returns at once: `endia exec -d`. The program's output is kept in the machine for
45
+ * `ProcessesClient.logs`.
46
+ *
47
+ * @param machine - The machine name or a unique prefix of its ID.
48
+ * @param request - The program and its arguments in `cmd`, and how to run it.
49
+ * @param signal - Cancels the requests.
50
+ * @returns The exec ID and the process the program runs in, which the process routes take.
51
+ */
52
+ async start(machine, request, signal) {
53
+ const created = await this.create(machine, request, signal);
54
+ return { id: created.id, ...await this.startDetached(created.id, signal) };
55
+ }
56
+ /**
57
+ * Creates an exec without starting it: `POST /v1/machines/{name}/exec`.
58
+ *
59
+ * @param machine - The machine name or a unique prefix of its ID.
60
+ * @param request - The program and its arguments in `cmd`, and how to run it.
61
+ * @param signal - Cancels the request.
62
+ * @returns The new exec's ID.
63
+ */
64
+ create(machine, request, signal) {
65
+ return this.connection.change({ method: "POST", path: Routes.machineExec(machine), body: jsonBody(request), signal });
66
+ }
67
+ /**
68
+ * Starts a created exec with the caller attached, over a WebSocket: `GET /v1/execs/{id}/start`.
69
+ *
70
+ * @param id - The exec ID.
71
+ * @param shape - Whether the exec was created with `tty` and with `attachStdin`, which decides how its messages are read and written.
72
+ * @param signal - Cancels the opening.
73
+ * @returns The running program's streams.
74
+ */
75
+ async attach(id, shape, signal) {
76
+ const socket = await this.connection.openWebSocket(Routes.execStart(id), signal);
77
+ return new ExecSession(id, socket, shape, this);
78
+ }
79
+ /**
80
+ * Starts a created exec without the caller attached: `POST /v1/execs/{id}/start` with `{ "detach": true }`.
81
+ *
82
+ * @param id - The exec ID.
83
+ * @param signal - Cancels the request.
84
+ * @returns The process the program runs in.
85
+ */
86
+ startDetached(id, signal) {
87
+ return this.connection.change({ method: "POST", path: Routes.execStart(id), body: jsonBody({ detach: true }), signal });
88
+ }
89
+ /**
90
+ * Reads an exec: what was asked for, its process, and its exit code once it has ended.
91
+ *
92
+ * @param id - The exec ID.
93
+ * @param signal - Cancels the request.
94
+ * @returns The exec.
95
+ */
96
+ inspect(id, signal) {
97
+ return this.connection.read(Routes.exec(id), signal);
98
+ }
99
+ /**
100
+ * Changes the size of an exec's console.
101
+ *
102
+ * @param id - The exec ID.
103
+ * @param rows - The number of rows.
104
+ * @param columns - The number of columns.
105
+ * @param signal - Cancels the request.
106
+ * @returns When the service has resized the console.
107
+ */
108
+ resize(id, rows, columns, signal) {
109
+ const query = new QueryText().add("h", rows).add("w", columns);
110
+ return this.connection.changeWithoutAnswer({ method: "POST", path: Routes.execResize(id) + query.toString(), signal });
111
+ }
112
+ }
113
+ async function collect(stream) {
114
+ const chunks = [];
115
+ for await (const chunk of stream) {
116
+ chunks.push(chunk);
117
+ }
118
+ return Buffer.concat(chunks).toString("utf8");
119
+ }
@@ -0,0 +1,206 @@
1
+ import type { Readable } from "node:stream";
2
+ import type { ComposeModel } from "../contract/compose.js";
3
+ import type { FleetDetail, FleetDownResult, FleetDryRun, FleetMachinesResult, FleetSummary, FleetUpResult } from "../contract/fleets.js";
4
+ import type { MachineLogLine, MachineSummary } from "../contract/machines.js";
5
+ import type { FleetPurpose, FleetPurposeResult } from "../contract/purposes.js";
6
+ import { type LogOptions } from "./machines.js";
7
+ import type { OperationHandle } from "./operations.js";
8
+ /** How `compose up` brings the fleet to the state its model describes, as the options of `endia compose up` choose it. */
9
+ export interface FleetUpOptions {
10
+ /** The services to bring up, with the services they depend on; every service when undefined or empty. */
11
+ readonly services?: readonly string[];
12
+ /** The number of machines of some services, whatever the model says, each written `<service>=<n>`. */
13
+ readonly scale?: readonly string[];
14
+ /** When images are built: `always`, `never`, or `missing`, the default, which builds a service's image only when it does not exist. */
15
+ readonly build?: "always" | "never" | "missing";
16
+ /** When existing machines are recreated: `force`, `never`, or `changed`, the default, which recreates a machine whose service changed. */
17
+ readonly recreate?: "force" | "never" | "changed";
18
+ /** Whether machines of services no longer in the model are removed, rather than named in a warning. */
19
+ readonly removeOrphans?: boolean;
20
+ /** The time in seconds Windows has to shut down in machines being recreated or removed. */
21
+ readonly timeout?: number;
22
+ /** The Compose files the model came from, which the fleet's record keeps and `compose ls` shows. */
23
+ readonly configFiles?: readonly string[];
24
+ /** The folder against which the model's relative paths were resolved, which the fleet's record keeps. */
25
+ readonly projectDirectory?: string;
26
+ /** The fleet's purpose, what it is being used for, in one line of 1 to 200 characters: `--purpose`. The service stores it, replacing any purpose the fleet has, before any other work; undefined leaves the fleet's purpose as it is. It cannot be given with a `FleetArchive`, which carries its own `purpose.json` or none. */
27
+ readonly purpose?: string;
28
+ /** Markdown notes for that purpose, at most 65536 bytes in UTF-8: the text of the file `--purpose-file` names. It needs `purpose`. */
29
+ readonly purposeDetail?: string;
30
+ readonly signal?: AbortSignal;
31
+ }
32
+ /** The options of `wait`, as the options of `endia compose up --wait` choose them. */
33
+ export interface FleetWaitOptions {
34
+ /** The services whose machines to wait for, with the services they depend on; every service when undefined or empty. */
35
+ readonly services?: readonly string[];
36
+ /** The longest wait in seconds, as `--wait-timeout`; no limit when undefined. */
37
+ readonly timeout?: number;
38
+ readonly signal?: AbortSignal;
39
+ }
40
+ /** The fleet's archive as the caller packed it, holding `compose.json` and every build context and bind mount source it needs. */
41
+ export interface FleetArchive {
42
+ /** The fleet's name, which is the Compose project name. */
43
+ readonly name: string;
44
+ /** The archive: its bytes, or a function that opens it afresh, so that a retried request sends the same bytes. */
45
+ readonly archive: Uint8Array | (() => Readable);
46
+ }
47
+ /** The options of `compose stop` and `compose restart`. */
48
+ export interface FleetServicesOptions {
49
+ /** The services whose machines to act on; every service when undefined or empty. */
50
+ readonly services?: readonly string[];
51
+ /** The time in seconds Windows has to shut down before the virtual machine is turned off. */
52
+ readonly timeout?: number;
53
+ readonly signal?: AbortSignal;
54
+ }
55
+ /**
56
+ * The fleet routes, which bring a Compose project up and down as `docker compose` does, and read, set and clear a fleet's purpose. The client does not read Compose files: `up`
57
+ * takes the resolved model, as `endia compose config --format json` prints it, and packs it with the build contexts and bind mount
58
+ * sources it names into the archive the service expects. The service changes a fleet in the background, so every method that changes
59
+ * one returns a handle that follows that work and gives its result.
60
+ */
61
+ export declare class FleetsClient {
62
+ private readonly connection;
63
+ private readonly operations;
64
+ /**
65
+ * Brings a fleet to the state its resolved model describes: `endia compose up -d`. The fleet's name is the model's `name`.
66
+ *
67
+ * The operation succeeds once every machine it created, recreated or started is ready: its virtual machine runs and it was
68
+ * configured in this boot, and when its image signs a user in by itself, that user is signed in with the desktop helper ready.
69
+ * A machine whose user is not signed in within `desktopReadyTimeoutSeconds` fails the operation with `TIMEOUT`. A machine that
70
+ * was already running and is kept unchanged is not checked again. Health checks are not waited for; `wait` waits for them, and
71
+ * for kept machines, as `endia compose up --wait` does.
72
+ *
73
+ * @param model - The resolved model, as an object or as the JSON text `endia compose config --format json` prints; or an archive
74
+ * the caller packed.
75
+ * @param options - Which services, when to build and recreate, and the fleet's purpose.
76
+ * @returns The operation; its result says what was done to each machine, network and volume.
77
+ * @throws Error when `purposeDetail` is given without `purpose`, when `purpose` has a line break or `purposeDetail` is too long, or
78
+ * when a purpose is given with a `FleetArchive`. Nothing is sent then.
79
+ */
80
+ up(model: ComposeModel | string | FleetArchive, options?: FleetUpOptions): Promise<OperationHandle<FleetUpResult>>;
81
+ /**
82
+ * Works out what `up` would do and changes nothing: `endia compose up --dry-run`.
83
+ *
84
+ * @param model - As for `up`.
85
+ * @param options - As for `up`. A purpose is checked and sent as `up` sends it.
86
+ * @returns The planned actions in order, and whether the host has the memory they need.
87
+ */
88
+ dryRun(model: ComposeModel | string | FleetArchive, options?: FleetUpOptions): Promise<FleetDryRun>;
89
+ /**
90
+ * Removes a fleet's machines and networks: `endia compose down`.
91
+ *
92
+ * @param name - The fleet's name.
93
+ * @param options - `volumes` deletes the named volumes too; `removeOrphans` removes machines of services no longer in the model;
94
+ * `timeout` as for `stop`.
95
+ * @returns The operation; its result names what was removed.
96
+ */
97
+ down(name: string, options?: {
98
+ readonly volumes?: boolean;
99
+ readonly removeOrphans?: boolean;
100
+ readonly timeout?: number;
101
+ readonly signal?: AbortSignal;
102
+ }): Promise<OperationHandle<FleetDownResult>>;
103
+ /**
104
+ * Shuts a fleet's machines down: `endia compose stop`.
105
+ *
106
+ * @param name - The fleet's name.
107
+ * @param options - The services, and the shutdown time.
108
+ * @returns The operation; its result names the machines stopped.
109
+ */
110
+ stop(name: string, options?: FleetServicesOptions): Promise<OperationHandle<FleetMachinesResult>>;
111
+ /**
112
+ * Starts a fleet's stopped machines: `endia compose start`.
113
+ *
114
+ * @param name - The fleet's name.
115
+ * @param options - The services.
116
+ * @returns The operation; its result names the machines started.
117
+ */
118
+ start(name: string, options?: {
119
+ readonly services?: readonly string[];
120
+ readonly signal?: AbortSignal;
121
+ }): Promise<OperationHandle<FleetMachinesResult>>;
122
+ /**
123
+ * Restarts a fleet's machines: `endia compose restart`.
124
+ *
125
+ * @param name - The fleet's name.
126
+ * @param options - The services, and the shutdown time.
127
+ * @returns The operation; its result names the machines restarted.
128
+ */
129
+ restart(name: string, options?: FleetServicesOptions): Promise<OperationHandle<FleetMachinesResult>>;
130
+ /**
131
+ * Lists fleets: `endia compose ls`.
132
+ *
133
+ * @param options - `all` includes fleets with no running machine.
134
+ * @returns The fleets.
135
+ */
136
+ list(options?: {
137
+ readonly all?: boolean;
138
+ readonly signal?: AbortSignal;
139
+ }): Promise<FleetSummary[]>;
140
+ /**
141
+ * Reads one fleet with its machines: `endia compose ps`.
142
+ *
143
+ * @param name - The fleet's name.
144
+ * @param signal - Cancels the request.
145
+ * @returns The fleet.
146
+ */
147
+ inspect(name: string, signal?: AbortSignal): Promise<FleetDetail>;
148
+ /**
149
+ * Reads a fleet's purpose with every change to it while the fleet has existed: `endia purpose show`.
150
+ *
151
+ * @param name - The fleet's name.
152
+ * @param signal - Cancels the request.
153
+ * @returns The purpose, or null, and the history, oldest first.
154
+ */
155
+ purpose(name: string, signal?: AbortSignal): Promise<FleetPurpose>;
156
+ /**
157
+ * Replaces a fleet's whole purpose, which its machines without a purpose of their own have: `endia purpose set`.
158
+ *
159
+ * @param name - The fleet's name.
160
+ * @param title - What the fleet is being used for, in one line of 1 to 200 characters.
161
+ * @param options - `detail` is Markdown notes of at most 65536 bytes in UTF-8; without it the detail stored before is cleared.
162
+ * @returns The purpose now stored.
163
+ * @throws Error when the title has a line break or the detail is too long. Nothing is sent then.
164
+ */
165
+ setPurpose(name: string, title: string, options?: {
166
+ readonly detail?: string;
167
+ readonly signal?: AbortSignal;
168
+ }): Promise<FleetPurposeResult>;
169
+ /**
170
+ * Removes a fleet's purpose: `endia purpose clear`. Clearing a fleet without a purpose changes nothing.
171
+ *
172
+ * @param name - The fleet's name.
173
+ * @param signal - Cancels the request.
174
+ * @returns The purpose now stored, which is null.
175
+ */
176
+ clearPurpose(name: string, signal?: AbortSignal): Promise<FleetPurposeResult>;
177
+ /**
178
+ * Waits until a fleet's machines are running and ready, and healthy when they have a health check, as `endia compose up --wait`
179
+ * waits once its operation has succeeded. `up`'s result already comes only once every machine it created, recreated or started
180
+ * was made ready; this wait adds what `--wait` adds: it checks every selected machine, kept ones too, waits for health checks to
181
+ * pass, and fails when a machine exits or becomes unhealthy.
182
+ *
183
+ * @param name - The fleet's name.
184
+ * @param options - Which services, as `up` takes them, and the longest wait.
185
+ * @returns The machines waited for, as last read.
186
+ * @throws MachineFailedError when a machine exited or became unhealthy.
187
+ * @throws DOMException named `TimeoutError` when some machine was not yet running, ready and healthy when the time-out passed. The
188
+ * machines carry on.
189
+ * @throws Error when a named service is not in the fleet's model.
190
+ */
191
+ wait(name: string, options?: FleetWaitOptions): Promise<MachineSummary[]>;
192
+ /**
193
+ * Reads the main-process output of a fleet's machines, each line naming its machine: `endia compose logs`.
194
+ *
195
+ * @param name - The fleet's name.
196
+ * @param options - Which lines; `services` limits them to some services, and `index` to one machine of each.
197
+ * @returns The lines, as they arrive.
198
+ */
199
+ logs(name: string, options?: LogOptions & {
200
+ readonly services?: readonly string[];
201
+ readonly index?: number;
202
+ }): AsyncGenerator<MachineLogLine, void, undefined>;
203
+ private sendFleet;
204
+ private sendArchive;
205
+ private sendAction;
206
+ }
@@ -0,0 +1,255 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { packFleet, selectServices } from "../archives/fleet-archive.js";
3
+ import { MachineFailedError } from "../errors.js";
4
+ import { purposeBody } from "../purposes.js";
5
+ import { Routes } from "../routes.js";
6
+ import { jsonBody, tarBody } from "../transport/api-connection.js";
7
+ import { QueryText } from "../transport/query-text.js";
8
+ import { addLogOptions } from "./machines.js";
9
+ /** How often `wait` reads the fleet, as `endia compose up --wait` does. */
10
+ const WaitPollMilliseconds = 1000;
11
+ /**
12
+ * The fleet routes, which bring a Compose project up and down as `docker compose` does, and read, set and clear a fleet's purpose. The client does not read Compose files: `up`
13
+ * takes the resolved model, as `endia compose config --format json` prints it, and packs it with the build contexts and bind mount
14
+ * sources it names into the archive the service expects. The service changes a fleet in the background, so every method that changes
15
+ * one returns a handle that follows that work and gives its result.
16
+ */
17
+ export class FleetsClient {
18
+ connection;
19
+ operations;
20
+ /** @internal */
21
+ constructor(connection, operations) {
22
+ this.connection = connection;
23
+ this.operations = operations;
24
+ }
25
+ /**
26
+ * Brings a fleet to the state its resolved model describes: `endia compose up -d`. The fleet's name is the model's `name`.
27
+ *
28
+ * The operation succeeds once every machine it created, recreated or started is ready: its virtual machine runs and it was
29
+ * configured in this boot, and when its image signs a user in by itself, that user is signed in with the desktop helper ready.
30
+ * A machine whose user is not signed in within `desktopReadyTimeoutSeconds` fails the operation with `TIMEOUT`. A machine that
31
+ * was already running and is kept unchanged is not checked again. Health checks are not waited for; `wait` waits for them, and
32
+ * for kept machines, as `endia compose up --wait` does.
33
+ *
34
+ * @param model - The resolved model, as an object or as the JSON text `endia compose config --format json` prints; or an archive
35
+ * the caller packed.
36
+ * @param options - Which services, when to build and recreate, and the fleet's purpose.
37
+ * @returns The operation; its result says what was done to each machine, network and volume.
38
+ * @throws Error when `purposeDetail` is given without `purpose`, when `purpose` has a line break or `purposeDetail` is too long, or
39
+ * when a purpose is given with a `FleetArchive`. Nothing is sent then.
40
+ */
41
+ async up(model, options = {}) {
42
+ const accepted = await this.sendFleet(model, options, false);
43
+ return this.operations.handle(accepted);
44
+ }
45
+ /**
46
+ * Works out what `up` would do and changes nothing: `endia compose up --dry-run`.
47
+ *
48
+ * @param model - As for `up`.
49
+ * @param options - As for `up`. A purpose is checked and sent as `up` sends it.
50
+ * @returns The planned actions in order, and whether the host has the memory they need.
51
+ */
52
+ dryRun(model, options = {}) {
53
+ return this.sendFleet(model, options, true);
54
+ }
55
+ /**
56
+ * Removes a fleet's machines and networks: `endia compose down`.
57
+ *
58
+ * @param name - The fleet's name.
59
+ * @param options - `volumes` deletes the named volumes too; `removeOrphans` removes machines of services no longer in the model;
60
+ * `timeout` as for `stop`.
61
+ * @returns The operation; its result names what was removed.
62
+ */
63
+ async down(name, options = {}) {
64
+ const query = new QueryText().addFlag("volumes", options.volumes).addFlag("removeOrphans", options.removeOrphans).add("timeout", options.timeout);
65
+ const accepted = await this.connection.change({ method: "DELETE", path: Routes.fleet(name) + query.toString(), signal: options.signal });
66
+ return this.operations.handle(accepted);
67
+ }
68
+ /**
69
+ * Shuts a fleet's machines down: `endia compose stop`.
70
+ *
71
+ * @param name - The fleet's name.
72
+ * @param options - The services, and the shutdown time.
73
+ * @returns The operation; its result names the machines stopped.
74
+ */
75
+ stop(name, options = {}) {
76
+ return this.sendAction(Routes.fleetStop(name), options.services, options.timeout, options.signal);
77
+ }
78
+ /**
79
+ * Starts a fleet's stopped machines: `endia compose start`.
80
+ *
81
+ * @param name - The fleet's name.
82
+ * @param options - The services.
83
+ * @returns The operation; its result names the machines started.
84
+ */
85
+ start(name, options = {}) {
86
+ return this.sendAction(Routes.fleetStart(name), options.services, undefined, options.signal);
87
+ }
88
+ /**
89
+ * Restarts a fleet's machines: `endia compose restart`.
90
+ *
91
+ * @param name - The fleet's name.
92
+ * @param options - The services, and the shutdown time.
93
+ * @returns The operation; its result names the machines restarted.
94
+ */
95
+ restart(name, options = {}) {
96
+ return this.sendAction(Routes.fleetRestart(name), options.services, options.timeout, options.signal);
97
+ }
98
+ /**
99
+ * Lists fleets: `endia compose ls`.
100
+ *
101
+ * @param options - `all` includes fleets with no running machine.
102
+ * @returns The fleets.
103
+ */
104
+ list(options = {}) {
105
+ return this.connection.read(Routes.fleets + new QueryText().addFlag("all", options.all).toString(), options.signal);
106
+ }
107
+ /**
108
+ * Reads one fleet with its machines: `endia compose ps`.
109
+ *
110
+ * @param name - The fleet's name.
111
+ * @param signal - Cancels the request.
112
+ * @returns The fleet.
113
+ */
114
+ inspect(name, signal) {
115
+ return this.connection.read(Routes.fleet(name), signal);
116
+ }
117
+ /**
118
+ * Reads a fleet's purpose with every change to it while the fleet has existed: `endia purpose show`.
119
+ *
120
+ * @param name - The fleet's name.
121
+ * @param signal - Cancels the request.
122
+ * @returns The purpose, or null, and the history, oldest first.
123
+ */
124
+ purpose(name, signal) {
125
+ return this.connection.read(Routes.fleetPurpose(name), signal);
126
+ }
127
+ /**
128
+ * Replaces a fleet's whole purpose, which its machines without a purpose of their own have: `endia purpose set`.
129
+ *
130
+ * @param name - The fleet's name.
131
+ * @param title - What the fleet is being used for, in one line of 1 to 200 characters.
132
+ * @param options - `detail` is Markdown notes of at most 65536 bytes in UTF-8; without it the detail stored before is cleared.
133
+ * @returns The purpose now stored.
134
+ * @throws Error when the title has a line break or the detail is too long. Nothing is sent then.
135
+ */
136
+ async setPurpose(name, title, options = {}) {
137
+ const body = purposeBody(title, options.detail);
138
+ return await this.connection.change({ method: "PUT", path: Routes.fleetPurpose(name), body: jsonBody(body), signal: options.signal });
139
+ }
140
+ /**
141
+ * Removes a fleet's purpose: `endia purpose clear`. Clearing a fleet without a purpose changes nothing.
142
+ *
143
+ * @param name - The fleet's name.
144
+ * @param signal - Cancels the request.
145
+ * @returns The purpose now stored, which is null.
146
+ */
147
+ clearPurpose(name, signal) {
148
+ return this.connection.change({ method: "DELETE", path: Routes.fleetPurpose(name), signal });
149
+ }
150
+ /**
151
+ * Waits until a fleet's machines are running and ready, and healthy when they have a health check, as `endia compose up --wait`
152
+ * waits once its operation has succeeded. `up`'s result already comes only once every machine it created, recreated or started
153
+ * was made ready; this wait adds what `--wait` adds: it checks every selected machine, kept ones too, waits for health checks to
154
+ * pass, and fails when a machine exits or becomes unhealthy.
155
+ *
156
+ * @param name - The fleet's name.
157
+ * @param options - Which services, as `up` takes them, and the longest wait.
158
+ * @returns The machines waited for, as last read.
159
+ * @throws MachineFailedError when a machine exited or became unhealthy.
160
+ * @throws DOMException named `TimeoutError` when some machine was not yet running, ready and healthy when the time-out passed. The
161
+ * machines carry on.
162
+ * @throws Error when a named service is not in the fleet's model.
163
+ */
164
+ async wait(name, options = {}) {
165
+ const deadline = options.timeout === undefined ? undefined : Date.now() + options.timeout * 1000;
166
+ let selected;
167
+ for (;;) {
168
+ const fleet = await this.inspect(name, options.signal);
169
+ selected ??= selectServices(fleet.model, options.services ?? []);
170
+ const machines = fleet.machines.filter((machine) => selected?.has(machine.service) === true);
171
+ const failed = machines.find(hasFailed);
172
+ if (failed !== undefined) {
173
+ throw new MachineFailedError(failed);
174
+ }
175
+ if (machines.every(isUp)) {
176
+ return machines;
177
+ }
178
+ if (deadline !== undefined && Date.now() >= deadline) {
179
+ throw new DOMException(`timed out after ${String(options.timeout)} seconds waiting for the machines to be running and healthy`, "TimeoutError");
180
+ }
181
+ await delay(WaitPollMilliseconds, undefined, { signal: options.signal });
182
+ }
183
+ }
184
+ /**
185
+ * Reads the main-process output of a fleet's machines, each line naming its machine: `endia compose logs`.
186
+ *
187
+ * @param name - The fleet's name.
188
+ * @param options - Which lines; `services` limits them to some services, and `index` to one machine of each.
189
+ * @returns The lines, as they arrive.
190
+ */
191
+ logs(name, options = {}) {
192
+ const query = addLogOptions(new QueryText(), options).addEach("services", options.services).add("index", options.index);
193
+ return this.connection.readLines(Routes.fleetLogs(name) + query.toString(), options.signal);
194
+ }
195
+ // Sends the archive with PUT /v1/fleets/{name}. A model is packed into a temporary archive, which is deleted once the service
196
+ // has answered.
197
+ async sendFleet(model, options, dryRun) {
198
+ const purpose = purposeOf(options);
199
+ if (isFleetArchive(model)) {
200
+ if (purpose !== undefined) {
201
+ throw new Error("A purpose cannot be given with a packed archive; put purpose.json in the archive.");
202
+ }
203
+ const archive = model.archive;
204
+ const body = tarBody(archive);
205
+ return this.sendArchive(model.name, body, options, dryRun);
206
+ }
207
+ const resolved = typeof model === "string" ? JSON.parse(model) : model;
208
+ const packed = await packFleet(resolved, typeof model === "string" ? model : JSON.stringify(model), options.services ?? [], purpose);
209
+ try {
210
+ return await this.sendArchive(resolved.name, packed.body, options, dryRun);
211
+ }
212
+ finally {
213
+ await packed.delete();
214
+ }
215
+ }
216
+ sendArchive(name, body, options, dryRun) {
217
+ const query = new QueryText()
218
+ .addEach("services", options.services)
219
+ .addEach("scale", options.scale)
220
+ .add("build", options.build)
221
+ .add("recreate", options.recreate)
222
+ .addFlag("removeOrphans", options.removeOrphans)
223
+ .add("timeout", options.timeout)
224
+ .addFlag("dryRun", dryRun)
225
+ .addEach("configFiles", options.configFiles)
226
+ .add("projectDirectory", options.projectDirectory);
227
+ return this.connection.change({ method: "PUT", path: Routes.fleet(name) + query.toString(), body, signal: options.signal }, false);
228
+ }
229
+ async sendAction(path, services, timeout, signal) {
230
+ const query = new QueryText().addEach("services", services).add("timeout", timeout);
231
+ const accepted = await this.connection.change({ method: "POST", path: path + query.toString(), signal });
232
+ return this.operations.handle(accepted);
233
+ }
234
+ }
235
+ // Detail without a title is refused as the CLI refuses --purpose-file without --purpose.
236
+ function purposeOf(options) {
237
+ if (options.purpose === undefined) {
238
+ if (options.purposeDetail !== undefined) {
239
+ throw new Error("purposeDetail needs purpose.");
240
+ }
241
+ return undefined;
242
+ }
243
+ return purposeBody(options.purpose, options.purposeDetail);
244
+ }
245
+ function isFleetArchive(model) {
246
+ return typeof model === "object" && "archive" in model && (model.archive instanceof Uint8Array || typeof model.archive === "function");
247
+ }
248
+ /** Whether a machine ends a wait in failure: it exited, or its health check made it unhealthy. */
249
+ function hasFailed(machine) {
250
+ return machine.health === "unhealthy" || machine.state === "exited";
251
+ }
252
+ /** Whether a machine is what a wait waits for: running and ready, and healthy when it has a health check. */
253
+ function isUp(machine) {
254
+ return machine.state === "running" && machine.ready && (machine.health === "none" || machine.health === "healthy");
255
+ }