relight-cli 0.1.0 → 0.3.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.
- package/README.md +77 -34
- package/package.json +12 -4
- package/src/cli.js +350 -1
- package/src/commands/apps.js +128 -0
- package/src/commands/auth.js +13 -4
- package/src/commands/config.js +282 -0
- package/src/commands/cost.js +593 -0
- package/src/commands/db.js +775 -0
- package/src/commands/deploy.js +264 -0
- package/src/commands/doctor.js +69 -13
- package/src/commands/domains.js +223 -0
- package/src/commands/logs.js +111 -0
- package/src/commands/open.js +42 -0
- package/src/commands/ps.js +121 -0
- package/src/commands/scale.js +132 -0
- package/src/commands/service.js +227 -0
- package/src/lib/clouds/aws.js +309 -35
- package/src/lib/clouds/cf.js +401 -2
- package/src/lib/clouds/gcp.js +255 -4
- package/src/lib/clouds/neon.js +147 -0
- package/src/lib/clouds/slicervm.js +139 -0
- package/src/lib/config.js +200 -2
- package/src/lib/docker.js +34 -0
- package/src/lib/link.js +31 -5
- package/src/lib/providers/aws/app.js +481 -0
- package/src/lib/providers/aws/db.js +504 -0
- package/src/lib/providers/aws/dns.js +232 -0
- package/src/lib/providers/aws/registry.js +59 -0
- package/src/lib/providers/cf/app.js +596 -0
- package/src/lib/providers/cf/bundle.js +70 -0
- package/src/lib/providers/cf/db.js +181 -0
- package/src/lib/providers/cf/dns.js +148 -0
- package/src/lib/providers/cf/registry.js +17 -0
- package/src/lib/providers/gcp/app.js +429 -0
- package/src/lib/providers/gcp/db.js +372 -0
- package/src/lib/providers/gcp/dns.js +166 -0
- package/src/lib/providers/gcp/registry.js +30 -0
- package/src/lib/providers/neon/db.js +306 -0
- package/src/lib/providers/resolve.js +79 -0
- package/src/lib/providers/slicervm/app.js +396 -0
- package/src/lib/providers/slicervm/db.js +33 -0
- package/src/lib/providers/slicervm/dns.js +58 -0
- package/src/lib/providers/slicervm/registry.js +7 -0
- package/worker-template/package.json +10 -0
- package/worker-template/src/index.js +260 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { Container } from "@cloudflare/containers";
|
|
2
|
+
|
|
3
|
+
export class AppContainer extends Container {
|
|
4
|
+
enableInternet = true;
|
|
5
|
+
|
|
6
|
+
constructor(ctx, env) {
|
|
7
|
+
super(ctx, env);
|
|
8
|
+
var appConfig = JSON.parse(env.RELIGHT_APP_CONFIG);
|
|
9
|
+
this.defaultPort = appConfig.port || 8080;
|
|
10
|
+
this.sleepAfter = appConfig.sleepAfter || "30s";
|
|
11
|
+
|
|
12
|
+
// Read env vars from native bindings (new format)
|
|
13
|
+
var envVars = {};
|
|
14
|
+
var allKeys = [...(appConfig.envKeys || []), ...(appConfig.secretKeys || [])];
|
|
15
|
+
if (allKeys.length > 0) {
|
|
16
|
+
for (var key of allKeys) {
|
|
17
|
+
if (env[key] !== undefined) envVars[key] = env[key];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// Backward compat: merge appConfig.env for old-format configs or plain values
|
|
21
|
+
if (appConfig.env) {
|
|
22
|
+
for (var key of Object.keys(appConfig.env)) {
|
|
23
|
+
if (envVars[key] === undefined) envVars[key] = appConfig.env[key];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
this.envVars = envVars;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
async fetch(request, env) {
|
|
32
|
+
var appConfig = JSON.parse(env.RELIGHT_APP_CONFIG);
|
|
33
|
+
|
|
34
|
+
// Hrana protocol handler - only active when D1 binding exists
|
|
35
|
+
if (env.DB) {
|
|
36
|
+
var url = new URL(request.url);
|
|
37
|
+
var path = url.pathname.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
38
|
+
if (request.method === "POST" && path === "/v2/pipeline") {
|
|
39
|
+
return handleHranaPipeline(request, env, appConfig);
|
|
40
|
+
}
|
|
41
|
+
if (request.method === "GET" && (path === "/v2" || path === "/v3")) {
|
|
42
|
+
return new Response("ok");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
var regions = appConfig.regions;
|
|
47
|
+
var instances = appConfig.instances || 2;
|
|
48
|
+
|
|
49
|
+
// Geo-aware routing: use Cloudflare's request.cf to pick closest region
|
|
50
|
+
var cf = request.cf || {};
|
|
51
|
+
var region = pickRegion(regions, cf);
|
|
52
|
+
|
|
53
|
+
var binding = env.APP_CONTAINER;
|
|
54
|
+
|
|
55
|
+
// Route to a random instance in the selected region
|
|
56
|
+
var idx = Math.floor(Math.random() * instances);
|
|
57
|
+
var objectId = binding.idFromName(region + "-" + idx);
|
|
58
|
+
var container = binding.get(objectId, { locationHint: region });
|
|
59
|
+
|
|
60
|
+
var response = await container.fetch(request);
|
|
61
|
+
|
|
62
|
+
// Also set on response for external observability (curl -I, devtools)
|
|
63
|
+
var headers = new Headers(response.headers);
|
|
64
|
+
headers.set("X-Relight-Region", region);
|
|
65
|
+
|
|
66
|
+
return response;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// Country -> preferred location hints (ordered by proximity)
|
|
72
|
+
// Uses request.cf.country (ISO 3166-1 alpha-2)
|
|
73
|
+
var COUNTRY_PREFERENCES = {
|
|
74
|
+
// Eastern Europe -> eeur first
|
|
75
|
+
PL: ["eeur", "weur"], CZ: ["eeur", "weur"], SK: ["eeur", "weur"],
|
|
76
|
+
HU: ["eeur", "weur"], RO: ["eeur", "weur"], BG: ["eeur", "weur"],
|
|
77
|
+
UA: ["eeur", "weur"], LT: ["eeur", "weur"], LV: ["eeur", "weur"],
|
|
78
|
+
EE: ["eeur", "weur"], HR: ["eeur", "weur"], SI: ["eeur", "weur"],
|
|
79
|
+
RS: ["eeur", "weur"], BA: ["eeur", "weur"], MK: ["eeur", "weur"],
|
|
80
|
+
AL: ["eeur", "weur"], ME: ["eeur", "weur"], MD: ["eeur", "weur"],
|
|
81
|
+
BY: ["eeur", "weur"], FI: ["eeur", "weur"], GR: ["eeur", "weur", "me"],
|
|
82
|
+
// Turkey - between eeur and me
|
|
83
|
+
TR: ["eeur", "me", "weur"],
|
|
84
|
+
// Middle East (CF puts these in AS continent)
|
|
85
|
+
AE: ["me", "eeur", "apac"], SA: ["me", "afr", "eeur"],
|
|
86
|
+
QA: ["me", "eeur", "apac"], BH: ["me", "eeur", "apac"],
|
|
87
|
+
KW: ["me", "eeur"], OM: ["me", "apac"], IL: ["me", "eeur"],
|
|
88
|
+
JO: ["me", "eeur"], LB: ["me", "eeur"], IQ: ["me", "eeur"],
|
|
89
|
+
IR: ["me", "apac", "eeur"], YE: ["me", "afr"],
|
|
90
|
+
// South Asia -> apac, but me as second choice
|
|
91
|
+
IN: ["apac", "me"], PK: ["apac", "me"], BD: ["apac", "me"],
|
|
92
|
+
LK: ["apac", "me"],
|
|
93
|
+
// North Africa - closer to Europe/ME than sub-Saharan Africa
|
|
94
|
+
EG: ["me", "eeur", "afr"], LY: ["afr", "me", "weur"],
|
|
95
|
+
TN: ["afr", "weur", "me"], DZ: ["afr", "weur"],
|
|
96
|
+
MA: ["afr", "weur"],
|
|
97
|
+
// Mexico / Central America -> wnam
|
|
98
|
+
MX: ["wnam", "enam", "sam"],
|
|
99
|
+
// Australia / NZ -> explicit oc
|
|
100
|
+
AU: ["oc", "apac"], NZ: ["oc", "apac"],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Fallback: continent -> preferred location hints
|
|
104
|
+
var CONTINENT_PREFERENCES = {
|
|
105
|
+
NA: ["enam", "wnam", "sam"],
|
|
106
|
+
SA: ["sam", "enam", "wnam"],
|
|
107
|
+
EU: ["weur", "eeur"],
|
|
108
|
+
AS: ["apac", "me", "eeur"],
|
|
109
|
+
OC: ["oc", "apac"],
|
|
110
|
+
AF: ["afr", "me", "weur"],
|
|
111
|
+
AN: ["oc", "sam", "apac"],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Pick the closest region from the app's deployed regions.
|
|
116
|
+
* Checks country first (request.cf.country), falls back to continent.
|
|
117
|
+
*/
|
|
118
|
+
function pickRegion(regions, cf) {
|
|
119
|
+
if (regions.length === 1) return regions[0];
|
|
120
|
+
if (!cf) return regions[0];
|
|
121
|
+
|
|
122
|
+
// try country-level match first
|
|
123
|
+
var preferences = COUNTRY_PREFERENCES[cf.country] || CONTINENT_PREFERENCES[cf.continent];
|
|
124
|
+
if (!preferences) return regions[0];
|
|
125
|
+
|
|
126
|
+
for (var hint of preferences) {
|
|
127
|
+
if (regions.includes(hint)) return hint;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return regions[0];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
// --- Hrana protocol handler (libSQL/sqld HTTP v2) ---
|
|
135
|
+
|
|
136
|
+
async function handleHranaPipeline(request, env, appConfig) {
|
|
137
|
+
// Auth check - DB_TOKEN is a secret_text binding
|
|
138
|
+
var dbToken = env.DB_TOKEN;
|
|
139
|
+
if (dbToken) {
|
|
140
|
+
var auth = request.headers.get("Authorization") || "";
|
|
141
|
+
var token = auth.startsWith("Bearer ") ? auth.slice(7) : "";
|
|
142
|
+
if (token !== dbToken) {
|
|
143
|
+
return Response.json(hranaError("AUTH_FAILED", "Invalid or missing auth token"), { status: 401 });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
var body;
|
|
148
|
+
try {
|
|
149
|
+
body = await request.json();
|
|
150
|
+
} catch {
|
|
151
|
+
return Response.json(hranaError("REQUEST_INVALID", "Invalid JSON body"), { status: 400 });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
var results = [];
|
|
155
|
+
for (var req of (body.requests || [])) {
|
|
156
|
+
try {
|
|
157
|
+
if (req.type === "execute") {
|
|
158
|
+
var result = await executeStmt(env.DB, req.stmt);
|
|
159
|
+
results.push({ type: "ok", response: { type: "execute", result } });
|
|
160
|
+
} else if (req.type === "batch") {
|
|
161
|
+
var stmts = req.batch.steps.map(function (step) { return step.stmt; });
|
|
162
|
+
var prepared = stmts.map(function (s) { return bindStmt(env.DB, s); });
|
|
163
|
+
var d1Results = await env.DB.batch(prepared);
|
|
164
|
+
var batchResults = d1Results.map(function (d1r) {
|
|
165
|
+
return { type: "ok", response: { type: "execute", result: convertD1Result(d1r) } };
|
|
166
|
+
});
|
|
167
|
+
results.push({ type: "ok", response: { type: "batch", result: { step_results: batchResults, step_errors: batchResults.map(function () { return null; }) } } });
|
|
168
|
+
} else if (req.type === "close") {
|
|
169
|
+
results.push({ type: "ok", response: { type: "close" } });
|
|
170
|
+
} else {
|
|
171
|
+
results.push({ type: "ok", response: { type: "none" } });
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
results.push({ type: "error", error: { message: e.message, code: "STMT_ERROR" } });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return Response.json({ baton: null, base_url: null, results });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function hranaValueToJS(v) {
|
|
182
|
+
if (!v || v.type === "null") return null;
|
|
183
|
+
if (v.type === "integer") return Number(v.value);
|
|
184
|
+
if (v.type === "float") return Number(v.value);
|
|
185
|
+
if (v.type === "text") return v.value;
|
|
186
|
+
if (v.type === "blob") {
|
|
187
|
+
var bin = atob(v.base64);
|
|
188
|
+
var bytes = new Uint8Array(bin.length);
|
|
189
|
+
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
190
|
+
return bytes;
|
|
191
|
+
}
|
|
192
|
+
return v.value !== undefined ? v.value : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function jsValueToHrana(v) {
|
|
196
|
+
if (v === null || v === undefined) return { type: "null" };
|
|
197
|
+
if (typeof v === "number") {
|
|
198
|
+
return Number.isInteger(v)
|
|
199
|
+
? { type: "integer", value: String(v) }
|
|
200
|
+
: { type: "float", value: v };
|
|
201
|
+
}
|
|
202
|
+
if (typeof v === "string") return { type: "text", value: v };
|
|
203
|
+
if (v instanceof Uint8Array || v instanceof ArrayBuffer) {
|
|
204
|
+
var arr = v instanceof ArrayBuffer ? new Uint8Array(v) : v;
|
|
205
|
+
var s = "";
|
|
206
|
+
for (var i = 0; i < arr.length; i++) s += String.fromCharCode(arr[i]);
|
|
207
|
+
return { type: "blob", base64: btoa(s) };
|
|
208
|
+
}
|
|
209
|
+
return { type: "text", value: String(v) };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function resolveArgs(stmt) {
|
|
213
|
+
if (stmt.named_args && stmt.named_args.length > 0) {
|
|
214
|
+
var obj = {};
|
|
215
|
+
for (var na of stmt.named_args) {
|
|
216
|
+
obj[na.name] = hranaValueToJS(na.value);
|
|
217
|
+
}
|
|
218
|
+
return obj;
|
|
219
|
+
}
|
|
220
|
+
if (stmt.args && stmt.args.length > 0) {
|
|
221
|
+
return stmt.args.map(hranaValueToJS);
|
|
222
|
+
}
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function bindStmt(db, stmt) {
|
|
227
|
+
var args = resolveArgs(stmt);
|
|
228
|
+
var prepared = db.prepare(stmt.sql);
|
|
229
|
+
return Array.isArray(args) && args.length > 0
|
|
230
|
+
? prepared.bind(...args)
|
|
231
|
+
: !Array.isArray(args)
|
|
232
|
+
? prepared.bind(args)
|
|
233
|
+
: prepared;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function executeStmt(db, stmt) {
|
|
237
|
+
var bound = bindStmt(db, stmt);
|
|
238
|
+
var d1Result = await bound.all();
|
|
239
|
+
return convertD1Result(d1Result);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function convertD1Result(d1Result) {
|
|
243
|
+
var rows = d1Result.results || [];
|
|
244
|
+
var cols = rows.length > 0
|
|
245
|
+
? Object.keys(rows[0]).map(function (name) { return { name, decltype: null }; })
|
|
246
|
+
: [];
|
|
247
|
+
var hranaRows = rows.map(function (row) {
|
|
248
|
+
return cols.map(function (col) { return jsValueToHrana(row[col.name]); });
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
cols,
|
|
252
|
+
rows: hranaRows,
|
|
253
|
+
affected_row_count: d1Result.meta?.changes || 0,
|
|
254
|
+
last_insert_rowid: d1Result.meta?.last_row_id != null ? String(d1Result.meta.last_row_id) : null,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function hranaError(code, message) {
|
|
259
|
+
return { results: [{ type: "error", error: { message, code } }] };
|
|
260
|
+
}
|