@eldrin-project/eldrin-app-core 0.0.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.
@@ -0,0 +1,592 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile, copyFile, readdir, mkdir, stat } from 'fs/promises';
3
+ import { existsSync } from 'fs';
4
+ import { resolve, basename, relative } from 'path';
5
+ import { execSync } from 'child_process';
6
+ import { createInterface } from 'readline';
7
+
8
+ // src/migrations/checksum.ts
9
+ var CHECKSUM_PREFIX = "sha256:";
10
+ async function calculateChecksum(content, options = {}) {
11
+ const encoder = new TextEncoder();
12
+ const data = encoder.encode(content);
13
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
14
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
15
+ const hex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
16
+ return options.prefixed ? `${CHECKSUM_PREFIX}${hex}` : hex;
17
+ }
18
+ async function calculatePrefixedChecksum(content) {
19
+ return calculateChecksum(content, { prefixed: true });
20
+ }
21
+
22
+ // src/migrations/sql-parser.ts
23
+ function isValidMigrationFilename(filename) {
24
+ if (!filename.endsWith(".sql") || filename.endsWith(".rollback.sql")) {
25
+ return false;
26
+ }
27
+ const pattern = /^\d{14}-[a-z0-9-]+\.sql$/;
28
+ return pattern.test(filename);
29
+ }
30
+ function extractTimestamp(filename) {
31
+ const match = filename.match(/^(\d{14})-/);
32
+ return match ? match[1] : null;
33
+ }
34
+
35
+ // src/migrations/marketplace.ts
36
+ async function readMigrationFiles(dir) {
37
+ if (!existsSync(dir)) {
38
+ return [];
39
+ }
40
+ const files = await readdir(dir);
41
+ const sqlFiles = files.filter((f) => isValidMigrationFilename(f)).sort();
42
+ const migrations = [];
43
+ for (const file of sqlFiles) {
44
+ const content = await readFile(resolve(dir, file), "utf-8");
45
+ migrations.push({
46
+ name: basename(file),
47
+ content
48
+ });
49
+ }
50
+ return migrations;
51
+ }
52
+ async function generateMigrationManifest(options) {
53
+ const { migrationsDir, database } = options;
54
+ const files = await readMigrationFiles(migrationsDir);
55
+ const migrations = await Promise.all(
56
+ files.map(async (file) => {
57
+ const timestamp = extractTimestamp(file.name);
58
+ const checksum = await calculatePrefixedChecksum(file.content);
59
+ return {
60
+ id: timestamp || file.name.replace(".sql", ""),
61
+ file: file.name,
62
+ checksum
63
+ };
64
+ })
65
+ );
66
+ return {
67
+ manifest: {
68
+ database,
69
+ migrations
70
+ },
71
+ files
72
+ };
73
+ }
74
+
75
+ // src/cli/release.ts
76
+ var DEFAULT_MARKETPLACE_URL = "https://eldrin.io";
77
+ function parseArgs() {
78
+ const args = process.argv.slice(2);
79
+ const options = {};
80
+ for (let i = 0; i < args.length; i++) {
81
+ const arg = args[i];
82
+ const nextArg = args[i + 1];
83
+ switch (arg) {
84
+ case "--type":
85
+ case "-t":
86
+ if (nextArg === "patch" || nextArg === "minor" || nextArg === "major") {
87
+ options.bumpType = nextArg;
88
+ } else {
89
+ console.error(`Invalid bump type: ${nextArg}. Must be patch, minor, or major.`);
90
+ process.exit(1);
91
+ }
92
+ i++;
93
+ break;
94
+ case "--version":
95
+ case "-v":
96
+ options.version = nextArg;
97
+ i++;
98
+ break;
99
+ case "--skip-build":
100
+ options.skipBuild = true;
101
+ break;
102
+ case "--manifest":
103
+ case "-m":
104
+ options.manifestPath = nextArg;
105
+ i++;
106
+ break;
107
+ case "--bundle":
108
+ case "-b":
109
+ options.bundlePath = nextArg;
110
+ i++;
111
+ break;
112
+ case "--migrations":
113
+ options.migrationsDir = nextArg;
114
+ i++;
115
+ break;
116
+ case "--output":
117
+ case "-o":
118
+ options.outputDir = nextArg;
119
+ i++;
120
+ break;
121
+ case "--marketplace-url":
122
+ case "-u":
123
+ options.marketplaceUrl = nextArg;
124
+ i++;
125
+ break;
126
+ case "--help":
127
+ case "-h":
128
+ printHelp();
129
+ process.exit(0);
130
+ }
131
+ }
132
+ return options;
133
+ }
134
+ function bumpVersion(version, type) {
135
+ const parts = version.split(".").map(Number);
136
+ if (parts.length !== 3 || parts.some(isNaN)) {
137
+ throw new Error(`Invalid version format: ${version}. Expected semver (e.g., 1.0.0)`);
138
+ }
139
+ const [major, minor, patch] = parts;
140
+ switch (type) {
141
+ case "major":
142
+ return `${major + 1}.0.0`;
143
+ case "minor":
144
+ return `${major}.${minor + 1}.0`;
145
+ case "patch":
146
+ return `${major}.${minor}.${patch + 1}`;
147
+ }
148
+ }
149
+ function compareVersions(a, b) {
150
+ const partsA = a.split(".").map(Number);
151
+ const partsB = b.split(".").map(Number);
152
+ for (let i = 0; i < 3; i++) {
153
+ if (partsA[i] < partsB[i]) return -1;
154
+ if (partsA[i] > partsB[i]) return 1;
155
+ }
156
+ return 0;
157
+ }
158
+ async function prompt(question) {
159
+ const rl = createInterface({
160
+ input: process.stdin,
161
+ output: process.stdout
162
+ });
163
+ return new Promise((resolve3) => {
164
+ rl.question(question, (answer) => {
165
+ rl.close();
166
+ resolve3(answer.trim());
167
+ });
168
+ });
169
+ }
170
+ async function promptChoice(question, options) {
171
+ console.log(question);
172
+ options.forEach((opt, i) => console.log(` ${i + 1}. ${opt}`));
173
+ while (true) {
174
+ const answer = await prompt(`Choose (1-${options.length}): `);
175
+ const choice = parseInt(answer, 10);
176
+ if (choice >= 1 && choice <= options.length) {
177
+ return choice - 1;
178
+ }
179
+ console.log(`Invalid choice. Please enter a number between 1 and ${options.length}.`);
180
+ }
181
+ }
182
+ async function fetchLatestVersion(marketplaceUrl, developerId, appId) {
183
+ const url = `${marketplaceUrl}/api/marketplace/versions?developerId=${encodeURIComponent(developerId)}&appId=${encodeURIComponent(appId)}`;
184
+ try {
185
+ const response = await fetch(url);
186
+ const contentType = response.headers.get("content-type") || "";
187
+ if (!contentType.includes("application/json")) {
188
+ throw new Error("Marketplace error, please try again later.");
189
+ }
190
+ const data = await response.json();
191
+ if (!data.success) {
192
+ throw new Error(data.error || "Marketplace error, please try again later.");
193
+ }
194
+ return data.latest || null;
195
+ } catch (error) {
196
+ if (error instanceof Error && error.message.startsWith("Marketplace")) {
197
+ throw error;
198
+ }
199
+ throw new Error("Marketplace error, please try again later.");
200
+ }
201
+ }
202
+ function printHelp() {
203
+ console.log(`
204
+ Eldrin Release Tool
205
+
206
+ Prepares an Eldrin app for marketplace submission.
207
+
208
+ Usage:
209
+ npx eldrin-release [options]
210
+
211
+ Options:
212
+ -t, --type <type> Bump version: patch, minor, or major
213
+ Fetches latest from marketplace and bumps accordingly
214
+ -v, --version <version> Override version explicitly (ignores --type)
215
+ --skip-build Skip running the build command
216
+ -m, --manifest <path> Path to manifest (default: ./eldrin-app.manifest.json)
217
+ -b, --bundle <path> Path to bundle (default: auto-detected from manifest)
218
+ --migrations <path> Path to migrations directory (default: ./migrations)
219
+ -o, --output <path> Output directory (default: ./dist/release/...)
220
+ -u, --marketplace-url Marketplace URL (default: https://eldrin.io)
221
+ -h, --help Show this help message
222
+
223
+ Examples:
224
+ npx eldrin-release # Re-release current version
225
+ npx eldrin-release --type patch # Bump patch (0.0.1 -> 0.0.2)
226
+ npx eldrin-release --type minor # Bump minor (0.0.1 -> 0.1.0)
227
+ npx eldrin-release --type major # Bump major (0.0.1 -> 1.0.0)
228
+ npx eldrin-release --version 2.0.0 # Set explicit version
229
+ npx eldrin-release --skip-build # Skip build step
230
+ `);
231
+ }
232
+ async function findManifest(projectRoot) {
233
+ const candidates = [
234
+ "eldrin-app.manifest.json",
235
+ "manifest.json"
236
+ ];
237
+ for (const candidate of candidates) {
238
+ const path = resolve(projectRoot, candidate);
239
+ if (existsSync(path)) {
240
+ return path;
241
+ }
242
+ }
243
+ return null;
244
+ }
245
+ async function findBundle(projectRoot, manifest) {
246
+ if (manifest.entry) {
247
+ const fromDist = resolve(projectRoot, "dist", manifest.entry);
248
+ if (existsSync(fromDist)) return fromDist;
249
+ }
250
+ const patterns = [
251
+ `dist/eldrin-${manifest.id}.js`,
252
+ `dist/${manifest.id}.js`,
253
+ "dist/bundle.js",
254
+ "dist/index.js"
255
+ ];
256
+ for (const pattern of patterns) {
257
+ const path = resolve(projectRoot, pattern);
258
+ if (existsSync(path)) {
259
+ return path;
260
+ }
261
+ }
262
+ const distDir = resolve(projectRoot, "dist");
263
+ if (existsSync(distDir)) {
264
+ const files = await readdir(distDir);
265
+ const jsFile = files.find((f) => f.endsWith(".js") && !f.endsWith(".d.js"));
266
+ if (jsFile) {
267
+ return resolve(distDir, jsFile);
268
+ }
269
+ }
270
+ return null;
271
+ }
272
+ async function ensureDir(dir) {
273
+ if (!existsSync(dir)) {
274
+ await mkdir(dir, { recursive: true });
275
+ }
276
+ }
277
+ async function loadConfig(projectRoot, options) {
278
+ const rcPath = resolve(projectRoot, ".eldrinrc.json");
279
+ let rcConfig = {};
280
+ if (existsSync(rcPath)) {
281
+ try {
282
+ rcConfig = JSON.parse(await readFile(rcPath, "utf-8"));
283
+ } catch {
284
+ }
285
+ }
286
+ return {
287
+ manifestPath: options.manifestPath || rcConfig.manifestPath || "./eldrin-app.manifest.json",
288
+ bundlePath: options.bundlePath || rcConfig.bundlePath || "",
289
+ migrationsDir: options.migrationsDir || rcConfig.migrationsDir || "./migrations",
290
+ buildCommand: rcConfig.buildCommand || "npm run build:lib",
291
+ clientDir: rcConfig.clientDir || "dist/client",
292
+ workerEntry: rcConfig.workerEntry || "dist/worker/index.js"
293
+ };
294
+ }
295
+ function getContentType(filename) {
296
+ const ext = filename.toLowerCase().split(".").pop();
297
+ const contentTypes = {
298
+ "html": "text/html",
299
+ "css": "text/css",
300
+ "js": "application/javascript",
301
+ "json": "application/json",
302
+ "svg": "image/svg+xml",
303
+ "png": "image/png",
304
+ "jpg": "image/jpeg",
305
+ "jpeg": "image/jpeg",
306
+ "ico": "image/x-icon",
307
+ "woff": "font/woff",
308
+ "woff2": "font/woff2",
309
+ "ttf": "font/ttf",
310
+ "eot": "application/vnd.ms-fontobject",
311
+ "webp": "image/webp",
312
+ "gif": "image/gif",
313
+ "map": "application/json"
314
+ };
315
+ return contentTypes[ext || ""] || "application/octet-stream";
316
+ }
317
+ async function bundleClientAssets(clientDir) {
318
+ const assets = [];
319
+ async function processDir(dir, baseDir) {
320
+ const items = await readdir(dir);
321
+ for (const item of items) {
322
+ const fullPath = resolve(dir, item);
323
+ const stats = await stat(fullPath);
324
+ if (stats.isDirectory()) {
325
+ if (!item.startsWith(".")) {
326
+ await processDir(fullPath, baseDir);
327
+ }
328
+ } else {
329
+ if (item === ".dev.vars" || item === "wrangler.json") continue;
330
+ const relativePath = "/" + relative(baseDir, fullPath);
331
+ const content = await readFile(fullPath);
332
+ const base64 = content.toString("base64");
333
+ assets.push({
334
+ path: relativePath,
335
+ contentType: getContentType(item),
336
+ size: stats.size,
337
+ base64
338
+ });
339
+ }
340
+ }
341
+ }
342
+ await processDir(clientDir, clientDir);
343
+ return assets;
344
+ }
345
+ async function createDeploymentBundle(workerPath, clientDir, version) {
346
+ const workerScript = await readFile(workerPath, "utf-8");
347
+ const assets = await bundleClientAssets(clientDir);
348
+ return {
349
+ version,
350
+ worker: {
351
+ script: workerScript,
352
+ main_module: "index.js"
353
+ },
354
+ assets
355
+ };
356
+ }
357
+ async function loadVersionsManifest(appDir) {
358
+ const versionsPath = resolve(appDir, "versions.json");
359
+ if (existsSync(versionsPath)) {
360
+ try {
361
+ const content = await readFile(versionsPath, "utf-8");
362
+ return JSON.parse(content);
363
+ } catch {
364
+ }
365
+ }
366
+ return { latest: "", versions: [] };
367
+ }
368
+ async function updateVersionsManifest(appDir, version, changelog) {
369
+ const manifest = await loadVersionsManifest(appDir);
370
+ const existingIndex = manifest.versions.findIndex((v) => v.version === version);
371
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
372
+ const changes = {
373
+ features: [],
374
+ fixes: [],
375
+ breaking: []
376
+ };
377
+ if (existingIndex === -1) {
378
+ changes.features.push(`Release ${version}`);
379
+ }
380
+ const entry = {
381
+ version,
382
+ releaseDate: today,
383
+ changes
384
+ };
385
+ if (existingIndex >= 0) {
386
+ manifest.versions[existingIndex] = entry;
387
+ } else {
388
+ manifest.versions.unshift(entry);
389
+ }
390
+ manifest.latest = version;
391
+ await writeFile(
392
+ resolve(appDir, "versions.json"),
393
+ JSON.stringify(manifest, null, 2)
394
+ );
395
+ }
396
+ async function release(options = {}) {
397
+ const projectRoot = process.cwd();
398
+ const config = await loadConfig(projectRoot, options);
399
+ console.log("\n\u{1F4E6} Eldrin Release Tool\n");
400
+ const manifestPath = resolve(projectRoot, config.manifestPath);
401
+ if (!existsSync(manifestPath)) {
402
+ const found = await findManifest(projectRoot);
403
+ if (!found) {
404
+ throw new Error(
405
+ "Could not find eldrin-app.manifest.json. Create one or specify with --manifest"
406
+ );
407
+ }
408
+ config.manifestPath = found;
409
+ }
410
+ console.log(`\u{1F4C4} Loading manifest from ${config.manifestPath}`);
411
+ const manifest = JSON.parse(
412
+ await readFile(resolve(projectRoot, config.manifestPath), "utf-8")
413
+ );
414
+ const developerId = manifest.developer.id;
415
+ const appId = manifest.id;
416
+ const localVersion = manifest.version || "0.0.0";
417
+ const marketplaceUrl = options.marketplaceUrl || process.env.ELDRIN_MARKETPLACE_URL || DEFAULT_MARKETPLACE_URL;
418
+ console.log(` App: ${appId} by ${developerId}`);
419
+ console.log(` Local version: ${localVersion}`);
420
+ console.log(` Fetching latest version from marketplace...`);
421
+ let remoteVersion = null;
422
+ try {
423
+ remoteVersion = await fetchLatestVersion(marketplaceUrl, developerId, appId);
424
+ } catch (error) {
425
+ console.log(` \u26A0\uFE0F Could not fetch remote version: ${error instanceof Error ? error.message : "Unknown error"}`);
426
+ }
427
+ if (remoteVersion) {
428
+ console.log(` Remote version: ${remoteVersion}`);
429
+ } else {
430
+ console.log(` Remote version: (none - new app)`);
431
+ }
432
+ let version;
433
+ if (options.version) {
434
+ version = options.version;
435
+ console.log(` Using explicit version: ${version}
436
+ `);
437
+ } else if (options.bumpType) {
438
+ const baseVersion = remoteVersion || localVersion;
439
+ version = bumpVersion(baseVersion, options.bumpType);
440
+ console.log(` Bumping ${options.bumpType}: ${baseVersion} \u2192 ${version}
441
+ `);
442
+ } else if (remoteVersion && compareVersions(remoteVersion, localVersion) > 0) {
443
+ console.log(`
444
+ \u26A0\uFE0F Remote version (${remoteVersion}) is higher than local (${localVersion})
445
+ `);
446
+ const choice = await promptChoice("What would you like to do?", [
447
+ `Use remote version (${remoteVersion})`,
448
+ "Bump version"
449
+ ]);
450
+ if (choice === 0) {
451
+ version = remoteVersion;
452
+ console.log(`
453
+ Using remote version: ${version}
454
+ `);
455
+ } else {
456
+ const bumpChoice = await promptChoice("\nSelect bump type:", [
457
+ `Patch (${bumpVersion(remoteVersion, "patch")})`,
458
+ `Minor (${bumpVersion(remoteVersion, "minor")})`,
459
+ `Major (${bumpVersion(remoteVersion, "major")})`
460
+ ]);
461
+ const bumpTypes = ["patch", "minor", "major"];
462
+ version = bumpVersion(remoteVersion, bumpTypes[bumpChoice]);
463
+ console.log(`
464
+ Bumping ${bumpTypes[bumpChoice]}: ${remoteVersion} \u2192 ${version}
465
+ `);
466
+ }
467
+ } else if (remoteVersion && compareVersions(remoteVersion, localVersion) === 0) {
468
+ console.log(`
469
+ Local and remote versions match (${localVersion})`);
470
+ console.log(` Re-releasing current version
471
+ `);
472
+ version = localVersion;
473
+ } else {
474
+ version = localVersion;
475
+ console.log(` Using local version: ${version}
476
+ `);
477
+ }
478
+ if (version !== manifest.version) {
479
+ console.log(`\u{1F4DD} Updating manifest version: ${manifest.version} \u2192 ${version}`);
480
+ const updatedManifest = { ...manifest, version };
481
+ await writeFile(
482
+ resolve(projectRoot, config.manifestPath),
483
+ JSON.stringify(updatedManifest, null, 2) + "\n"
484
+ );
485
+ const packageJsonPath = resolve(projectRoot, "package.json");
486
+ if (existsSync(packageJsonPath)) {
487
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
488
+ if (packageJson.version !== version) {
489
+ console.log(`\u{1F4DD} Updating package.json version: ${packageJson.version} \u2192 ${version}`);
490
+ packageJson.version = version;
491
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
492
+ }
493
+ }
494
+ }
495
+ if (!options.skipBuild) {
496
+ console.log("\u{1F528} Building library bundle...");
497
+ try {
498
+ execSync(config.buildCommand, { stdio: "inherit", cwd: projectRoot });
499
+ } catch {
500
+ throw new Error("Build failed. Fix errors and try again, or use --skip-build");
501
+ }
502
+ } else {
503
+ console.log("\u23ED\uFE0F Skipping build (--skip-build)");
504
+ }
505
+ const bundlePath = config.bundlePath ? resolve(projectRoot, config.bundlePath) : await findBundle(projectRoot, manifest);
506
+ if (!bundlePath || !existsSync(bundlePath)) {
507
+ throw new Error(
508
+ `Bundle not found. Build the app first or specify with --bundle`
509
+ );
510
+ }
511
+ console.log(`\u{1F4E6} Found bundle: ${bundlePath}`);
512
+ const appDir = resolve(projectRoot, `dist/release/${developerId}/${appId}`);
513
+ const outputDir = options.outputDir || resolve(appDir, `v${version}`);
514
+ const migrationsOutputDir = resolve(appDir, "migrations");
515
+ console.log("\u{1F4C1} Creating output directories...");
516
+ await ensureDir(outputDir);
517
+ await ensureDir(migrationsOutputDir);
518
+ console.log("\u{1F4C4} Writing manifest...");
519
+ const outputManifest = { ...manifest, version };
520
+ await writeFile(
521
+ resolve(outputDir, "eldrin-app.manifest.json"),
522
+ JSON.stringify(outputManifest, null, 2)
523
+ );
524
+ console.log("\u{1F4E6} Copying bundle...");
525
+ await copyFile(bundlePath, resolve(outputDir, "bundle.js"));
526
+ const clientDir = resolve(projectRoot, config.clientDir || "dist/client");
527
+ const workerPath = resolve(projectRoot, config.workerEntry || "dist/worker/index.js");
528
+ if (existsSync(clientDir) && existsSync(workerPath)) {
529
+ console.log("\u{1F4E6} Creating deployment bundle...");
530
+ const deployBundle = await createDeploymentBundle(workerPath, clientDir, version);
531
+ await writeFile(
532
+ resolve(outputDir, "deploy-bundle.json"),
533
+ JSON.stringify(deployBundle)
534
+ // No pretty print to save space
535
+ );
536
+ console.log(` \u2713 ${deployBundle.assets.length} client asset(s) bundled`);
537
+ console.log(` \u2713 Worker script: ${deployBundle.worker.script.length} bytes`);
538
+ }
539
+ const migrationsDir = resolve(projectRoot, config.migrationsDir);
540
+ if (existsSync(migrationsDir)) {
541
+ console.log("\u{1F5C3}\uFE0F Generating migration manifest...");
542
+ const result = await generateMigrationManifest({
543
+ migrationsDir,
544
+ database: appId
545
+ });
546
+ if (result.files.length > 0) {
547
+ await writeFile(
548
+ resolve(migrationsOutputDir, "index.json"),
549
+ JSON.stringify(result.manifest, null, 2)
550
+ );
551
+ for (const file of result.files) {
552
+ await writeFile(resolve(migrationsOutputDir, file.name), file.content);
553
+ }
554
+ console.log(` \u2713 ${result.files.length} migration(s) processed`);
555
+ } else {
556
+ console.log(" \u2139\uFE0F No migrations found");
557
+ }
558
+ } else {
559
+ console.log("\u2139\uFE0F No migrations directory found");
560
+ }
561
+ console.log("\u{1F4CB} Updating versions manifest...");
562
+ await updateVersionsManifest(appDir, version);
563
+ console.log(` \u2713 versions.json updated (latest: ${version})`);
564
+ console.log("\n\u2705 Release package created successfully!\n");
565
+ console.log(`\u{1F4CD} Output: ${outputDir}`);
566
+ console.log("\nFiles:");
567
+ console.log(" \u2022 eldrin-app.manifest.json");
568
+ console.log(" \u2022 bundle.js");
569
+ if (existsSync(resolve(outputDir, "deploy-bundle.json"))) {
570
+ console.log(" \u2022 deploy-bundle.json (deployment bundle with client assets)");
571
+ }
572
+ if (existsSync(migrationsDir)) {
573
+ console.log(" \u2022 migrations/index.json");
574
+ console.log(" \u2022 migrations/*.sql");
575
+ }
576
+ console.log(" \u2022 versions.json (at app level)");
577
+ console.log("\n\u{1F4CB} Next steps:");
578
+ console.log(` npx eldrin-submit -d ${outputDir}`);
579
+ console.log("");
580
+ }
581
+ async function main() {
582
+ const options = parseArgs();
583
+ await release(options);
584
+ }
585
+ main().catch((error) => {
586
+ console.error("\n\u274C Release failed:", error.message);
587
+ process.exit(1);
588
+ });
589
+
590
+ export { release };
591
+ //# sourceMappingURL=release.js.map
592
+ //# sourceMappingURL=release.js.map