@domesystems/templates 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/dome-templates.mjs
CHANGED
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import readline from "node:readline/promises";
|
|
5
6
|
import { readManifest } from "./manifest.mjs";
|
|
6
7
|
import { imageForRuntime, renderCompose } from "./runtime.mjs";
|
|
7
8
|
import { copyTemplate, initializeEnvironment } from "./template-files.mjs";
|
|
@@ -67,7 +68,11 @@ function init(slug, options, output) {
|
|
|
67
68
|
copyTemplate(source, target);
|
|
68
69
|
const environmentCreated = initializeEnvironment(target);
|
|
69
70
|
output(`Initialized ${slug} in ${target}.`);
|
|
70
|
-
if (environmentCreated)
|
|
71
|
+
if (environmentCreated) {
|
|
72
|
+
output("Created .env from .env.example.");
|
|
73
|
+
output("When you run `up`, Dome will prompt for import credentials; they are never written to .env.");
|
|
74
|
+
output("After import, `up` writes DOME_TOKEN and DOME_GATEWAY_URL into .env. Add any other template-specific runtime values before starting.");
|
|
75
|
+
}
|
|
71
76
|
return target;
|
|
72
77
|
}
|
|
73
78
|
|
|
@@ -83,7 +88,117 @@ function run(command, args, cwd) {
|
|
|
83
88
|
if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} exited with status ${result.status}.`);
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
|
|
91
|
+
function parseFinalJSON(output, command) {
|
|
92
|
+
const starts = [...output.matchAll(/(?:^|\n)\{/g)].map((match) => match.index + (match[0][0] === "\n" ? 1 : 0));
|
|
93
|
+
for (const start of starts.reverse()) {
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(output.slice(start));
|
|
96
|
+
} catch {
|
|
97
|
+
// The CLI may have printed an interactive prompt before its final JSON result.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`${command} completed without a readable JSON result.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function runJSON(command, args, cwd, { displayOutput = true } = {}) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const child = spawn(command, args, { cwd, stdio: ["inherit", "pipe", "inherit"] });
|
|
106
|
+
let output = "";
|
|
107
|
+
child.stdout.on("data", (chunk) => {
|
|
108
|
+
const text = chunk.toString();
|
|
109
|
+
output += text;
|
|
110
|
+
if (displayOutput) process.stdout.write(text);
|
|
111
|
+
});
|
|
112
|
+
child.on("error", (error) => reject(new Error(`Could not run ${command}: ${error.message}`)));
|
|
113
|
+
child.on("close", (status) => {
|
|
114
|
+
if (status !== 0) {
|
|
115
|
+
reject(new Error(`${command} ${args.join(" ")} exited with status ${status}.`));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
resolve(parseFinalJSON(output, command));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
reject(error);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function replaceEnvironmentValue(source, name, value) {
|
|
128
|
+
const expression = new RegExp(`^${name}=.*$`, "m");
|
|
129
|
+
if (!expression.test(source)) throw new Error(`The runtime .env is missing ${name}.`);
|
|
130
|
+
return source.replace(expression, `${name}=${JSON.stringify(value)}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function undefinedEnvironmentValues(source) {
|
|
134
|
+
return source
|
|
135
|
+
.split(/\r?\n/)
|
|
136
|
+
.map((line) => line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(?:\s*|""|''|undefined)$/)?.[1])
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function fillRuntimeEnvironment(target, output) {
|
|
141
|
+
const environmentPath = path.join(target, ".env");
|
|
142
|
+
let environment = fs.readFileSync(environmentPath, "utf8");
|
|
143
|
+
const missing = undefinedEnvironmentValues(environment);
|
|
144
|
+
if (missing.length === 0) return;
|
|
145
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
146
|
+
throw new Error(`Missing runtime .env values: ${missing.join(", ")}. Set them before starting Docker.`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
output(`Complete ${environmentPath} before starting the app.`);
|
|
150
|
+
const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
151
|
+
const write = terminal._writeToOutput.bind(terminal);
|
|
152
|
+
terminal._writeToOutput = (text) => {
|
|
153
|
+
if (text === "\n" || text === "\r\n") write(text);
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
for (const name of missing) {
|
|
157
|
+
const value = (await terminal.question(`Enter ${name}: `)).trim();
|
|
158
|
+
if (!value) throw new Error(`${name} is required to start the app.`);
|
|
159
|
+
environment = replaceEnvironmentValue(environment, name, value);
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
terminal.close();
|
|
163
|
+
}
|
|
164
|
+
fs.writeFileSync(environmentPath, environment);
|
|
165
|
+
output(`Saved runtime values to ${environmentPath}.`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function gatewayURL(response) {
|
|
169
|
+
return response.data?.gateway?.endpoints?.gatewayUrl ?? response.data?.gateway?.endpoints?.gateway_url;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function writeRuntimeEnvironment(target, manifest, importResponse, output) {
|
|
173
|
+
const jobID = importResponse.data?.job?.id;
|
|
174
|
+
if (!jobID) throw new Error("Dome import succeeded but did not return an import job ID.");
|
|
175
|
+
|
|
176
|
+
const gatewayName = manifest.catalog?.gateway?.name;
|
|
177
|
+
if (!gatewayName) throw new Error("template.yaml must declare catalog.gateway.name to configure the runtime.");
|
|
178
|
+
|
|
179
|
+
const environmentPath = path.join(target, ".env");
|
|
180
|
+
let environment = fs.readFileSync(environmentPath, "utf8");
|
|
181
|
+
// Validate the local contract before consuming a one-time secret.
|
|
182
|
+
for (const name of ["DOME_TOKEN", "DOME_GATEWAY_URL"]) {
|
|
183
|
+
if (!new RegExp(`^${name}=.*$`, "m").test(environment)) throw new Error(`The runtime .env is missing ${name}.`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const gateway = await runJSON("dome", ["gateways", "get", gatewayName, "--format", "json"], target, { displayOutput: false });
|
|
187
|
+
const url = gatewayURL(gateway);
|
|
188
|
+
if (!url) throw new Error(`Gateway ${gatewayName} has no runtime endpoint.`);
|
|
189
|
+
|
|
190
|
+
const revealed = await runJSON("dome", ["import", "outputs", jobID, "--format", "json"], target, { displayOutput: false });
|
|
191
|
+
const outputs = revealed.data?.outputs ?? {};
|
|
192
|
+
const tokens = Object.entries(outputs).filter(([name, value]) => name.endsWith("_token") && value);
|
|
193
|
+
if (tokens.length !== 1) throw new Error("The import must produce exactly one *_token output to configure DOME_TOKEN.");
|
|
194
|
+
|
|
195
|
+
environment = replaceEnvironmentValue(environment, "DOME_TOKEN", tokens[0][1]);
|
|
196
|
+
environment = replaceEnvironmentValue(environment, "DOME_GATEWAY_URL", url);
|
|
197
|
+
fs.writeFileSync(environmentPath, environment);
|
|
198
|
+
output(`Configured ${environmentPath} with the one-time agent token and Gateway URL.`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function main(argv, { output = console.log, error = console.error } = {}) {
|
|
87
202
|
try {
|
|
88
203
|
const parsed = parseArguments(argv);
|
|
89
204
|
if (parsed.help) {
|
|
@@ -102,8 +217,12 @@ export function main(argv, { output = console.log, error = console.error } = {})
|
|
|
102
217
|
fs.writeFileSync(composePath, renderCompose(manifest.app.runtime));
|
|
103
218
|
output(`Generated ${path.basename(composePath)} using ${imageForRuntime(manifest.app.runtime)}.`);
|
|
104
219
|
|
|
105
|
-
if (options.provision)
|
|
220
|
+
if (options.provision) {
|
|
221
|
+
const imported = await runJSON("dome", ["import", "dome.tf", "--format", "json"], target);
|
|
222
|
+
await writeRuntimeEnvironment(target, manifest, imported, output);
|
|
223
|
+
}
|
|
106
224
|
if (options.start) {
|
|
225
|
+
await fillRuntimeEnvironment(target, output);
|
|
107
226
|
const args = ["compose", "-f", ".dome-compose.yaml", "up"];
|
|
108
227
|
if (options.detach) args.push("--detach");
|
|
109
228
|
run("docker", args, target);
|