odoro 2.0.0 → 2.1.0

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.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ export { buildProject, reportBuild } from './chunk-DF67LADH.js';
3
+ import './chunk-HS7NGZLM.js';
4
+ import './chunk-RTWJ5GFB.js';
5
+ import './chunk-7ZB5MAM6.js';
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { extractEntries, cssProviderFor } from './chunk-OMHEQVIW.js';
3
- import { transformHtmlWith, sourcePlugin, esbuildPluginsFrom, applyAlias, isBareSpecifier, readSuffix, textModule, urlModule, workerModule } from './chunk-WWTDEV4Y.js';
2
+ import { extractEntries, cssProviderFor } from './chunk-HS7NGZLM.js';
3
+ import { transformHtmlWith, sourcePlugin, esbuildPluginsFrom, ASSET_EXTENSIONS, applyAlias, isBareSpecifier, readSuffix, textModule, urlModule, workerModule } from './chunk-RTWJ5GFB.js';
4
4
  import { info, colors, size, success, duration } from './chunk-7ZB5MAM6.js';
5
5
  import { existsSync } from 'fs';
6
6
  import { mkdir, writeFile, readFile, rm, cp, stat } from 'fs/promises';
@@ -630,20 +630,14 @@ async function buildProject(config) {
630
630
  "process.env.NODE_ENV": JSON.stringify("production"),
631
631
  ...config.define
632
632
  },
633
- loader: {
634
- ".svg": "file",
635
- ".png": "file",
636
- ".jpg": "file",
637
- ".jpeg": "file",
638
- ".gif": "file",
639
- ".webp": "file",
640
- ".avif": "file",
641
- ".ico": "file",
642
- ".woff": "file",
643
- ".woff2": "file",
644
- ".mp4": "file",
645
- ".webm": "file"
646
- },
633
+ /*
634
+ * La meme liste que celle du serveur de developpement, et non une copie.
635
+ *
636
+ * Les deux ont diverge : le `.ttf` manquait aux deux, et rien ne l aurait
637
+ * dit tant qu un gabarit n en embarquerait pas. Une seule liste ne peut
638
+ * plus se desaccorder d elle-meme.
639
+ */
640
+ loader: Object.fromEntries(ASSET_EXTENSIONS.map((suffixe) => [suffixe, "file"])),
647
641
  plugins: [
648
642
  // The order is that of resolution: the suffixes first, because they
649
643
  // change the target; the aliases next; the pass over the sources last, it
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { sourcePlugin, depFileName, SOURCE_EXTENSIONS, listen, openBrowser, INTERNAL_PREFIX, applyAlias, readSuffix, hasExtension, STYLE_EXTENSIONS, ASSET_EXTENSIONS, isBareSpecifier, DEPS_PREFIX, urlToFile, wantsStylesheet, wrapAsset, isAssetRequest, wrapJson, transformHtmlWith, textModule, urlModule, workerModule, fileToUrl, wrapStyle, transformModule } from './chunk-WWTDEV4Y.js';
2
+ import { sourcePlugin, depFileName, SOURCE_EXTENSIONS, listen, openBrowser, INTERNAL_PREFIX, applyAlias, readSuffix, hasExtension, STYLE_EXTENSIONS, ASSET_EXTENSIONS, isBareSpecifier, MIME, DEPS_PREFIX, urlToFile, wantsStylesheet, wrapAsset, isAssetRequest, wrapJson, sendFile, transformHtmlWith, textModule, urlModule, workerModule, fileToUrl, wrapStyle, transformModule } from './chunk-RTWJ5GFB.js';
3
3
  import { info, warn, success, duration, colors, error } from './chunk-7ZB5MAM6.js';
4
- import { existsSync, readFileSync, statSync, watch, createReadStream } from 'fs';
4
+ import { existsSync, readFileSync, statSync, watch } from 'fs';
5
5
  import { readFile, rm, mkdir, writeFile } from 'fs/promises';
6
6
  import { createServer, request } from 'http';
7
7
  import { createServer as createServer$1 } from 'https';
@@ -804,28 +804,6 @@ async function bundleRefreshRuntime() {
804
804
 
805
805
  // src/dev/server.ts
806
806
  var UTILITIES_PATH = `${INTERNAL_PREFIX}utilities.css`;
807
- var MIME = {
808
- ".html": "text/html; charset=utf-8",
809
- ".js": "text/javascript; charset=utf-8",
810
- ".mjs": "text/javascript; charset=utf-8",
811
- ".json": "application/json; charset=utf-8",
812
- ".css": "text/css; charset=utf-8",
813
- ".svg": "image/svg+xml",
814
- ".png": "image/png",
815
- ".jpg": "image/jpeg",
816
- ".jpeg": "image/jpeg",
817
- ".gif": "image/gif",
818
- ".webp": "image/webp",
819
- ".avif": "image/avif",
820
- ".ico": "image/x-icon",
821
- ".woff": "font/woff",
822
- ".woff2": "font/woff2",
823
- ".mp4": "video/mp4",
824
- ".webm": "video/webm",
825
- ".txt": "text/plain; charset=utf-8",
826
- ".md": "text/markdown; charset=utf-8",
827
- ".markdown": "text/markdown; charset=utf-8"
828
- };
829
807
  var SCRIPT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
830
808
  function cleanUrl(url) {
831
809
  return (url.split("?")[0] ?? url).split("#")[0] ?? url;
@@ -944,12 +922,12 @@ async function startDevServer(config) {
944
922
  }
945
923
  node.code = hotPreamble(url2) + body;
946
924
  }
947
- send(response, node.code, MIME[".js"]);
925
+ send(response, node.code, MIME[".js"] ?? "text/javascript");
948
926
  };
949
927
  const serveStyle = async (response, file, direct) => {
950
928
  const { css, files } = await assembleStylesheet(file, config.root);
951
929
  if (direct) {
952
- send(response, css, MIME[".css"]);
930
+ send(response, css, MIME[".css"] ?? "text/css");
953
931
  return;
954
932
  }
955
933
  const url2 = fileToUrl(file, config.root);
@@ -965,22 +943,22 @@ async function startDevServer(config) {
965
943
  dependency.selfAccepting = false;
966
944
  }
967
945
  }
968
- send(response, node.code, MIME[".js"]);
946
+ send(response, node.code, MIME[".js"] ?? "text/javascript");
969
947
  };
