redkite 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +819 -0
  3. package/bin/redkite.mjs +12 -0
  4. package/dist/build.d.ts +27 -0
  5. package/dist/build.d.ts.map +1 -0
  6. package/dist/build.js +132 -0
  7. package/dist/build.js.map +1 -0
  8. package/dist/checks.d.ts +4 -0
  9. package/dist/checks.d.ts.map +1 -0
  10. package/dist/checks.js +49 -0
  11. package/dist/checks.js.map +1 -0
  12. package/dist/cli/agent.d.ts +2 -0
  13. package/dist/cli/agent.d.ts.map +1 -0
  14. package/dist/cli/agent.js +24 -0
  15. package/dist/cli/agent.js.map +1 -0
  16. package/dist/cli/config.d.ts +5 -0
  17. package/dist/cli/config.d.ts.map +1 -0
  18. package/dist/cli/config.js +217 -0
  19. package/dist/cli/config.js.map +1 -0
  20. package/dist/cli/index.d.ts +11 -0
  21. package/dist/cli/index.d.ts.map +1 -0
  22. package/dist/cli/index.js +438 -0
  23. package/dist/cli/index.js.map +1 -0
  24. package/dist/cli/log.d.ts +9 -0
  25. package/dist/cli/log.d.ts.map +1 -0
  26. package/dist/cli/log.js +128 -0
  27. package/dist/cli/log.js.map +1 -0
  28. package/dist/cli/screen.d.ts +34 -0
  29. package/dist/cli/screen.d.ts.map +1 -0
  30. package/dist/cli/screen.js +298 -0
  31. package/dist/cli/screen.js.map +1 -0
  32. package/dist/cli/viewer.d.ts +9 -0
  33. package/dist/cli/viewer.d.ts.map +1 -0
  34. package/dist/cli/viewer.js +139 -0
  35. package/dist/cli/viewer.js.map +1 -0
  36. package/dist/config.d.ts +7 -0
  37. package/dist/config.d.ts.map +1 -0
  38. package/dist/config.js +79 -0
  39. package/dist/config.js.map +1 -0
  40. package/dist/deploy.d.ts +19 -0
  41. package/dist/deploy.d.ts.map +1 -0
  42. package/dist/deploy.js +275 -0
  43. package/dist/deploy.js.map +1 -0
  44. package/dist/docker.d.ts +96 -0
  45. package/dist/docker.d.ts.map +1 -0
  46. package/dist/docker.js +325 -0
  47. package/dist/docker.js.map +1 -0
  48. package/dist/dockerfile.d.ts +14 -0
  49. package/dist/dockerfile.d.ts.map +1 -0
  50. package/dist/dockerfile.js +160 -0
  51. package/dist/dockerfile.js.map +1 -0
  52. package/dist/health.d.ts +13 -0
  53. package/dist/health.d.ts.map +1 -0
  54. package/dist/health.js +54 -0
  55. package/dist/health.js.map +1 -0
  56. package/dist/host.d.ts +21 -0
  57. package/dist/host.d.ts.map +1 -0
  58. package/dist/host.js +27 -0
  59. package/dist/host.js.map +1 -0
  60. package/dist/index.d.ts +26 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +26 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/layout.d.ts +5 -0
  65. package/dist/layout.d.ts.map +1 -0
  66. package/dist/layout.js +45 -0
  67. package/dist/layout.js.map +1 -0
  68. package/dist/localHost.d.ts +7 -0
  69. package/dist/localHost.d.ts.map +1 -0
  70. package/dist/localHost.js +32 -0
  71. package/dist/localHost.js.map +1 -0
  72. package/dist/log.d.ts +14 -0
  73. package/dist/log.d.ts.map +1 -0
  74. package/dist/log.js +15 -0
  75. package/dist/log.js.map +1 -0
  76. package/dist/nginx.d.ts +4 -0
  77. package/dist/nginx.d.ts.map +1 -0
  78. package/dist/nginx.js +53 -0
  79. package/dist/nginx.js.map +1 -0
  80. package/dist/pipeline.d.ts +103 -0
  81. package/dist/pipeline.d.ts.map +1 -0
  82. package/dist/pipeline.js +142 -0
  83. package/dist/pipeline.js.map +1 -0
  84. package/dist/presets/index.d.ts +3 -0
  85. package/dist/presets/index.d.ts.map +1 -0
  86. package/dist/presets/index.js +3 -0
  87. package/dist/presets/index.js.map +1 -0
  88. package/dist/presets/nextApp.d.ts +15 -0
  89. package/dist/presets/nextApp.d.ts.map +1 -0
  90. package/dist/presets/nextApp.js +53 -0
  91. package/dist/presets/nextApp.js.map +1 -0
  92. package/dist/presets/nodeApp.d.ts +19 -0
  93. package/dist/presets/nodeApp.d.ts.map +1 -0
  94. package/dist/presets/nodeApp.js +29 -0
  95. package/dist/presets/nodeApp.js.map +1 -0
  96. package/dist/secrets/bitwarden.d.ts +3 -0
  97. package/dist/secrets/bitwarden.d.ts.map +1 -0
  98. package/dist/secrets/bitwarden.js +6 -0
  99. package/dist/secrets/bitwarden.js.map +1 -0
  100. package/dist/secrets/refs.d.ts +9 -0
  101. package/dist/secrets/refs.d.ts.map +1 -0
  102. package/dist/secrets/refs.js +20 -0
  103. package/dist/secrets/refs.js.map +1 -0
  104. package/dist/secrets/store.d.ts +9 -0
  105. package/dist/secrets/store.d.ts.map +1 -0
  106. package/dist/secrets/store.js +104 -0
  107. package/dist/secrets/store.js.map +1 -0
  108. package/dist/services/ensure.d.ts +18 -0
  109. package/dist/services/ensure.d.ts.map +1 -0
  110. package/dist/services/ensure.js +93 -0
  111. package/dist/services/ensure.js.map +1 -0
  112. package/dist/services/index.d.ts +19 -0
  113. package/dist/services/index.d.ts.map +1 -0
  114. package/dist/services/index.js +27 -0
  115. package/dist/services/index.js.map +1 -0
  116. package/dist/services/planned.d.ts +20 -0
  117. package/dist/services/planned.d.ts.map +1 -0
  118. package/dist/services/planned.js +77 -0
  119. package/dist/services/planned.js.map +1 -0
  120. package/dist/shell.d.ts +11 -0
  121. package/dist/shell.d.ts.map +1 -0
  122. package/dist/shell.js +65 -0
  123. package/dist/shell.js.map +1 -0
  124. package/dist/source.d.ts +16 -0
  125. package/dist/source.d.ts.map +1 -0
  126. package/dist/source.js +108 -0
  127. package/dist/source.js.map +1 -0
  128. package/dist/sshHost.d.ts +15 -0
  129. package/dist/sshHost.d.ts.map +1 -0
  130. package/dist/sshHost.js +91 -0
  131. package/dist/sshHost.js.map +1 -0
  132. package/dist/steps.d.ts +27 -0
  133. package/dist/steps.d.ts.map +1 -0
  134. package/dist/steps.js +69 -0
  135. package/dist/steps.js.map +1 -0
  136. package/dist/topology.d.ts +40 -0
  137. package/dist/topology.d.ts.map +1 -0
  138. package/dist/topology.js +113 -0
  139. package/dist/topology.js.map +1 -0
  140. package/dist/types.d.ts +100 -0
  141. package/dist/types.d.ts.map +1 -0
  142. package/dist/types.js +2 -0
  143. package/dist/types.js.map +1 -0
  144. package/package.json +64 -0
  145. package/src/build.ts +236 -0
  146. package/src/checks.ts +65 -0
  147. package/src/cli/agent.ts +28 -0
  148. package/src/cli/config.ts +275 -0
  149. package/src/cli/index.ts +559 -0
  150. package/src/cli/log.ts +168 -0
  151. package/src/cli/screen.ts +409 -0
  152. package/src/cli/viewer.ts +192 -0
  153. package/src/config.ts +90 -0
  154. package/src/deploy.ts +409 -0
  155. package/src/docker.ts +433 -0
  156. package/src/dockerfile.ts +228 -0
  157. package/src/health.ts +83 -0
  158. package/src/host.ts +54 -0
  159. package/src/index.ts +25 -0
  160. package/src/layout.ts +41 -0
  161. package/src/localHost.ts +50 -0
  162. package/src/log.ts +38 -0
  163. package/src/nginx.ts +63 -0
  164. package/src/pipeline.ts +335 -0
  165. package/src/presets/index.ts +2 -0
  166. package/src/presets/nextApp.ts +70 -0
  167. package/src/presets/nodeApp.ts +46 -0
  168. package/src/secrets/bitwarden.ts +7 -0
  169. package/src/secrets/refs.ts +32 -0
  170. package/src/secrets/store.ts +147 -0
  171. package/src/services/ensure.ts +160 -0
  172. package/src/services/index.ts +48 -0
  173. package/src/services/planned.ts +119 -0
  174. package/src/shell.ts +95 -0
  175. package/src/source.ts +171 -0
  176. package/src/sshHost.ts +145 -0
  177. package/src/steps.ts +101 -0
  178. package/src/topology.ts +199 -0
  179. package/src/types.ts +243 -0
