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.
- package/LICENSE +21 -0
- package/README.md +819 -0
- package/bin/redkite.mjs +12 -0
- package/dist/build.d.ts +27 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +132 -0
- package/dist/build.js.map +1 -0
- package/dist/checks.d.ts +4 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +49 -0
- package/dist/checks.js.map +1 -0
- package/dist/cli/agent.d.ts +2 -0
- package/dist/cli/agent.d.ts.map +1 -0
- package/dist/cli/agent.js +24 -0
- package/dist/cli/agent.js.map +1 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +217 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +11 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +438 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/log.d.ts +9 -0
- package/dist/cli/log.d.ts.map +1 -0
- package/dist/cli/log.js +128 -0
- package/dist/cli/log.js.map +1 -0
- package/dist/cli/screen.d.ts +34 -0
- package/dist/cli/screen.d.ts.map +1 -0
- package/dist/cli/screen.js +298 -0
- package/dist/cli/screen.js.map +1 -0
- package/dist/cli/viewer.d.ts +9 -0
- package/dist/cli/viewer.d.ts.map +1 -0
- package/dist/cli/viewer.js +139 -0
- package/dist/cli/viewer.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +79 -0
- package/dist/config.js.map +1 -0
- package/dist/deploy.d.ts +19 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +275 -0
- package/dist/deploy.js.map +1 -0
- package/dist/docker.d.ts +96 -0
- package/dist/docker.d.ts.map +1 -0
- package/dist/docker.js +325 -0
- package/dist/docker.js.map +1 -0
- package/dist/dockerfile.d.ts +14 -0
- package/dist/dockerfile.d.ts.map +1 -0
- package/dist/dockerfile.js +160 -0
- package/dist/dockerfile.js.map +1 -0
- package/dist/health.d.ts +13 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +54 -0
- package/dist/health.js.map +1 -0
- package/dist/host.d.ts +21 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +27 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +5 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +45 -0
- package/dist/layout.js.map +1 -0
- package/dist/localHost.d.ts +7 -0
- package/dist/localHost.d.ts.map +1 -0
- package/dist/localHost.js +32 -0
- package/dist/localHost.js.map +1 -0
- package/dist/log.d.ts +14 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +15 -0
- package/dist/log.js.map +1 -0
- package/dist/nginx.d.ts +4 -0
- package/dist/nginx.d.ts.map +1 -0
- package/dist/nginx.js +53 -0
- package/dist/nginx.js.map +1 -0
- package/dist/pipeline.d.ts +103 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +142 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/presets/index.d.ts +3 -0
- package/dist/presets/index.d.ts.map +1 -0
- package/dist/presets/index.js +3 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/presets/nextApp.d.ts +15 -0
- package/dist/presets/nextApp.d.ts.map +1 -0
- package/dist/presets/nextApp.js +53 -0
- package/dist/presets/nextApp.js.map +1 -0
- package/dist/presets/nodeApp.d.ts +19 -0
- package/dist/presets/nodeApp.d.ts.map +1 -0
- package/dist/presets/nodeApp.js +29 -0
- package/dist/presets/nodeApp.js.map +1 -0
- package/dist/secrets/bitwarden.d.ts +3 -0
- package/dist/secrets/bitwarden.d.ts.map +1 -0
- package/dist/secrets/bitwarden.js +6 -0
- package/dist/secrets/bitwarden.js.map +1 -0
- package/dist/secrets/refs.d.ts +9 -0
- package/dist/secrets/refs.d.ts.map +1 -0
- package/dist/secrets/refs.js +20 -0
- package/dist/secrets/refs.js.map +1 -0
- package/dist/secrets/store.d.ts +9 -0
- package/dist/secrets/store.d.ts.map +1 -0
- package/dist/secrets/store.js +104 -0
- package/dist/secrets/store.js.map +1 -0
- package/dist/services/ensure.d.ts +18 -0
- package/dist/services/ensure.d.ts.map +1 -0
- package/dist/services/ensure.js +93 -0
- package/dist/services/ensure.js.map +1 -0
- package/dist/services/index.d.ts +19 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +27 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/planned.d.ts +20 -0
- package/dist/services/planned.d.ts.map +1 -0
- package/dist/services/planned.js +77 -0
- package/dist/services/planned.js.map +1 -0
- package/dist/shell.d.ts +11 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +65 -0
- package/dist/shell.js.map +1 -0
- package/dist/source.d.ts +16 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +108 -0
- package/dist/source.js.map +1 -0
- package/dist/sshHost.d.ts +15 -0
- package/dist/sshHost.d.ts.map +1 -0
- package/dist/sshHost.js +91 -0
- package/dist/sshHost.js.map +1 -0
- package/dist/steps.d.ts +27 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/steps.js +69 -0
- package/dist/steps.js.map +1 -0
- package/dist/topology.d.ts +40 -0
- package/dist/topology.d.ts.map +1 -0
- package/dist/topology.js +113 -0
- package/dist/topology.js.map +1 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -0
- package/src/build.ts +236 -0
- package/src/checks.ts +65 -0
- package/src/cli/agent.ts +28 -0
- package/src/cli/config.ts +275 -0
- package/src/cli/index.ts +559 -0
- package/src/cli/log.ts +168 -0
- package/src/cli/screen.ts +409 -0
- package/src/cli/viewer.ts +192 -0
- package/src/config.ts +90 -0
- package/src/deploy.ts +409 -0
- package/src/docker.ts +433 -0
- package/src/dockerfile.ts +228 -0
- package/src/health.ts +83 -0
- package/src/host.ts +54 -0
- package/src/index.ts +25 -0
- package/src/layout.ts +41 -0
- package/src/localHost.ts +50 -0
- package/src/log.ts +38 -0
- package/src/nginx.ts +63 -0
- package/src/pipeline.ts +335 -0
- package/src/presets/index.ts +2 -0
- package/src/presets/nextApp.ts +70 -0
- package/src/presets/nodeApp.ts +46 -0
- package/src/secrets/bitwarden.ts +7 -0
- package/src/secrets/refs.ts +32 -0
- package/src/secrets/store.ts +147 -0
- package/src/services/ensure.ts +160 -0
- package/src/services/index.ts +48 -0
- package/src/services/planned.ts +119 -0
- package/src/shell.ts +95 -0
- package/src/source.ts +171 -0
- package/src/sshHost.ts +145 -0
- package/src/steps.ts +101 -0
- package/src/topology.ts +199 -0
- 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
|
+
}
|