970
948
  const serveSuffixed = async (response, file, path, suffix) => {
971
949
  if (suffix === "raw") {
972
950
  send(
973
951
  response,
974
952
  textModule(await readFile(file, "utf8")),
975
- MIME[".js"]
953
+ MIME[".js"] ?? "text/javascript"
976
954
  );
977
955
  return;
978
956
  }
979
957
  if (suffix === "url") {
980
- send(response, urlModule(path), MIME[".js"]);
958
+ send(response, urlModule(path), MIME[".js"] ?? "text/javascript");
981
959
  return;
982
960
  }
983
- send(response, workerModule(path), MIME[".js"]);
961
+ send(response, workerModule(path), MIME[".js"] ?? "text/javascript");
984
962
  };
985
963
  const directoryIndex = (directory) => {
986
964
  if (!existsSync(directory) || !statSync(directory).isDirectory()) return void 0;
@@ -990,10 +968,8 @@ async function startDevServer(config) {
990
968
  }
991
969
  return void 0;
992
970
  };
993
- const serveFile = (response, file) => {
994
- const type = MIME[extname(file).toLowerCase()] ?? "application/octet-stream";
995
- response.writeHead(200, { "Content-Type": type, "Cache-Control": "no-cache" });
996
- createReadStream(file).pipe(response);
971
+ const serveFile = (response, file, incoming) => {
972
+ sendFile(response, file, { range: incoming?.headers.range });
997
973
  };
998
974
  const serveHtml = async (response, route = "/") => {
999
975
  const html = await readFile(indexFile, "utf8");
@@ -1002,7 +978,7 @@ async function startDevServer(config) {
1002
978
  injectClient(html, utilities !== void 0),
1003
979
  { dev: true, route }
1004
980
  );
1005
- send(response, rendered, MIME[".html"]);
981
+ send(response, rendered, MIME[".html"] ?? "text/html");
1006
982
  };
1007
983
  const forward = (incoming, response, target) => {
1008
984
  const url2 = new URL(incoming.url ?? "/", target);
@@ -1072,7 +1048,7 @@ async function startDevServer(config) {
1072
1048
  const name = last.endsWith(".js") ? last : depFileName(last);
1073
1049
  const file2 = join(deps.directory, name);
1074
1050
  if (existsSync(file2)) {
1075
- serveFile(response, file2);
1051
+ serveFile(response, file2, incoming);
1076
1052
  return;
1077
1053
  }
1078
1054
  send(
@@ -1107,7 +1083,7 @@ async function startDevServer(config) {
1107
1083
  if (url2.includes("?import")) {
1108
1084
  send(response, wrapAsset(path), MIME[".js"] ?? "text/javascript");
1109
1085
  } else {
1110
- serveFile(response, file);
1086
+ serveFile(response, file, incoming);
1111
1087
  }
1112
1088
  return;
1113
1089
  }
@@ -1120,21 +1096,21 @@ async function startDevServer(config) {
1120
1096
  const json = await readFile(file, "utf8");
1121
1097
  send(response, wrapJson(json), MIME[".js"] ?? "text/javascript");
1122
1098
  } else {
1123
- serveFile(response, file);
1099
+ serveFile(response, file, incoming);
1124
1100
  }
1125
1101
  return;
1126
1102
  }
1127
- serveFile(response, file);
1103
+ serveFile(response, file, incoming);
1128
1104
  return;
1129
1105
  }
1130
1106
  const publicFile = join(config.publicDir, path.replace(/^\//, ""));
1131
1107
  if (existsSync(publicFile) && statSync(publicFile).isFile()) {
1132
- serveFile(response, publicFile);
1108
+ serveFile(response, publicFile, incoming);
1133
1109
  return;
1134
1110
  }
1135
1111
  const index = directoryIndex(publicFile);
1136
1112
  if (index !== void 0) {
1137
- serveFile(response, index);
1113
+ serveFile(response, index, incoming);
1138
1114
  return;
1139
1115
  }
1140
1116
  if (!extname(path) && !isAssetRequest(incoming.headers)) {
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile, appendFile, mkdir, chmod } from 'fs/promises';
3
+ import { existsSync } from 'fs';
4
+ import { join, dirname } from 'path';
5
+ import { platform, homedir } from 'os';
6
+ import { randomUUID } from 'crypto';
7
+
8
+ function checkDatabaseUrl(value) {
9
+ const url = value.trim();
10
+ if (url.length === 0) return "The URL cannot be empty.";
11
+ if (!/^postgres(ql)?:\/\//i.test(url)) {
12
+ return "A PostgreSQL URL starts with postgres:// or postgresql://";
13
+ }
14
+ let parsed;
15
+ try {
16
+ parsed = new URL(url);
17
+ } catch {
18
+ return "This string is not a valid URL.";
19
+ }
20
+ if (parsed.hostname.length === 0) return "No host in this URL.";
21
+ if (parsed.pathname.replace(/^\//, "").length === 0) {
22
+ return "No database name: add it after the port, for example /my_project";
23
+ }
24
+ if (!/^(localhost|127\.0\.0\.1|\[::1\])$/i.test(parsed.hostname) && parsed.searchParams.get("sslmode") === "disable") {
25
+ return "sslmode=disable on a remote database: the traffic would go in the clear.";
26
+ }
27
+ return void 0;
28
+ }
29
+ async function writeDatabaseUrl(target, url) {
30
+ const path = join(target, ".env");
31
+ const line = `DATABASE_URL=${url}
32
+ `;
33
+ if (!existsSync(path)) {
34
+ const example = join(target, ".env.example");
35
+ const base = existsSync(example) ? await readFile(example, "utf8") : "";
36
+ await writeFile(path, base.replace(/^DATABASE_URL=.*$/m, line.trimEnd()), "utf8");
37
+ if (!base.includes("DATABASE_URL=")) await appendFile(path, line, "utf8");
38
+ return;
39
+ }
40
+ const current = await readFile(path, "utf8");
41
+ await writeFile(
42
+ path,
43
+ current.includes("DATABASE_URL=") ? current.replace(/^DATABASE_URL=.*$/m, line.trimEnd()) : current + line,
44
+ "utf8"
45
+ );
46
+ }
47
+ async function assertEnvIgnored(target) {
48
+ const path = join(target, ".gitignore");
49
+ if (!existsSync(path)) {
50
+ return "No .gitignore: the .env file risks being versioned.";
51
+ }
52
+ const content = await readFile(path, "utf8");
53
+ const ignored = content.split("\n").map((line) => line.trim()).some((line) => line === ".env" || line === ".env*" || line === "*.env");
54
+ return ignored ? void 0 : ".env does not appear in the .gitignore: your credentials risk being versioned.";
55
+ }
56
+ var PROVIDER_PENDING = [
57
+ "Provisioning goes through @odoro-cli/cloud-sdk, installed separately:",
58
+ "",
59
+ " npm install --save-dev @odoro-cli/cloud-sdk",
60
+ " odoro db:login",
61
+ " odoro db:create --env production",
62
+ "",
63
+ "It does not ship with odoro: this binary is downloaded on every project creation, and most projects use their own database."
64
+ ].join("\n");
65
+
66
+ // src/db/sdk.ts
67
+ var SDK_PACKAGE = "@odoro-cli/cloud-sdk";
68
+ async function loadSdk() {
69
+ try {
70
+ const specifier = SDK_PACKAGE;
71
+ const sdk = await import(specifier);
72
+ return { ok: true, sdk };
73
+ } catch {
74
+ return {
75
+ ok: false,
76
+ reason: [
77
+ `${SDK_PACKAGE} could not be loaded.`,
78
+ "",
79
+ "It ships with odoro, so this is usually a partial install:",
80
+ "",
81
+ " npm install",
82
+ "",
83
+ "Until it loads, a project can still bring its own PostgreSQL URL."
84
+ ].join("\n")
85
+ };
86
+ }
87
+ }
88
+ function configPath(env = processEnv()) {
89
+ const xdg = env["XDG_CONFIG_HOME"];
90
+ if (xdg !== void 0 && xdg.length > 0) return join(xdg, "odoro", "config.json");
91
+ if (platform() === "win32") {
92
+ const appData = env["APPDATA"];
93
+ if (appData !== void 0 && appData.length > 0) {
94
+ return join(appData, "odoro", "config.json");
95
+ }
96
+ }
97
+ return join(homedir(), ".config", "odoro", "config.json");
98
+ }
99
+ function processEnv() {
100
+ return process.env;
101
+ }
102
+ async function readUserConfig(path = configPath()) {
103
+ try {
104
+ return JSON.parse(await readFile(path, "utf8"));
105
+ } catch {
106
+ return {};
107
+ }
108
+ }
109
+ async function writeUserConfig(config, path = configPath()) {
110
+ await mkdir(dirname(path), { recursive: true });
111
+ await writeFile(path, `${JSON.stringify(config, null, 2)}
112
+ `, "utf8");
113
+ if (platform() === "win32") return { path, restricted: false };
114
+ try {
115
+ await chmod(path, 384);
116
+ return { path, restricted: true };
117
+ } catch {
118
+ return { path, restricted: false };
119
+ }
120
+ }
121
+ async function storeToken(apiUrl, token, path = configPath()) {
122
+ const current = await readUserConfig(path);
123
+ return await writeUserConfig(
124
+ {
125
+ ...current,
126
+ tokens: { ...current.tokens, [apiUrl]: token },
127
+ defaultApiUrl: current.defaultApiUrl ?? apiUrl
128
+ },
129
+ path
130
+ );
131
+ }
132
+ async function findToken(apiUrl, path = configPath(), env = processEnv()) {
133
+ const fromEnv = env["ODORO_TOKEN"];
134
+ if (fromEnv !== void 0 && fromEnv.length > 0) return fromEnv;
135
+ const config = await readUserConfig(path);
136
+ return config.tokens?.[apiUrl];
137
+ }
138
+ var DEFAULT_API_URL = "https://db.odoro.dev";
139
+ function idempotencyKey() {
140
+ return randomUUID();
141
+ }
142
+ var CANCELLED = /* @__PURE__ */ Symbol("cancelled");
143
+ function cancelled(value) {
144
+ return value === CANCELLED;
145
+ }
146
+ async function ensureToken(prompts, apiUrl) {
147
+ const known = await findToken(apiUrl);
148
+ if (known !== void 0) return known;
149
+ prompts.log.info(`No token yet for ${apiUrl}. Create one at ${apiUrl}/tokens.`);
150
+ const value = await prompts.password({
151
+ message: "Platform token",
152
+ validate: (input) => input.startsWith("odk_") ? void 0 : "A token starts with odk_live_ or odk_test_"
153
+ });
154
+ if (prompts.isCancel(value)) return CANCELLED;
155
+ const report = await storeToken(apiUrl, value);
156
+ prompts.log.success(`Token recorded in ${report.path}`);
157
+ return value;
158
+ }
159
+ async function chooseEnvironment(prompts, client) {
160
+ const { projects } = await client.projects.list();
161
+ const all = projects.flatMap(
162
+ (project) => project.environments.map((environment) => ({
163
+ id: environment.id,
164
+ label: `${project.name} / ${environment.name}`
165
+ }))
166
+ );
167
+ if (all.length === 0) {
168
+ prompts.log.warn(
169
+ `No environment on this account. Create a project at ${DEFAULT_API_URL}, then run \`odoro db:create\`.`
170
+ );
171
+ return void 0;
172
+ }
173
+ const only = all[0];
174
+ if (all.length === 1 && only !== void 0) {
175
+ prompts.log.info(`Environment: ${only.label}`);
176
+ return only;
177
+ }
178
+ const picked = await prompts.select({
179
+ message: "Environment",
180
+ options: all.map((entry) => ({ value: entry.id, label: entry.label }))
181
+ });
182
+ if (prompts.isCancel(picked)) return CANCELLED;
183
+ return all.find((entry) => entry.id === picked);
184
+ }
185
+ async function chooseRegion(prompts, client) {
186
+ const { regions } = await client.regions.list();
187
+ if (regions.length === 0) return void 0;
188
+ const preferred = regions.includes("eu-central-1") ? "eu-central-1" : regions[0];
189
+ const picked = await prompts.select({
190
+ message: "Region",
191
+ initialValue: preferred,
192
+ options: regions.map((region) => ({ value: region, label: region }))
193
+ });
194
+ if (prompts.isCancel(picked)) return CANCELLED;
195
+ return picked;
196
+ }
197
+ async function provision(input) {
198
+ const abort = new AbortController();
199
+ const onInterrupt = () => abort.abort();
200
+ process.once("SIGINT", onInterrupt);
201
+ try {
202
+ const finished = await input.client.databases.createAndWait(
203
+ {
204
+ idempotencyKey: idempotencyKey(),
205
+ environmentId: input.environmentId,
206
+ ...input.name === void 0 ? {} : { name: input.name },
207
+ ...input.region === void 0 ? {} : { region: input.region }
208
+ },
209
+ { signal: abort.signal }
210
+ );
211
+ if (finished.state !== "succeeded") {
212
+ return {
213
+ ok: false,
214
+ reason: finished.error ?? `The operation ended in state "${finished.state}". \`odoro db:status\` will show where it stands.`
215
+ };
216
+ }
217
+ const databaseId = finished.subject;
218
+ if (databaseId === void 0) {
219
+ return {
220
+ ok: false,
221
+ reason: "The operation succeeded without naming the database it created. `odoro db:status` will show it."
222
+ };
223
+ }
224
+ const { connectionString } = await input.client.credentials.rotate({ databaseId });
225
+ return { ok: true, databaseId, connectionString };
226
+ } finally {
227
+ process.removeListener("SIGINT", onInterrupt);
228
+ }
229
+ }
230
+
231
+ export { DEFAULT_API_URL, PROVIDER_PENDING, SDK_PACKAGE, assertEnvIgnored, cancelled, checkDatabaseUrl, chooseEnvironment, chooseRegion, ensureToken, findToken, loadSdk, provision, storeToken, writeDatabaseUrl };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from 'child_process';
3
- import { existsSync, statSync, readdirSync } from 'fs';
2
+ import { statSync, createReadStream, existsSync, readdirSync } from 'fs';
4
3
  import { resolve, dirname, extname, relative, join } from 'path';
4
+ import { spawn } from 'child_process';
5
5
  import { readFile } from 'fs/promises';
6
6
  import { build } from 'esbuild';
7
7
 
@@ -76,6 +76,105 @@ async function listen(server, port, host, attempts = ATTEMPTS) {
76
76
  `[odoro] No free port between ${String(port)} and ${String(port + attempts - 1)}.`
77
77
  );
78
78
  }
79
+ var MIME = {
80
+ ".html": "text/html; charset=utf-8",
81
+ ".js": "text/javascript; charset=utf-8",
82
+ ".mjs": "text/javascript; charset=utf-8",
83
+ ".css": "text/css; charset=utf-8",
84
+ ".json": "application/json; charset=utf-8",
85
+ ".map": "application/json; charset=utf-8",
86
+ ".txt": "text/plain; charset=utf-8",
87
+ ".md": "text/markdown; charset=utf-8",
88
+ ".markdown": "text/markdown; charset=utf-8",
89
+ ".xml": "application/xml; charset=utf-8",
90
+ ".webmanifest": "application/manifest+json",
91
+ ".svg": "image/svg+xml",
92
+ ".png": "image/png",
93
+ ".jpg": "image/jpeg",
94
+ ".jpeg": "image/jpeg",
95
+ ".gif": "image/gif",
96
+ ".webp": "image/webp",
97
+ ".avif": "image/avif",
98
+ ".ico": "image/x-icon",
99
+ ".woff": "font/woff",
100
+ ".woff2": "font/woff2",
101
+ ".ttf": "font/ttf",
102
+ ".otf": "font/otf",
103
+ ".mp4": "video/mp4",
104
+ ".m4v": "video/mp4",
105
+ ".webm": "video/webm",
106
+ ".mov": "video/quicktime",
107
+ ".ogv": "video/ogg",
108
+ ".mp3": "audio/mpeg",
109
+ ".m4a": "audio/mp4",
110
+ ".wav": "audio/wav",
111
+ ".oga": "audio/ogg",
112
+ ".glb": "model/gltf-binary",
113
+ ".gltf": "model/gltf+json",
114
+ ".pdf": "application/pdf",
115
+ ".wasm": "application/wasm"
116
+ };
117
+ function mimeOf(file) {
118
+ return MIME[extname(file).toLowerCase()] ?? "application/octet-stream";
119
+ }
120
+ function parseRange(header, size) {
121
+ if (header === void 0) return null;
122
+ const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
123
+ if (match === null) return null;
124
+ const [, rawStart, rawEnd] = match;
125
+ if (rawStart === "" && rawEnd === "") return null;
126
+ if (rawStart === "") {
127
+ const length = Number(rawEnd);
128
+ if (!Number.isFinite(length) || length <= 0) return "unsatisfiable";
129
+ return { start: Math.max(0, size - length), end: size - 1 };
130
+ }
131
+ const start = Number(rawStart);
132
+ if (!Number.isFinite(start) || start >= size) return "unsatisfiable";
133
+ const end = rawEnd === "" ? size - 1 : Math.min(Number(rawEnd), size - 1);
134
+ if (!Number.isFinite(end) || end < start) return "unsatisfiable";
135
+ return { start, end };
136
+ }
137
+ function sendFile(response, file, options = {}) {
138
+ const size = statSync(file).size;
139
+ const type = mimeOf(file);
140
+ const cacheControl = options.cacheControl ?? "no-cache";
141
+ const range = parseRange(options.range, size);
142
+ if (range === "unsatisfiable") {
143
+ response.writeHead(416, {
144
+ "Content-Range": `bytes */${String(size)}`,
145
+ "Accept-Ranges": "bytes",
146
+ "Content-Type": "text/plain; charset=utf-8"
147
+ });
148
+ response.end("Range not satisfiable");
149
+ return;
150
+ }
151
+ if (range === null) {
152
+ response.writeHead(200, {
153
+ "Content-Type": type,
154
+ "Content-Length": String(size),
155
+ "Accept-Ranges": "bytes",
156
+ "Cache-Control": cacheControl
157
+ });
158
+ if (options.headOnly === true) {
159
+ response.end();
160
+ return;
161
+ }
162
+ createReadStream(file).pipe(response);
163
+ return;
164
+ }
165
+ response.writeHead(206, {
166
+ "Content-Type": type,
167
+ "Content-Length": String(range.end - range.start + 1),
168
+ "Content-Range": `bytes ${String(range.start)}-${String(range.end)}/${String(size)}`,
169
+ "Accept-Ranges": "bytes",
170
+ "Cache-Control": cacheControl
171
+ });
172
+ if (options.headOnly === true) {
173
+ response.end();
174
+ return;
175
+ }
176
+ createReadStream(file, { start: range.start, end: range.end }).pipe(response);
177
+ }
79
178
  function openBrowser(url) {
80
179
  const [command, ...arguments_] = process.platform === "win32" ? (
81
180
  // `start` is a built-in command: it goes through the interpreter. The
@@ -347,6 +446,7 @@ var INTERNAL_PREFIX = "/@odoro/";
347
446
  var SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
348
447
  var STYLE_EXTENSIONS = [".css"];
349
448
  var ASSET_EXTENSIONS = [
449
+ // Images
350
450
  ".svg",
351
451
  ".png",
352
452
  ".jpg",
@@ -355,10 +455,35 @@ var ASSET_EXTENSIONS = [
355
455
  ".webp",
356
456
  ".avif",
357
457
  ".ico",
458
+ /*
459
+ * Polices — les quatre formats, pas seulement le moderne.
460
+ *
461
+ * Un gabarit qui embarque ses coupes statiques les livre souvent en `.ttf` :
462
+ * la version variable est en `woff2`, les instances ne le sont pas toujours.
463
+ * Sans le `.ttf` ici, la construction s arrete sur « No loader is configured
464
+ * for ".ttf" files » — ce qui au moins se voit, contrairement a la plupart
465
+ * des lacunes de ce fichier.
466
+ */
358
467
  ".woff",
359
468
  ".woff2",
469
+ ".ttf",
470
+ ".otf",
471
+ ".eot",
472
+ // Video et audio
360
473
  ".mp4",
361
- ".webm"
474
+ ".m4v",
475
+ ".webm",
476
+ ".mov",
477
+ ".ogv",
478
+ ".mp3",
479
+ ".m4a",
480
+ ".wav",
481
+ ".oga",
482
+ // Ce qu une scene demande
483
+ ".glb",
484
+ ".gltf",
485
+ ".hdr",
486
+ ".pdf"
362
487
  ];
363
488
  function depFileName(specifier) {
364
489
  return `${specifier.replace(/^@/, "").split("/").join("_")}.js`;
@@ -601,4 +726,4 @@ function wrapAsset(url) {
601
726
  `;
602
727
  }
603
728
 
604
- export { ASSET_EXTENSIONS, DEPS_PREFIX, INTERNAL_PREFIX, SOURCE_EXTENSIONS, STYLE_EXTENSIONS, applyAlias, depFileName, esbuildPluginsFrom, fileToUrl, hasExtension, hasGlob, isAssetRequest, isBareSpecifier, listen, openBrowser, readSuffix, sourcePlugin, textModule, transformGlob, transformHtmlWith, transformModule, urlModule, urlToFile, wantsStylesheet, workerModule, wrapAsset, wrapJson, wrapStyle };
729
+ export { ASSET_EXTENSIONS, DEPS_PREFIX, INTERNAL_PREFIX, MIME, SOURCE_EXTENSIONS, STYLE_EXTENSIONS, applyAlias, depFileName, esbuildPluginsFrom, fileToUrl, hasExtension, hasGlob, isAssetRequest, isBareSpecifier, listen, openBrowser, readSuffix, sendFile, sourcePlugin, textModule, transformGlob, transformHtmlWith, transformModule, urlModule, urlToFile, wantsStylesheet, workerModule, wrapAsset, wrapJson, wrapStyle };
@@ -1,26 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { isAssetRequest, listen, openBrowser } from './chunk-WWTDEV4Y.js';
2
+ import { isAssetRequest, sendFile, listen, openBrowser } from './chunk-RTWJ5GFB.js';
3
3
  import { warn, success, info, colors } from './chunk-7ZB5MAM6.js';
4
- import { existsSync, statSync, createReadStream } from 'fs';
4
+ import { existsSync, statSync } from 'fs';
5
5
  import { createServer } from 'http';
6
6
  import { normalize, join, extname } from 'path';
7
7
 
8
- var MIME = {
9
- ".html": "text/html; charset=utf-8",
10
- ".js": "text/javascript; charset=utf-8",
11
- ".css": "text/css; charset=utf-8",
12
- ".json": "application/json; charset=utf-8",
13
- ".svg": "image/svg+xml",
14
- ".png": "image/png",
15
- ".jpg": "image/jpeg",
16
- ".webp": "image/webp",
17
- ".ico": "image/x-icon",
18
- ".woff2": "font/woff2",
19
- ".map": "application/json; charset=utf-8",
20
- ".txt": "text/plain; charset=utf-8",
21
- ".md": "text/markdown; charset=utf-8",
22
- ".markdown": "text/markdown; charset=utf-8"
23
- };
24
8
  async function startPreviewServer(config, port = config.server.port + 1) {
25
9
  if (!existsSync(config.outDir)) {
26
10
  throw new Error(`[odoro] Nothing to preview: run "odoro build" first.`);
@@ -42,13 +26,12 @@ async function startPreviewServer(config, port = config.server.port + 1) {
42
26
  response.end("Forbidden");
43
27
  return;
44
28
  }
45
- const type = MIME[extname(file).toLowerCase()] ?? "application/octet-stream";
46
29
  const immutable = relativePath.startsWith("assets/");
47
- response.writeHead(200, {
48
- "Content-Type": type,
49
- "Cache-Control": immutable ? "public, max-age=31536000, immutable" : "no-cache"
30
+ sendFile(response, file, {
31
+ cacheControl: immutable ? "public, max-age=31536000, immutable" : "no-cache",
32
+ range: incoming.headers.range,
33
+ headOnly: incoming.method === "HEAD"
50
34
  });
51
- createReadStream(file).pipe(response);
52
35
  });
53
36
  const { port: obtained, requested } = await listen(
54
37
  server,
package/dist/cli.js CHANGED
@@ -89,6 +89,7 @@ ${colors.bold("create options")}
89
89
 
90
90
  ${colors.bold("Database options")}
91
91
  --env <name> Target environment (production, staging, preview-42)
92
+ --region <name> Where the database lives; asked when absent
92
93
  --from <env> Environment to branch from
93
94
  --name <name> Name of the branch
94
95
  --api <url> Root of the API, instead of the default one
@@ -148,7 +149,7 @@ function rootFrom(flags, positional) {
148
149
  async function run(argv) {
149
150
  const { command, positional, flags } = parseArgs(argv);
150
151
  if (flags["version"] === true) {
151
- const manifest = await import('./package-T4Z7OBOH.js');
152
+ const manifest = await import('./package-3ZJOEXM4.js');
152
153
  console.log(manifest.default.version);
153
154
  return 0;
154
155
  }
@@ -159,7 +160,7 @@ async function run(argv) {
159
160
  switch (command) {
160
161
  case "create":
161
162
  case "new": {
162
- const { createCommand } = await import('./create-QX5LEWMS.js');
163
+ const { createCommand } = await import('./create-B2TLV6XI.js');
163
164
  const options = {};
164
165
  if (positional[0] !== void 0) options.name = positional[0];
165
166
  if (typeof flags["template"] === "string") options.template = flags["template"];
@@ -173,7 +174,7 @@ async function run(argv) {
173
174
  return createCommand(options);
174
175
  }
175
176
  case "dev": {
176
- const { startDevServer } = await import('./server-2WP562EV.js');
177
+ const { startDevServer } = await import('./server-ZB3P3EGS.js');
177
178
  const config = await loadConfig(
178
179
  rootFrom(flags, positional),
179
180
  overridesFrom(flags),
@@ -183,14 +184,14 @@ async function run(argv) {
183
184
  return new Promise(() => void 0);
184
185
  }
185
186
  case "build": {
186
- const { buildProject, reportBuild } = await import('./build-LZAUDJST.js');
187
+ const { buildProject, reportBuild } = await import('./build-XJIP4GNT.js');
187
188
  const config = await loadConfig(rootFrom(flags, positional), overridesFrom(flags));
188
189
  const output = await buildProject(config);
189
190
  reportBuild(output, process.cwd());
190
191
  return 0;
191
192
  }
192
193
  case "preview": {
193
- const { startPreviewServer } = await import('./preview-P4WY5GEY.js');
194
+ const { startPreviewServer } = await import('./preview-3IRG7MHV.js');
194
195
  const config = await loadConfig(rootFrom(flags, positional), overridesFrom(flags));
195
196
  const port = numberFlag(flags, "port");
196
197
  await startPreviewServer(config, port);
@@ -217,11 +218,12 @@ async function run(argv) {
217
218
  case "db:status":
218
219
  case "db:create":
219
220
  case "db:branch": {
220
- const db = await import('./commands-EUGVLQWL.js');
221
+ const db = await import('./commands-UOWN3IK4.js');
221
222
  const options = {
222
223
  root: typeof flags["root"] === "string" ? flags["root"] : process.cwd(),
223
224
  ...typeof flags["api"] === "string" ? { apiUrl: flags["api"] } : {},
224
225
  ...typeof flags["env"] === "string" ? { env: flags["env"] } : {},
226
+ ...typeof flags["region"] === "string" ? { region: flags["region"] } : {},
225
227
  yes: flags["yes"] === true
226
228
  };
227
229
  if (command === "db:login") return db.loginCommand(options);
@@ -1,83 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { writeDatabaseUrl, assertEnvIgnored } from './chunk-P3DJITWE.js';
2
+ import { storeToken, chooseEnvironment, cancelled, chooseRegion, writeDatabaseUrl, assertEnvIgnored, SDK_PACKAGE, loadSdk, findToken } from './chunk-LIKVHWB4.js';
3
3
  import { info, success, colors, warn, error } from './chunk-7ZB5MAM6.js';
4
4
  import { randomBytes } from 'crypto';
5
- import { readFile, mkdir, writeFile, chmod } from 'fs/promises';
6
- import { platform, homedir } from 'os';
7
- import { dirname, join } from 'path';
8
5
 
9
- function configPath(env = processEnv()) {
10
- const xdg = env["XDG_CONFIG_HOME"];
11
- if (xdg !== void 0 && xdg.length > 0) return join(xdg, "odoro", "config.json");
12
- if (platform() === "win32") {
13
- const appData = env["APPDATA"];
14
- if (appData !== void 0 && appData.length > 0) {
15
- return join(appData, "odoro", "config.json");
16
- }
17
- }
18
- return join(homedir(), ".config", "odoro", "config.json");
19
- }
20
- function processEnv() {
21
- return process.env;
22
- }
23
- async function readUserConfig(path = configPath()) {
24
- try {
25
- return JSON.parse(await readFile(path, "utf8"));
26
- } catch {
27
- return {};
28
- }
29
- }
30
- async function writeUserConfig(config, path = configPath()) {
31
- await mkdir(dirname(path), { recursive: true });
32
- await writeFile(path, `${JSON.stringify(config, null, 2)}
33
- `, "utf8");
34
- if (platform() === "win32") return { path, restricted: false };
35
- try {
36
- await chmod(path, 384);
37
- return { path, restricted: true };
38
- } catch {
39
- return { path, restricted: false };
40
- }
41
- }
42
- async function storeToken(apiUrl, token, path = configPath()) {
43
- const current = await readUserConfig(path);
44
- return await writeUserConfig(
45
- {
46
- ...current,
47
- tokens: { ...current.tokens, [apiUrl]: token },
48
- defaultApiUrl: current.defaultApiUrl ?? apiUrl
49
- },
50
- path
51
- );
52
- }
53
- async function findToken(apiUrl, path = configPath(), env = processEnv()) {
54
- const fromEnv = env["ODORO_TOKEN"];
55
- if (fromEnv !== void 0 && fromEnv.length > 0) return fromEnv;
56
- const config = await readUserConfig(path);
57
- return config.tokens?.[apiUrl];
58
- }
59
-
60
- // src/db/sdk.ts
61
- var SDK_PACKAGE = "@odoro-cli/cloud-sdk";
62
- async function loadSdk() {
63
- try {
64
- const specifier = SDK_PACKAGE;
65
- const sdk = await import(specifier);
66
- return { ok: true, sdk };
67
- } catch {
68
- return {
69
- ok: false,
70
- reason: `This command needs ${SDK_PACKAGE}, which is not installed.
71
-
72
- npm install --save-dev ${SDK_PACKAGE}
73
-
74
- It does not ship with odoro: this binary is downloaded on every
75
- project creation, and most projects do not use the platform.`
76
- };
77
- }
78
- }
79
-
80
- // src/db/commands.ts
81
6
  var DEFAULT_API_URL = "https://db.odoro.dev";
82
7
  function idempotencyKey() {
83
8
  return randomBytes(24).toString("hex");
@@ -142,20 +67,31 @@ async function statusCommand(options) {
142
67
  async function createCommand(options) {
143
68
  const connection = await connect(options);
144
69
  if (connection === void 0) return 1;
145
- const environmentId = options.env;
70
+ const prompts = await import('@clack/prompts');
71
+ let environmentId = options.env;
146
72
  if (environmentId === void 0) {
147
- error("Name the environment: `odoro db:create --env production`.");
148
- return 1;
73
+ const chosen = await chooseEnvironment(prompts, connection.client);
74
+ if (cancelled(chosen) || chosen === void 0) return 1;
75
+ environmentId = chosen.id;
76
+ }
77
+ let region = options.region;
78
+ if (region === void 0) {
79
+ const chosen = await chooseRegion(prompts, connection.client);
80
+ if (cancelled(chosen)) return 1;
81
+ region = chosen;
149
82
  }
150
- const prompts = await import('@clack/prompts');
151
83
  const spinner = prompts.spinner();
152
- spinner.start("Provisioning");
84
+ spinner.start(`Provisioning in ${region ?? "the default region"}`);
153
85
  const abort = new AbortController();
154
86
  const onInterrupt = () => abort.abort();
155
87
  process.once("SIGINT", onInterrupt);
156
88
  try {
157
89
  const finished = await connection.client.databases.createAndWait(
158
- { idempotencyKey: idempotencyKey(), environmentId, region: "eu-central-1" },
90
+ {
91
+ idempotencyKey: idempotencyKey(),
92
+ environmentId,
93
+ ...region === void 0 ? {} : { region }
94
+ },
159
95
  { signal: abort.signal }
160
96
  );
161
97
  spinner.stop("Database provisioned");
@@ -235,7 +171,8 @@ function describe(cause, sdk) {
235
171
  var DB_HELP = [
236
172
  " db:login Record a platform token",
237
173
  " db:status List the databases of the project",
238
- " db:create --env <e> Provision a database and write .env",
174
+ " db:create Provision a database and write .env",
175
+ " (--env and --region skip the questions)",
239
176
  " db:branch --from <e> --name <n>",
240
177
  " Create a per-branch preview",
241
178
  ` (requires ${SDK_PACKAGE})`
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { writeDatabaseUrl, assertEnvIgnored, PROVIDER_PENDING, checkDatabaseUrl } from './chunk-P3DJITWE.js';
2
+ import { writeDatabaseUrl, assertEnvIgnored, PROVIDER_PENDING, checkDatabaseUrl, loadSdk, ensureToken, DEFAULT_API_URL, cancelled, chooseEnvironment, chooseRegion, provision } from './chunk-LIKVHWB4.js';
3
3
  import { execSync, spawn } from 'child_process';
4
4
  import { existsSync, statSync, readdirSync, readFileSync } from 'fs';
5
5
  import { resolve, basename, dirname, join } from 'path';
@@ -146,10 +146,10 @@ function availableTemplates(root = templatesRoot()) {
146
146
  var FAMILY_VERSIONS = {
147
147
  "@odoro-cli/engine": "2.0.0",
148
148
  "@odoro-cli/icons": "2.0.0",
149
- "@odoro-cli/libs": "2.0.0",
149
+ "@odoro-cli/libs": "2.1.0",
150
150
  "@odoro-cli/server": "1.1.0",
151
- "create-odoro": "1.0.10",
152
- "odoro": "2.0.0"
151
+ "create-odoro": "1.0.11",
152
+ "odoro": "2.1.0"
153
153
  };
154
154
 
155
155
  // src/scaffold/scaffold.ts
@@ -279,29 +279,63 @@ var TEMPLATE_LABELS = {
279
279
  "react-ts": "Single-page app \u2014 React, TypeScript, Odoro router and motion",
280
280
  "react-ts-server": "Client and server \u2014 the same app, plus a modular @odoro-cli/server base and a Dockerfile"
281
281
  };
282
- async function askDatabase() {
282
+ async function provisionOnPlatform(projectName) {
283
+ const load = await loadSdk();
284
+ if (!load.ok) {
285
+ prompts.log.warn(load.reason);
286
+ return void 0;
287
+ }
288
+ const token = await ensureToken(prompts, DEFAULT_API_URL);
289
+ if (cancelled(token)) return void 0;
290
+ const client = load.sdk.createClient({ baseUrl: DEFAULT_API_URL, token });
291
+ const environment = await chooseEnvironment(prompts, client);
292
+ if (cancelled(environment) || environment === void 0) return void 0;
293
+ const region = await chooseRegion(prompts, client);
294
+ if (cancelled(region)) return void 0;
295
+ const spinner2 = prompts.spinner();
296
+ spinner2.start(`Provisioning in ${region ?? "the default region"}`);
297
+ const result = await provision({
298
+ client,
299
+ environmentId: environment.id,
300
+ ...region === void 0 ? {} : { region },
301
+ name: projectName
302
+ });
303
+ if (!result.ok) {
304
+ spinner2.stop("Provisioning did not finish");
305
+ prompts.log.warn(result.reason);
306
+ return void 0;
307
+ }
308
+ spinner2.stop(`Database ready in ${environment.label}`);
309
+ return {
310
+ choice: "provider",
311
+ url: result.connectionString,
312
+ note: `Database provisioned on ${environment.label}. DATABASE_URL is in .env \u2014 do not commit it.`
313
+ };
314
+ }
315
+ async function askDatabase(projectName) {
283
316
  const choice = ensure(
284
317
  await prompts.select({
285
318
  message: "Database",
286
- initialValue: "url",
319
+ initialValue: "provider",
287
320
  options: [
288
321
  {
289
322
  value: "provider",
290
323
  label: "Odoro provider",
291
- hint: "automatic provisioning \u2014 coming soon"
324
+ hint: "provisioned now, PostgreSQL, region of your choosing"
292
325
  },
293
326
  {
294
327
  value: "url",
295
328
  label: "Existing PostgreSQL URL",
296
- hint: "Neon, Supabase, RDS, your own"
329
+ hint: "bring your own"
297
330
  },
298
331
  { value: "later", label: "Set up later", hint: ".env.example only" }
299
332
  ]
300
333
  })
301
334
  );
302
335
  if (choice === "provider") {
303
- prompts.log.warn(PROVIDER_PENDING);
304
- return { choice, note: "Run `odoro db:create` as soon as the platform is there." };
336
+ const provisioned = await provisionOnPlatform(projectName);
337
+ if (provisioned !== void 0) return provisioned;
338
+ return { choice, note: PROVIDER_PENDING };
305
339
  }
306
340
  if (choice === "later") {
307
341
  return {
@@ -485,7 +519,7 @@ async function createCommand(options) {
485
519
  prompts.cancel(`Unknown package manager: "${manager}".`);
486
520
  return 1;
487
521
  }
488
- const database = template === "react-ts-server" && options.yes !== true ? await askDatabase() : void 0;
522
+ const database = template === "react-ts-server" && options.yes !== true ? await askDatabase(packageName) : void 0;
489
523
  const withGit = options.git ?? (options.yes === true ? true : ensure(await prompts.confirm({ message: "Initialize a git repository?" })));
490
524
  const withInstall = options.install ?? (options.yes === true ? true : ensure(
491
525
  await prompts.confirm({
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- export { startPreviewServer } from './chunk-PRWVZ2SM.js';
2
+ export { startPreviewServer } from './chunk-TONYIKCN.js';
3
3
  export { clientEnv, defineConfig, loadConfig, loadEnv } from './chunk-QOVBLN7A.js';
4
4
  import './chunk-T6RLHSCW.js';
5
- export { buildManifest, buildProject, chunksFor, prerender, reportBuild } from './chunk-Q2KCMTB5.js';
6
- export { ModuleGraph, detectSelfAccepting, optimizeDeps, scanDependencies, startDevServer } from './chunk-OMHEQVIW.js';
7
- export { depFileName, hasGlob, transformGlob } from './chunk-WWTDEV4Y.js';
5
+ export { buildManifest, buildProject, chunksFor, prerender, reportBuild } from './chunk-DF67LADH.js';
6
+ export { ModuleGraph, detectSelfAccepting, optimizeDeps, scanDependencies, startDevServer } from './chunk-HS7NGZLM.js';
7
+ export { depFileName, hasGlob, transformGlob } from './chunk-RTWJ5GFB.js';
8
8
  import './chunk-7ZB5MAM6.js';
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "odoro",
5
- version: "2.0.0",
5
+ version: "2.1.0",
6
6
  type: "module",
7
7
  license: "UNLICENSED",
8
8
  author: "BouBouw",
@@ -47,6 +47,7 @@ var package_default = {
47
47
  dependencies: {
48
48
  "@babel/core": "^7.29.7",
49
49
  "@clack/prompts": "^0.11.0",
50
+ "@odoro-cli/cloud-sdk": "^0.1.0",
50
51
  esbuild: "^0.28.2",
51
52
  picocolors: "^1.1.1",
52
53
  "react-refresh": "^0.18.0"
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ export { startPreviewServer } from './chunk-TONYIKCN.js';
3
+ import './chunk-RTWJ5GFB.js';
4
+ import './chunk-7ZB5MAM6.js';
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- export { extractEntries, injectClient, startDevServer } from './chunk-OMHEQVIW.js';
3
- import './chunk-WWTDEV4Y.js';
2
+ export { extractEntries, injectClient, startDevServer } from './chunk-HS7NGZLM.js';
3
+ import './chunk-RTWJ5GFB.js';
4
4
  import './chunk-7ZB5MAM6.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "odoro",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "author": "BouBouw",
@@ -37,6 +37,7 @@
37
37
  "dependencies": {
38
38
  "@babel/core": "^7.29.7",
39
39
  "@clack/prompts": "^0.11.0",
40
+ "@odoro-cli/cloud-sdk": "^0.1.0",
40
41
  "esbuild": "^0.28.2",
41
42
  "picocolors": "^1.1.1",
42
43
  "react-refresh": "^0.18.0"
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- export { buildProject, reportBuild } from './chunk-Q2KCMTB5.js';
3
- import './chunk-OMHEQVIW.js';
4
- import './chunk-WWTDEV4Y.js';
5
- import './chunk-7ZB5MAM6.js';
@@ -1,64 +0,0 @@
1
- #!/usr/bin/env node
2
- import { readFile, writeFile, appendFile } from 'fs/promises';
3
- import { existsSync } from 'fs';
4
- import { join } from 'path';
5
-
6
- function checkDatabaseUrl(value) {
7
- const url = value.trim();
8
- if (url.length === 0) return "The URL cannot be empty.";
9
- if (!/^postgres(ql)?:\/\//i.test(url)) {
10
- return "A PostgreSQL URL starts with postgres:// or postgresql://";
11
- }
12
- let parsed;
13
- try {
14
- parsed = new URL(url);
15
- } catch {
16
- return "This string is not a valid URL.";
17
- }
18
- if (parsed.hostname.length === 0) return "No host in this URL.";
19
- if (parsed.pathname.replace(/^\//, "").length === 0) {
20
- return "No database name: add it after the port, for example /my_project";
21
- }
22
- if (!/^(localhost|127\.0\.0\.1|\[::1\])$/i.test(parsed.hostname) && parsed.searchParams.get("sslmode") === "disable") {
23
- return "sslmode=disable on a remote database: the traffic would go in the clear.";
24
- }
25
- return void 0;
26
- }
27
- async function writeDatabaseUrl(target, url) {
28
- const path = join(target, ".env");
29
- const line = `DATABASE_URL=${url}
30
- `;
31
- if (!existsSync(path)) {
32
- const example = join(target, ".env.example");
33
- const base = existsSync(example) ? await readFile(example, "utf8") : "";
34
- await writeFile(path, base.replace(/^DATABASE_URL=.*$/m, line.trimEnd()), "utf8");
35
- if (!base.includes("DATABASE_URL=")) await appendFile(path, line, "utf8");
36
- return;
37
- }
38
- const current = await readFile(path, "utf8");
39
- await writeFile(
40
- path,
41
- current.includes("DATABASE_URL=") ? current.replace(/^DATABASE_URL=.*$/m, line.trimEnd()) : current + line,
42
- "utf8"
43
- );
44
- }
45
- async function assertEnvIgnored(target) {
46
- const path = join(target, ".gitignore");
47
- if (!existsSync(path)) {
48
- return "No .gitignore: the .env file risks being versioned.";
49
- }
50
- const content = await readFile(path, "utf8");
51
- const ignored = content.split("\n").map((line) => line.trim()).some((line) => line === ".env" || line === ".env*" || line === "*.env");
52
- return ignored ? void 0 : ".env does not appear in the .gitignore: your credentials risk being versioned.";
53
- }
54
- var PROVIDER_PENDING = [
55
- "Provisioning goes through @odoro-cli/cloud-sdk, installed separately:",
56
- "",
57
- " npm install --save-dev @odoro-cli/cloud-sdk",
58
- " odoro db:login",
59
- " odoro db:create --env production",
60
- "",
61
- "It does not ship with odoro: this binary is downloaded on every project creation, and most projects use their own database."
62
- ].join("\n");
63
-
64
- export { PROVIDER_PENDING, assertEnvIgnored, checkDatabaseUrl, writeDatabaseUrl };
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- export { startPreviewServer } from './chunk-PRWVZ2SM.js';
3
- import './chunk-WWTDEV4Y.js';
4
- import './chunk-7ZB5MAM6.js';