@@ -0,0 +1,275 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ import type { Deployment, Environment } from "../types.js";
7
+
8
+ // Found by convention rather than named on the command line. A repository has
9
+ // one deployment, and requiring the path is a flag nobody ever varies.
10
+ //
11
+ // The environments live beside it, one file each, so the thing that differs
12
+ // between staging and production is a file rather than a key several levels
13
+ // down a literal.
14
+
15
+ const EXTENSIONS = ["ts", "mts", "js", "mjs"] as const;
16
+
17
+ const CANDIDATES = EXTENSIONS.map((extension) => `redkite.config.${extension}`);
18
+
19
+ // redkite.<environment>.config.<extension>, beside the deployment it belongs to
20
+ const PER_ENVIRONMENT = /^redkite\.([a-z0-9-]+)\.config\.(ts|mts|js|mjs)$/;
21
+
22
+ // Node strips types itself from 22.18 on, which is what lets redkite ship with
23
+ // no dependencies. These are the ways that can fail on a config it cannot read.
24
+ // A SyntaxError is the same class of failure: a package without "type":
25
+ // "module" makes a .ts file CommonJS, where an import statement is not legal
26
+ const LOADER = new Set([
27
+ "ERR_UNKNOWN_FILE_EXTENSION",
28
+ "ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX",
29
+ "ERR_MODULE_NOT_FOUND",
30
+ "ERR_INVALID_MODULE_SPECIFIER",
31
+ ]);
32
+
33
+ export async function loadConfig(explicit?: string): Promise<Deployment> {
34
+ const path = explicit ? resolve(explicit) : discover(process.cwd());
35
+ const config = defaultOf(await load(path), path) as Deployment;
36
+
37
+ const beside = await loadEnvironments(dirname(path));
38
+ const named = await loadNamed(manifestOf(explicit ? dirname(path) : process.cwd()));
39
+
40
+ for (const name of Object.keys(named)) {
41
+ if (!beside[name]) continue;
42
+
43
+ throw new Error(
44
+ `${name} is named by package.json and also sits beside the deployment. ` +
45
+ "An environment comes from one place or the other",
46
+ );
47
+ }
48
+
49
+ return { ...rooted(config, dirname(path)), environments: { ...beside, ...named } };
50
+ }
51
+
52
+ // A source path belongs to the file that named it, not to wherever the command
53
+ // was run. Resolving it here is what lets a deploy from a workspace and one
54
+ // from the root build the same tree
55
+ function rooted(config: Deployment, directory: string): Deployment {
56
+ if (!config.apps.some((app) => app.path)) return config;
57
+
58
+ return {
59
+ ...config,
60
+ apps: config.apps.map((app) =>
61
+ app.path ? { ...app, path: resolve(directory, app.path) } : app,
62
+ ),
63
+ };
64
+ }
65
+
66
+ // package.json may name the file each environment lives in, for a repository
67
+ // that keeps them somewhere the naming convention would not find them
68
+ async function loadNamed(manifest: Manifest | undefined) {
69
+ const declared = manifest?.redkite.environments;
70
+ if (!manifest || !declared) return {};
71
+
72
+ const environments: Record<string, Environment> = {};
73
+
74
+ for (const [name, where] of Object.entries(declared)) {
75
+ if (typeof where !== "string") {
76
+ throw new Error(`package.json names ${name} as something other than a path`);
77
+ }
78
+
79
+ const path = resolve(manifest.root, where);
80
+ if (!existsSync(path)) {
81
+ throw new Error(`package.json points ${name} at ${path}, which is not there`);
82
+ }
83
+
84
+ environments[name] = defaultOf(await load(path), path) as Environment;
85
+ }
86
+
87
+ return environments;
88
+ }
89
+
90
+ // A deployment is one file at the root of the project, and a deploy is as
91
+ // likely to be run from a workspace inside it as from there
92
+ export function discover(from: string): string {
93
+ let directory = from;
94
+
95
+ for (;;) {
96
+ const declared = directoryFrom(directory);
97
+
98
+ // Saying where the files are and not putting them there is a mistake worth
99
+ // stopping for, rather than a reason to keep looking further up
100
+ if (declared) return found(declared) ?? missing(declared, directory);
101
+
102
+ const here = found(directory);
103
+ if (here) return here;
104
+
105
+ const parent = dirname(directory);
106
+ if (parent === directory) break;
107
+
108
+ directory = parent;
109
+ }
110
+
111
+ throw new Error(
112
+ `No redkite.config.ts found in ${from} or any directory above it. A deployment ` +
113
+ 'is one file at the root of the project, or wherever package.json\'s ' +
114
+ '"redkite": { "directory": … } says it is',
115
+ );
116
+ }
117
+
118
+ // Every environment that lives in a file of its own, keyed by the name in it
119
+ export async function loadEnvironments(directory: string) {
120
+ const environments: Record<string, Environment> = {};
121
+ const seen: Record<string, string> = {};
122
+
123
+ for (const entry of readdirSync(directory).sort()) {
124
+ const name = PER_ENVIRONMENT.exec(entry)?.[1];
125
+ if (!name) continue;
126
+
127
+ const first = seen[name];
128
+ if (first) {
129
+ throw new Error(`${name} is defined by both ${first} and ${entry}`);
130
+ }
131
+
132
+ seen[name] = entry;
133
+ const path = join(directory, entry);
134
+ environments[name] = defaultOf(await load(path), path) as Environment;
135
+ }
136
+
137
+ return environments;
138
+ }
139
+
140
+ function found(directory: string) {
141
+ for (const candidate of CANDIDATES) {
142
+ const path = join(directory, candidate);
143
+ if (existsSync(path)) return path;
144
+ }
145
+
146
+ return undefined;
147
+ }
148
+
149
+ function missing(declared: string, from: string): never {
150
+ throw new Error(
151
+ `${join(from, "package.json")} points redkite at ${declared}, which holds no ` +
152
+ `redkite.config.${EXTENSIONS.join(", redkite.config.")}`,
153
+ );
154
+ }
155
+
156
+ // What a package.json says about redkite, and where it said it. The paths it
157
+ // names are read against its own directory rather than the working one
158
+ type Manifest = {
159
+ root: string;
160
+ redkite: { directory?: unknown; environments?: Record<string, unknown> };
161
+ };
162
+
163
+ function manifestAt(directory: string): Manifest | undefined {
164
+ const path = join(directory, "package.json");
165
+ if (!existsSync(path)) return undefined;
166
+
167
+ try {
168
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as {
169
+ redkite?: Manifest["redkite"];
170
+ };
171
+
172
+ return parsed.redkite ? { root: directory, redkite: parsed.redkite } : undefined;
173
+ } catch {
174
+ // A package.json that does not parse is not this tool's to complain about
175
+ return undefined;
176
+ }
177
+ }
178
+
179
+ // The nearest one above wherever this was run, which is the same walk the
180
+ // config itself is found by
181
+ function manifestOf(from: string) {
182
+ let directory = from;
183
+
184
+ for (;;) {
185
+ const found = manifestAt(directory);
186
+ if (found) return found;
187
+
188
+ const parent = dirname(directory);
189
+ if (parent === directory) return undefined;
190
+
191
+ directory = parent;
192
+ }
193
+ }
194
+
195
+ // package.json says where the deployment files live, for a repository that
196
+ // would rather not keep them at its root
197
+ function directoryFrom(directory: string) {
198
+ const declared = manifestAt(directory)?.redkite.directory;
199
+ return typeof declared === "string" ? resolve(directory, declared) : undefined;
200
+ }
201
+
202
+ function defaultOf(module: unknown, path: string) {
203
+ const found = module as { default?: { default?: unknown } };
204
+
205
+ // A config inside a CommonJS package transpiles to CommonJS, and Node hands
206
+ // back the whole module.exports as the default, wrapping the real one
207
+ const config = found.default?.default ?? found.default;
208
+ if (config) return config;
209
+
210
+ throw new Error(`${path} has no default export`);
211
+ }
212
+
213
+ async function load(path: string) {
214
+ const url = pathToFileURL(path).href;
215
+
216
+ try {
217
+ return await import(url);
218
+ } catch (error) {
219
+ if (!unreadable(error)) throw error;
220
+
221
+ // tsx resolves what Node will not: a path mapped by tsconfig, and the
222
+ // ./thing.js specifier TypeScript writes for a sibling ./thing.ts
223
+ const register = await tsxFrom(dirname(path));
224
+ if (!register) throw cannotRead(path, error);
225
+
226
+ const unregister = register();
227
+
228
+ try {
229
+ return await import(url);
230
+ } finally {
231
+ await unregister();
232
+ }
233
+ }
234
+ }
235
+
236
+ // Resolved from the project being deployed rather than depended on, so redkite
237
+ // installs as one package and still reads a config that needs more
238
+ async function tsxFrom(directory: string) {
239
+ try {
240
+ const require = createRequire(join(directory, "redkite.js"));
241
+ const api = (await import(
242
+ pathToFileURL(require.resolve("tsx/esm/api")).href
243
+ )) as { register: () => () => Promise<void> };
244
+
245
+ return api.register;
246
+ } catch {
247
+ return undefined;
248
+ }
249
+ }
250
+
251
+ function unreadable(error: unknown): error is Error {
252
+ if (!(error instanceof Error)) return false;
253
+ if (error instanceof SyntaxError) return true;
254
+
255
+ return "code" in error && typeof error.code === "string" && LOADER.has(error.code);
256
+ }
257
+
258
+ // Node reads TypeScript itself, within limits a config can run into. Each of
259
+ // them has a one line fix, and none of them is obvious from what Node throws
260
+ function cannotRead(path: string, error: Error) {
261
+ const remedies = [
262
+ ' · A package without "type": "module" makes a .ts file CommonJS, where',
263
+ " an import statement is not legal. Name it redkite.config.mts instead",
264
+ " · Node resolves a relative import by the file it names, so a sibling is",
265
+ " ./thing.ts rather than ./thing.js",
266
+ " · Node reads TypeScript from 22.18 on, and this is " + process.version,
267
+ " · Anything else: add tsx to this project and redkite will read the config",
268
+ " through it",
269
+ ];
270
+
271
+ return new Error(
272
+ `Could not read ${path}\n\n${error.message}\n\n${remedies.join("\n")}`,
273
+ { cause: error },
274
+ );
275
+ }