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
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,35 @@
1
- # Temporary Holding Version
1
+ # endia
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Client for EastIndia, which runs Windows desktop test machines that tests drive like Docker containers. Early preview: the product is not released yet.
4
+
5
+ The package needs Node 20 or later and ships ES modules with TypeScript declarations. It calls the EastIndia service's HTTPS API, so a Node test runner can build images, bring fleets of machines up and down, and run programs in them without the `endia` command line.
6
+
7
+ ## Example
8
+
9
+ `ledger/Endiafile` holds a recipe of two stages; each stage begins with a `FROM`, and `AS` names it:
10
+
11
+ ```dockerfile
12
+ FROM eastindia/development:latest AS tools
13
+ RUN choco install -y git
14
+
15
+ FROM tools AS app
16
+ COPY publish/ C:/app/
17
+ ```
18
+
19
+ ```js
20
+ import { EastIndiaClient } from "endia";
21
+
22
+ // Connects where the endia command line would: EASTINDIA_HOST and EASTINDIA_CERT_PATH, or its current context.
23
+ const client = EastIndiaClient.fromEnv();
24
+ console.log((await client.version()).version);
25
+
26
+ const build = await client.images.build({ folder: "./ledger" }, { tags: ["lab/ledger:1"] });
27
+ const built = await build.result({ onLog: (text) => process.stdout.write(text) });
28
+ console.log(built.digest);
29
+
30
+ // A recipe of several stages: target names the stage to build, here the first.
31
+ const tools = await (await client.images.build({ folder: "./ledger" }, { target: "tools", tags: ["lab/tools:1"] })).result();
32
+ console.log(tools.stages?.map((stage) => [stage.name, stage.cacheHit]));
33
+
34
+ client.close();
35
+ ```
@@ -0,0 +1,36 @@
1
+ import type { RequestBody } from "../transport/api-connection.js";
2
+ /** One entry of an archive: the file, folder or link on disk, and the name the entry takes in the archive. */
3
+ export interface ArchiveEntry {
4
+ /** The full path on disk. A link is archived as a link, never as what it points at. */
5
+ readonly source: string;
6
+ /** The name in the archive, with `/` separators; a folder's name ends in `/`. */
7
+ readonly name: string;
8
+ }
9
+ /**
10
+ * A tar archive written to a temporary file, so that a request that is sent again after a failed connection sends the same bytes.
11
+ * The archive belongs to the caller that wrote it, which deletes it with `delete` once the request has been answered.
12
+ */
13
+ export declare class ArchiveFile {
14
+ private readonly folder;
15
+ /** The archive's full path. */
16
+ readonly path: string;
17
+ /** The archive's size in bytes. */
18
+ readonly size: number;
19
+ private constructor();
20
+ /**
21
+ * Writes an archive of the given entries, in the given order, in the POSIX pax format with UTF-8 names.
22
+ *
23
+ * @param entries - The entries. A folder entry holds only itself; each file under it needs its own entry.
24
+ * @param texts - Files that exist only in memory, such as a resolved Compose model, each written under its name.
25
+ * @param followLinks - Whether a link is written as what it points at, rather than as a link.
26
+ * @returns The archive.
27
+ */
28
+ static write(entries: readonly ArchiveEntry[], texts?: readonly {
29
+ name: string;
30
+ text: string;
31
+ }[], followLinks?: boolean): Promise<ArchiveFile>;
32
+ /** The archive as a request body, opened afresh for each attempt. */
33
+ get body(): RequestBody;
34
+ /** Deletes the archive and its temporary folder. */
35
+ delete(): Promise<void>;
36
+ }
@@ -0,0 +1,71 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { create } from "tar";
6
+ import { MediaTypes } from "../routes.js";
7
+ /** The prefix of the temporary folder that holds an archive while it is sent. */
8
+ const TemporaryFolderPrefix = "eastindia-client-";
9
+ /** The file name of the archive inside its temporary folder. */
10
+ const ArchiveFileName = "archive.tar";
11
+ /**
12
+ * A tar archive written to a temporary file, so that a request that is sent again after a failed connection sends the same bytes.
13
+ * The archive belongs to the caller that wrote it, which deletes it with `delete` once the request has been answered.
14
+ */
15
+ export class ArchiveFile {
16
+ folder;
17
+ /** The archive's full path. */
18
+ path;
19
+ /** The archive's size in bytes. */
20
+ size;
21
+ constructor(folder, size) {
22
+ this.folder = folder;
23
+ this.path = path.join(folder, ArchiveFileName);
24
+ this.size = size;
25
+ }
26
+ /**
27
+ * Writes an archive of the given entries, in the given order, in the POSIX pax format with UTF-8 names.
28
+ *
29
+ * @param entries - The entries. A folder entry holds only itself; each file under it needs its own entry.
30
+ * @param texts - Files that exist only in memory, such as a resolved Compose model, each written under its name.
31
+ * @param followLinks - Whether a link is written as what it points at, rather than as a link.
32
+ * @returns The archive.
33
+ */
34
+ static async write(entries, texts = [], followLinks = false) {
35
+ const folder = await mkdtemp(path.join(tmpdir(), TemporaryFolderPrefix));
36
+ try {
37
+ // The texts are written beside the archive and added like any other file, under their own names.
38
+ const written = await Promise.all(texts.map(async (entry, index) => {
39
+ const source = path.join(folder, `text-${String(index)}`);
40
+ await writeFile(source, entry.text, "utf8");
41
+ return { source, name: entry.name };
42
+ }));
43
+ const all = [...written, ...entries];
44
+ const names = new Map(all.map((entry) => [path.resolve(entry.source), entry.name]));
45
+ await create({
46
+ file: path.join(folder, ArchiveFileName),
47
+ preservePaths: true,
48
+ noDirRecurse: true,
49
+ follow: followLinks,
50
+ // Each entry is read from its full path and renamed to its name in the archive.
51
+ onWriteEntry: (entry) => {
52
+ entry.path = names.get(entry.absolute) ?? entry.path;
53
+ },
54
+ }, all.map((entry) => entry.source));
55
+ const size = (await stat(path.join(folder, ArchiveFileName))).size;
56
+ return new ArchiveFile(folder, size);
57
+ }
58
+ catch (error) {
59
+ await rm(folder, { recursive: true, force: true });
60
+ throw error;
61
+ }
62
+ }
63
+ /** The archive as a request body, opened afresh for each attempt. */
64
+ get body() {
65
+ return { contentType: MediaTypes.tar, open: () => createReadStream(this.path), length: this.size };
66
+ }
67
+ /** Deletes the archive and its temporary folder. */
68
+ async delete() {
69
+ await rm(this.folder, { recursive: true, force: true });
70
+ }
71
+ }
@@ -0,0 +1,63 @@
1
+ import { ArchiveFile, type ArchiveEntry } from "./archive-file.js";
2
+ /** The archive of a build context, and the recipe's path inside it, which the build request names as `dockerfile`. */
3
+ export interface PackedContext {
4
+ readonly archive: ArchiveFile;
5
+ readonly recipe: string;
6
+ }
7
+ /**
8
+ * Packs a build context folder as `endia build` sends it: a tar archive of every file, folder and link in the folder that the
9
+ * ignore file does not exclude. A link is archived as a link, never as what it points at. The recipe and the ignore file are always
10
+ * sent.
11
+ *
12
+ * The ignore file is chosen as `ignoreFileFor` chooses it. A recipe outside the folder is added at the archive's root as
13
+ * `.dockerfile.` followed by 16 random hexadecimal digits, as Docker adds it, with its own ignore file beside it under that name
14
+ * followed by `.endiaignore` or `.dockerignore`, as the file on disk is named.
15
+ *
16
+ * @param folder - The context folder.
17
+ * @param recipe - The recipe's path, inside the folder or outside it; when undefined, `Endiafile` in the folder when it exists,
18
+ * and `Dockerfile` otherwise.
19
+ * @returns The archive and the recipe's path inside it; the caller deletes the archive.
20
+ * @throws Error when the folder does not exist, when a recipe outside the folder does not exist, or when the ignore file holds a
21
+ * malformed pattern.
22
+ */
23
+ export declare function packBuildContext(folder: string, recipe?: string): Promise<PackedContext>;
24
+ /**
25
+ * Lists the entries of a context folder that a build sends, each folder followed by what it holds, in ordinal order of name within
26
+ * each folder. `compose up` sends each service's build context with the same entries.
27
+ *
28
+ * @param root - The context folder's full path.
29
+ * @param recipe - The recipe's path inside the folder, with `/` separators, or undefined when it lies outside.
30
+ * @param ignoreFile - The ignore file's full path, which may not exist.
31
+ * @returns Each entry's full path and its path inside the folder; a folder's path ends in `/`.
32
+ */
33
+ export declare function contextEntries(root: string, recipe: string | undefined, ignoreFile: string): Promise<ArchiveEntry[]>;
34
+ /**
35
+ * Gives the recipe of a build that names none, as the `endia` CLI chooses it: `Endiafile` at the root of the context folder when it
36
+ * exists, and `Dockerfile` otherwise.
37
+ *
38
+ * @param root - The context folder's full path.
39
+ * @returns The recipe's file name.
40
+ */
41
+ export declare function defaultRecipe(root: string): string;
42
+ /**
43
+ * Gives the ignore file that applies to a recipe, as the `endia` CLI chooses it: the first of these that exists. A recipe's own
44
+ * ignore file is named as the recipe's file name followed by the ignore file's name, beside the recipe.
45
+ *
46
+ * 1. The recipe's own `.endiaignore`.
47
+ * 2. The recipe's own `.dockerignore`.
48
+ * 3. `.endiaignore` at the root of the folder.
49
+ * 4. `.dockerignore` at the root of the folder.
50
+ *
51
+ * @param root - The context folder's full path.
52
+ * @param recipe - The recipe's full path.
53
+ * @returns The ignore file's full path; `.dockerignore` at the root, which does not exist, when none of the four exists.
54
+ */
55
+ export declare function ignoreFileFor(root: string, recipe: string): string;
56
+ /**
57
+ * Gives a path's place inside a folder.
58
+ *
59
+ * @param root - The folder's full path.
60
+ * @param target - A full path.
61
+ * @returns The path inside the folder with `/` separators, or undefined when it lies outside the folder.
62
+ */
63
+ export declare function pathInside(root: string, target: string): string | undefined;
@@ -0,0 +1,132 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { lstat, readdir, readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { ArchiveFile } from "./archive-file.js";
6
+ import { DockerIgnore, EndiaIgnoreFileName, IgnoreFileName } from "./docker-ignore.js";
7
+ /** EastIndia's name for a recipe, which a build that names none reads first. */
8
+ const Endiafile = "Endiafile";
9
+ /** Docker's name for a recipe, which a build that names none reads when the context holds no `Endiafile`. */
10
+ const Dockerfile = "Dockerfile";
11
+ /** The name prefix of a recipe that lies outside the context folder, which Docker adds at the archive's root under a random name. */
12
+ const OutsideRecipePrefix = ".dockerfile.";
13
+ /** The number of random bytes in that name; each byte gives two hexadecimal digits, so the name ends in 16 digits. */
14
+ const OutsideRecipeRandomBytes = 8;
15
+ /**
16
+ * Packs a build context folder as `endia build` sends it: a tar archive of every file, folder and link in the folder that the
17
+ * ignore file does not exclude. A link is archived as a link, never as what it points at. The recipe and the ignore file are always
18
+ * sent.
19
+ *
20
+ * The ignore file is chosen as `ignoreFileFor` chooses it. A recipe outside the folder is added at the archive's root as
21
+ * `.dockerfile.` followed by 16 random hexadecimal digits, as Docker adds it, with its own ignore file beside it under that name
22
+ * followed by `.endiaignore` or `.dockerignore`, as the file on disk is named.
23
+ *
24
+ * @param folder - The context folder.
25
+ * @param recipe - The recipe's path, inside the folder or outside it; when undefined, `Endiafile` in the folder when it exists,
26
+ * and `Dockerfile` otherwise.
27
+ * @returns The archive and the recipe's path inside it; the caller deletes the archive.
28
+ * @throws Error when the folder does not exist, when a recipe outside the folder does not exist, or when the ignore file holds a
29
+ * malformed pattern.
30
+ */
31
+ export async function packBuildContext(folder, recipe) {
32
+ const root = path.resolve(folder);
33
+ if (!existsSync(root) || !statSync(root).isDirectory()) {
34
+ throw new Error(`The build context '${folder}' is not a folder.`);
35
+ }
36
+ const recipePath = path.resolve(root, recipe ?? defaultRecipe(root));
37
+ const inside = pathInside(root, recipePath);
38
+ if (inside === undefined && !existsSync(recipePath)) {
39
+ throw new Error(`The recipe '${recipePath}' does not exist.`);
40
+ }
41
+ const ignoreFile = ignoreFileFor(root, recipePath);
42
+ const entries = await contextEntries(root, inside, ignoreFile);
43
+ const recipeName = inside ?? OutsideRecipePrefix + randomBytes(OutsideRecipeRandomBytes).toString("hex");
44
+ if (inside === undefined) {
45
+ entries.push({ source: recipePath, name: recipeName });
46
+ // Only a recipe's own ignore file lies outside the folder; the root one is already among the entries.
47
+ if (pathInside(root, ignoreFile) === undefined) {
48
+ entries.push({ source: ignoreFile, name: recipeName + ignoreFile.slice(recipePath.length) });
49
+ }
50
+ }
51
+ return { archive: await ArchiveFile.write(entries), recipe: recipeName };
52
+ }
53
+ /**
54
+ * Lists the entries of a context folder that a build sends, each folder followed by what it holds, in ordinal order of name within
55
+ * each folder. `compose up` sends each service's build context with the same entries.
56
+ *
57
+ * @param root - The context folder's full path.
58
+ * @param recipe - The recipe's path inside the folder, with `/` separators, or undefined when it lies outside.
59
+ * @param ignoreFile - The ignore file's full path, which may not exist.
60
+ * @returns Each entry's full path and its path inside the folder; a folder's path ends in `/`.
61
+ */
62
+ export async function contextEntries(root, recipe, ignoreFile) {
63
+ const ignore = existsSync(ignoreFile) ? DockerIgnore.parse(await readFile(ignoreFile, "utf8"), ignoreFile) : DockerIgnore.empty;
64
+ const alwaysSent = new Set([recipe, pathInside(root, ignoreFile)].filter((name) => name !== undefined));
65
+ const entries = [];
66
+ await collectEntries(root, "", ignore, alwaysSent, entries);
67
+ return entries;
68
+ }
69
+ /**
70
+ * Gives the recipe of a build that names none, as the `endia` CLI chooses it: `Endiafile` at the root of the context folder when it
71
+ * exists, and `Dockerfile` otherwise.
72
+ *
73
+ * @param root - The context folder's full path.
74
+ * @returns The recipe's file name.
75
+ */
76
+ export function defaultRecipe(root) {
77
+ return isFile(path.join(root, Endiafile)) ? Endiafile : Dockerfile;
78
+ }
79
+ /**
80
+ * Gives the ignore file that applies to a recipe, as the `endia` CLI chooses it: the first of these that exists. A recipe's own
81
+ * ignore file is named as the recipe's file name followed by the ignore file's name, beside the recipe.
82
+ *
83
+ * 1. The recipe's own `.endiaignore`.
84
+ * 2. The recipe's own `.dockerignore`.
85
+ * 3. `.endiaignore` at the root of the folder.
86
+ * 4. `.dockerignore` at the root of the folder.
87
+ *
88
+ * @param root - The context folder's full path.
89
+ * @param recipe - The recipe's full path.
90
+ * @returns The ignore file's full path; `.dockerignore` at the root, which does not exist, when none of the four exists.
91
+ */
92
+ export function ignoreFileFor(root, recipe) {
93
+ const candidates = [recipe + EndiaIgnoreFileName, recipe + IgnoreFileName, path.join(root, EndiaIgnoreFileName), path.join(root, IgnoreFileName)];
94
+ return candidates.find(isFile) ?? path.join(root, IgnoreFileName);
95
+ }
96
+ function isFile(file) {
97
+ return existsSync(file) && statSync(file).isFile();
98
+ }
99
+ /**
100
+ * Gives a path's place inside a folder.
101
+ *
102
+ * @param root - The folder's full path.
103
+ * @param target - A full path.
104
+ * @returns The path inside the folder with `/` separators, or undefined when it lies outside the folder.
105
+ */
106
+ export function pathInside(root, target) {
107
+ const relative = path.relative(root, target).split(path.sep).join("/");
108
+ return relative === ".." || relative.startsWith("../") || path.isAbsolute(relative) ? undefined : relative;
109
+ }
110
+ // Lists a folder's entries and, for each sub-folder that needs it, the sub-folder's entries. A sub-folder the ignore file leaves out
111
+ // is still listed when the recipe or the ignore file lies under it, or when the ignore file has a `!` line, which could re-include a
112
+ // file under it; Docker's own walk does the same. A link is never followed.
113
+ async function collectEntries(folder, prefix, ignore, alwaysSent, entries) {
114
+ const names = (await readdir(folder)).sort(compareOrdinal);
115
+ for (const name of names) {
116
+ const relative = prefix + name;
117
+ const source = path.join(folder, name);
118
+ const item = await lstat(source);
119
+ const sent = alwaysSent.has(relative) || !ignore.isIgnored(relative);
120
+ const walked = item.isDirectory() && (sent || ignore.reincludes || [...alwaysSent].some((kept) => kept.startsWith(`${relative}/`)));
121
+ if (sent) {
122
+ entries.push({ source, name: item.isDirectory() ? `${relative}/` : relative });
123
+ }
124
+ if (walked) {
125
+ await collectEntries(source, `${relative}/`, ignore, alwaysSent, entries);
126
+ }
127
+ }
128
+ }
129
+ // Ordinal order compares UTF-16 code units, as the CLI orders names, rather than the locale's order.
130
+ function compareOrdinal(first, second) {
131
+ return first < second ? -1 : first > second ? 1 : 0;
132
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Docker's name for an ignore file. A recipe may have its own ignore file beside it, named as the recipe's file name followed by
3
+ * this name, such as `recipes/dotnet.Dockerfile.dockerignore`.
4
+ */
5
+ export declare const IgnoreFileName = ".dockerignore";
6
+ /** EastIndia's name for an ignore file, which a build reads in preference to a `.dockerignore` in the same place. */
7
+ export declare const EndiaIgnoreFileName = ".endiaignore";
8
+ /**
9
+ * A build context's ignore file: which of the context's files a build leaves out of the archive it sends, so that no `COPY` can copy
10
+ * them and they do not change the build key. It matches paths by the same rules as the `endia` CLI, which follows Docker's
11
+ * `.dockerignore` matching. A folder sent by this client therefore gives the same archive as the same folder sent by the CLI.
12
+ *
13
+ * Patterns are cleaned and anchored at the context root, and a leading `/` is dropped. `*` and `?` stop at `/`, `**` spans folders,
14
+ * and `[...]` is a character class. A later line overrides an earlier one, and `!` re-includes. A path is ignored when the pattern
15
+ * that decides it matches the path itself or any of its parent folders.
16
+ */
17
+ export declare class DockerIgnore {
18
+ private readonly patterns;
19
+ private constructor();
20
+ /** An ignore file that excludes nothing, for a context without one. */
21
+ static readonly empty: DockerIgnore;
22
+ /**
23
+ * Reads the text of an ignore file. Lines end in LF or CRLF. Blank lines and lines whose first non-blank character is `#` are
24
+ * skipped.
25
+ *
26
+ * @param text - The file's content.
27
+ * @param file - The file's path, which error messages name.
28
+ * @returns The ignore file.
29
+ * @throws Error naming the file, the line and the column of every malformed pattern.
30
+ */
31
+ static parse(text: string, file?: string): DockerIgnore;
32
+ /**
33
+ * Whether any line re-includes with `!`. Only then can a path under a left-out folder be sent, so only then must a walk of the
34
+ * context look inside a left-out folder.
35
+ */
36
+ get reincludes(): boolean;
37
+ /**
38
+ * Decides whether a context path is left out. The patterns apply in file order, and the last pattern that matches decides. The
39
+ * context root itself is never left out.
40
+ *
41
+ * @param path - A path relative to the context root, with `/` separators.
42
+ * @returns True when the path, or one of its parent folders, is left out.
43
+ * @throws Error when the path leads outside the context.
44
+ */
45
+ isIgnored(path: string): boolean;
46
+ }
47
+ /**
48
+ * Cleans a path with `/` separators by the rules of Go's `path.Clean`, which Docker applies to ignore patterns.
49
+ *
50
+ * - Empty and `.` segments are dropped.
51
+ * - A `..` removes the segment before it; at the start of a relative path it stays.
52
+ * - An empty result is `.`.
53
+ *
54
+ * @param path - The path.
55
+ * @returns The cleaned path.
56
+ */
57
+ export declare function cleanPath(path: string): string;