@krema-build/krema 0.1.0-rc.10

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/krema.js ADDED
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Krema CLI shim.
4
+ * Resolves the best way to run Krema (native binary, JAR+Java, or JAR+auto-install JDK)
5
+ * and execs the resolved command with inherited stdio.
6
+ */
7
+
8
+ const fs = require('node:fs');
9
+ const { spawn } = require('node:child_process');
10
+ const readline = require('node:readline');
11
+ const { resolve, findJava } = require('../lib/resolve');
12
+ const { read, write } = require('../lib/cache');
13
+ const { installJdk } = require('../lib/jdk');
14
+ const { JAVA_VERSION, VERSION } = require('../lib/constants');
15
+
16
+ async function main() {
17
+ const args = process.argv.slice(2);
18
+
19
+ // Try cached resolution first (validate that cached files still exist)
20
+ const cached = read();
21
+ if (cached && cached.version === VERSION) {
22
+ if (cached.mode === 'native' && cached.path && fs.existsSync(cached.path)) {
23
+ return exec(cached.path, args);
24
+ }
25
+ if (cached.mode === 'jar' && cached.java && cached.jar && fs.existsSync(cached.jar)) {
26
+ return exec(cached.java, ['--enable-native-access=ALL-UNNAMED', '-jar', cached.jar, ...args]);
27
+ }
28
+ }
29
+
30
+ // Full resolution
31
+ const resolved = await resolve();
32
+
33
+ if (resolved.mode === 'native') {
34
+ write({ mode: 'native', path: resolved.binary, version: VERSION });
35
+ return exec(resolved.binary, args);
36
+ }
37
+
38
+ // JAR mode — need Java
39
+ let javaPath = resolved.java;
40
+
41
+ if (!javaPath) {
42
+ // Interactive prompt for JDK install
43
+ if (process.stdin.isTTY) {
44
+ const answer = await prompt(
45
+ `Java ${JAVA_VERSION} is required but was not found.\n` +
46
+ `Install Eclipse Temurin ${JAVA_VERSION} to ~/.krema/jdk? [Y/n] `
47
+ );
48
+
49
+ if (answer === '' || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
50
+ try {
51
+ javaPath = await installJdk();
52
+ } catch (err) {
53
+ console.error(`\nFailed to install JDK: ${err.message}`);
54
+ process.exit(1);
55
+ }
56
+ } else {
57
+ printManualInstallInstructions();
58
+ process.exit(1);
59
+ }
60
+ } else {
61
+ // Non-interactive: try auto-install
62
+ try {
63
+ javaPath = await installJdk();
64
+ } catch (err) {
65
+ console.error(`Error: Java ${JAVA_VERSION} is required but was not found.`);
66
+ printManualInstallInstructions();
67
+ process.exit(1);
68
+ }
69
+ }
70
+ }
71
+
72
+ write({ mode: 'jar', java: javaPath, jar: resolved.jar, version: VERSION });
73
+ return exec(javaPath, ['--enable-native-access=ALL-UNNAMED', '-jar', resolved.jar, ...args]);
74
+ }
75
+
76
+ function exec(command, args) {
77
+ const child = spawn(command, args, { stdio: 'inherit' });
78
+ child.on('error', (err) => {
79
+ console.error(`Failed to start: ${err.message}`);
80
+ process.exit(1);
81
+ });
82
+ child.on('close', (code) => {
83
+ process.exit(code ?? 1);
84
+ });
85
+ }
86
+
87
+ function prompt(question) {
88
+ return new Promise((resolve) => {
89
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
90
+ rl.question(question, (answer) => {
91
+ rl.close();
92
+ resolve(answer.trim());
93
+ });
94
+ });
95
+ }
96
+
97
+ function printManualInstallInstructions() {
98
+ console.error('');
99
+ console.error(`To use Krema, install Java ${JAVA_VERSION} and either:`);
100
+ console.error(' - Set KREMA_JAVA_HOME to point at the JDK');
101
+ console.error(` - Ensure /usr/libexec/java_home -v ${JAVA_VERSION} works (macOS)`);
102
+ console.error(' - Or add java to your PATH');
103
+ console.error('');
104
+ console.error('Download from: https://adoptium.net/temurin/releases/');
105
+ }
106
+
107
+ main().catch((err) => {
108
+ console.error(err.message);
109
+ process.exit(1);
110
+ });
package/lib/cache.js ADDED
@@ -0,0 +1,18 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { CACHE_FILE, KREMA_HOME } = require('./constants');
4
+
5
+ function read() {
6
+ try {
7
+ return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+
13
+ function write(data) {
14
+ fs.mkdirSync(KREMA_HOME, { recursive: true });
15
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2) + '\n');
16
+ }
17
+
18
+ module.exports = { read, write };
@@ -0,0 +1,36 @@
1
+ const path = require('node:path');
2
+ const os = require('node:os');
3
+
4
+ const VERSION = '0.1.0-rc.10';
5
+ const JAVA_VERSION = '25';
6
+ const GITHUB_REPO = 'krema-build/krema';
7
+ const JAR_NAME = 'krema-cli.jar';
8
+
9
+ const KREMA_HOME = path.join(os.homedir(), '.krema');
10
+ const LIB_DIR = path.join(KREMA_HOME, 'lib');
11
+ const JDK_DIR = path.join(KREMA_HOME, 'jdk');
12
+ const CACHE_FILE = path.join(KREMA_HOME, 'cache.json');
13
+ const TEMURIN_DIR = path.join(JDK_DIR, `temurin-${JAVA_VERSION}`);
14
+
15
+ function jarUrl(version) {
16
+ return `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${JAR_NAME}`;
17
+ }
18
+
19
+ function nativeBinaryUrl(version, platformKey) {
20
+ const name = platformKey === 'win32-x64' ? 'krema-windows-x64.exe' : `krema-${platformKey}`;
21
+ return `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${name}`;
22
+ }
23
+
24
+ module.exports = {
25
+ VERSION,
26
+ JAVA_VERSION,
27
+ GITHUB_REPO,
28
+ JAR_NAME,
29
+ KREMA_HOME,
30
+ LIB_DIR,
31
+ JDK_DIR,
32
+ CACHE_FILE,
33
+ TEMURIN_DIR,
34
+ jarUrl,
35
+ nativeBinaryUrl,
36
+ };
@@ -0,0 +1,69 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const https = require('node:https');
4
+ const http = require('node:http');
5
+
6
+ function follow(url) {
7
+ return url.startsWith('https://') ? https : http;
8
+ }
9
+
10
+ function download(url, dest, { onProgress, quiet } = {}) {
11
+ return new Promise((resolve, reject) => {
12
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
13
+ const file = fs.createWriteStream(dest);
14
+
15
+ const get = (currentUrl) => {
16
+ follow(currentUrl).get(currentUrl, (res) => {
17
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
18
+ res.resume();
19
+ get(res.headers.location);
20
+ return;
21
+ }
22
+
23
+ if (res.statusCode !== 200) {
24
+ file.close();
25
+ fs.unlinkSync(dest);
26
+ reject(new Error(`Download failed: HTTP ${res.statusCode} for ${currentUrl}`));
27
+ return;
28
+ }
29
+
30
+ const total = parseInt(res.headers['content-length'], 10) || 0;
31
+ let downloaded = 0;
32
+
33
+ res.on('data', (chunk) => {
34
+ downloaded += chunk.length;
35
+ if (onProgress && total > 0) {
36
+ onProgress(downloaded, total);
37
+ } else if (!quiet && total > 0 && process.stderr.isTTY) {
38
+ const pct = Math.round((downloaded / total) * 100);
39
+ const mb = (downloaded / 1048576).toFixed(1);
40
+ const totalMb = (total / 1048576).toFixed(1);
41
+ process.stderr.write(`\r Downloading... ${mb}/${totalMb} MB (${pct}%)`);
42
+ }
43
+ });
44
+
45
+ res.pipe(file);
46
+
47
+ file.on('finish', () => {
48
+ if (!quiet && total > 0 && process.stderr.isTTY) {
49
+ process.stderr.write('\n');
50
+ }
51
+ file.close(resolve);
52
+ });
53
+
54
+ file.on('error', (err) => {
55
+ fs.unlinkSync(dest);
56
+ reject(err);
57
+ });
58
+ }).on('error', (err) => {
59
+ file.close();
60
+ try { fs.unlinkSync(dest); } catch {}
61
+ reject(err);
62
+ });
63
+ };
64
+
65
+ get(url);
66
+ });
67
+ }
68
+
69
+ module.exports = { download };
package/lib/jdk.js ADDED
@@ -0,0 +1,73 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { execFileSync } = require('node:child_process');
4
+ const { getPlatform } = require('./platform');
5
+ const { JAVA_VERSION, JDK_DIR, TEMURIN_DIR } = require('./constants');
6
+ const { download } = require('./download');
7
+
8
+ function adoptiumUrl() {
9
+ const platform = getPlatform();
10
+ if (!platform) {
11
+ return null;
12
+ }
13
+ return `https://api.adoptium.net/v3/binary/latest/${JAVA_VERSION}/ga/${platform.adoptiumOs}/${platform.adoptiumArch}/jdk/hotspot/normal/eclipse`;
14
+ }
15
+
16
+ function temurinJavaPath() {
17
+ // After extraction, Temurin tarballs contain a top-level directory like
18
+ // jdk-25+36 (or jdk-25.0.1+9 etc). We find it dynamically.
19
+ if (!fs.existsSync(TEMURIN_DIR)) {
20
+ return null;
21
+ }
22
+ const entries = fs.readdirSync(TEMURIN_DIR);
23
+ for (const entry of entries) {
24
+ const candidate = process.platform === 'darwin'
25
+ ? path.join(TEMURIN_DIR, entry, 'Contents', 'Home', 'bin', 'java')
26
+ : path.join(TEMURIN_DIR, entry, 'bin', 'java');
27
+ if (fs.existsSync(candidate)) {
28
+ return candidate;
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+
34
+ async function installJdk() {
35
+ const url = adoptiumUrl();
36
+ if (!url) {
37
+ throw new Error(`No Adoptium JDK available for ${process.platform}-${process.arch}`);
38
+ }
39
+
40
+ const ext = process.platform === 'win32' ? '.zip' : '.tar.gz';
41
+ const tmpFile = path.join(JDK_DIR, `temurin-${JAVA_VERSION}${ext}`);
42
+
43
+ console.error(` Downloading Eclipse Temurin ${JAVA_VERSION}...`);
44
+ await download(url, tmpFile);
45
+
46
+ console.error(' Extracting...');
47
+ fs.mkdirSync(TEMURIN_DIR, { recursive: true });
48
+
49
+ if (ext === '.tar.gz') {
50
+ execFileSync('tar', ['xzf', tmpFile, '-C', TEMURIN_DIR], { stdio: 'pipe' });
51
+ } else {
52
+ // Windows: use PowerShell to extract zip
53
+ execFileSync('powershell', [
54
+ '-NoProfile', '-Command',
55
+ `Expand-Archive -Path '${tmpFile}' -DestinationPath '${TEMURIN_DIR}' -Force`
56
+ ], { stdio: 'pipe' });
57
+ }
58
+
59
+ fs.unlinkSync(tmpFile);
60
+
61
+ const javaPath = temurinJavaPath();
62
+ if (!javaPath) {
63
+ throw new Error('JDK extraction succeeded but java binary not found');
64
+ }
65
+
66
+ // Verify
67
+ const out = execFileSync(javaPath, ['-version'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
68
+ // java -version writes to stderr
69
+ console.error(` Installed: ${javaPath}`);
70
+ return javaPath;
71
+ }
72
+
73
+ module.exports = { adoptiumUrl, temurinJavaPath, installJdk };
@@ -0,0 +1,25 @@
1
+ const os = require('node:os');
2
+ const process = require('node:process');
3
+
4
+ const PLATFORMS = {
5
+ 'darwin-arm64': { npmPkg: '@krema-build/cli-darwin-arm64', adoptiumOs: 'mac', adoptiumArch: 'aarch64' },
6
+ 'darwin-x64': { npmPkg: '@krema-build/cli-darwin-x64', adoptiumOs: 'mac', adoptiumArch: 'x64' },
7
+ 'linux-x64': { npmPkg: '@krema-build/cli-linux-x64', adoptiumOs: 'linux', adoptiumArch: 'x64' },
8
+ 'linux-arm64': { npmPkg: '@krema-build/cli-linux-arm64', adoptiumOs: 'linux', adoptiumArch: 'aarch64' },
9
+ 'win32-x64': { npmPkg: '@krema-build/cli-win32-x64', adoptiumOs: 'windows', adoptiumArch: 'x64' },
10
+ };
11
+
12
+ function getPlatformKey() {
13
+ return `${process.platform}-${os.arch()}`;
14
+ }
15
+
16
+ function getPlatform() {
17
+ const key = getPlatformKey();
18
+ const platform = PLATFORMS[key];
19
+ if (!platform) {
20
+ return null;
21
+ }
22
+ return { key, ...platform };
23
+ }
24
+
25
+ module.exports = { PLATFORMS, getPlatformKey, getPlatform };
package/lib/resolve.js ADDED
@@ -0,0 +1,143 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { execFileSync } = require('node:child_process');
4
+ const { getPlatform } = require('./platform');
5
+ const { JAVA_VERSION, LIB_DIR, JAR_NAME, VERSION } = require('./constants');
6
+ const { jarUrl } = require('./constants');
7
+ const { download } = require('./download');
8
+ const { temurinJavaPath } = require('./jdk');
9
+
10
+ /**
11
+ * Try to find the native binary from the platform-specific npm optional dependency.
12
+ * Returns the path to the binary, or null.
13
+ */
14
+ function findNativeBinary() {
15
+ const platform = getPlatform();
16
+ if (!platform) return null;
17
+
18
+ try {
19
+ const pkgDir = path.dirname(require.resolve(`${platform.npmPkg}/package.json`));
20
+ const bin = path.join(pkgDir, 'bin', process.platform === 'win32' ? 'krema.exe' : 'krema');
21
+ if (fs.existsSync(bin)) {
22
+ return bin;
23
+ }
24
+ } catch {
25
+ // Package not installed (wrong platform or optional dep skipped)
26
+ }
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * Find the fat JAR. Downloads it if not present.
32
+ * Returns the path to the JAR.
33
+ */
34
+ async function findOrDownloadJar() {
35
+ const jarPath = path.join(LIB_DIR, JAR_NAME);
36
+ if (fs.existsSync(jarPath)) {
37
+ return jarPath;
38
+ }
39
+
40
+ console.error(` Downloading ${JAR_NAME}...`);
41
+ await download(jarUrl(VERSION), jarPath);
42
+ return jarPath;
43
+ }
44
+
45
+ /**
46
+ * Find a Java 25 installation.
47
+ * Search order:
48
+ * 1. KREMA_JAVA_HOME env
49
+ * 2. macOS: /usr/libexec/java_home -v 25
50
+ * 3. JAVA_HOME (if version matches)
51
+ * 4. java on PATH (if version matches)
52
+ * 5. ~/.krema/jdk/temurin-25/
53
+ * Returns the path to the java binary, or null.
54
+ */
55
+ function findJava() {
56
+ // 1. KREMA_JAVA_HOME override
57
+ if (process.env.KREMA_JAVA_HOME) {
58
+ const java = path.join(process.env.KREMA_JAVA_HOME, 'bin', 'java');
59
+ if (fs.existsSync(java)) return java;
60
+ }
61
+
62
+ // 2. macOS: /usr/libexec/java_home
63
+ if (process.platform === 'darwin') {
64
+ try {
65
+ const jh = execFileSync('/usr/libexec/java_home', ['-v', JAVA_VERSION], {
66
+ encoding: 'utf8',
67
+ stdio: ['pipe', 'pipe', 'pipe'],
68
+ }).trim();
69
+ const java = path.join(jh, 'bin', 'java');
70
+ if (jh && fs.existsSync(java)) return java;
71
+ } catch {
72
+ // Not found
73
+ }
74
+ }
75
+
76
+ // 3. JAVA_HOME if version matches
77
+ if (process.env.JAVA_HOME) {
78
+ const java = path.join(process.env.JAVA_HOME, 'bin', 'java');
79
+ if (fs.existsSync(java) && checkJavaVersion(java)) return java;
80
+ }
81
+
82
+ // 4. java on PATH
83
+ try {
84
+ const javaOnPath = execFileSync(process.platform === 'win32' ? 'where' : 'which', ['java'], {
85
+ encoding: 'utf8',
86
+ stdio: ['pipe', 'pipe', 'pipe'],
87
+ }).trim().split('\n')[0];
88
+ if (javaOnPath && checkJavaVersion(javaOnPath)) return javaOnPath;
89
+ } catch {
90
+ // Not found
91
+ }
92
+
93
+ // 5. ~/.krema/jdk/temurin-25/
94
+ const temurin = temurinJavaPath();
95
+ if (temurin) return temurin;
96
+
97
+ return null;
98
+ }
99
+
100
+ function checkJavaVersion(javaPath) {
101
+ try {
102
+ const output = execFileSync(javaPath, ['-version'], {
103
+ encoding: 'utf8',
104
+ stdio: ['pipe', 'pipe', 'pipe'],
105
+ });
106
+ // java -version outputs to stderr, but execFileSync with stdio pipe captures it
107
+ // Some JDKs output to stdout, some to stderr. Check both.
108
+ const combined = output || '';
109
+ return parseJavaVersion(combined) === JAVA_VERSION;
110
+ } catch (err) {
111
+ // java -version writes to stderr which becomes err.stderr
112
+ if (err.stderr) {
113
+ return parseJavaVersion(err.stderr) === JAVA_VERSION;
114
+ }
115
+ return false;
116
+ }
117
+ }
118
+
119
+ function parseJavaVersion(text) {
120
+ const match = text.match(/"(\d+)/);
121
+ return match ? match[1] : null;
122
+ }
123
+
124
+ /**
125
+ * Resolve the execution mode. Returns { mode, args } where:
126
+ * - mode "native": args = [binaryPath, ...userArgs]
127
+ * - mode "jar": args = [javaPath, "-jar", jarPath, ...userArgs]
128
+ */
129
+ async function resolve() {
130
+ // Tier 1: Native binary
131
+ const nativeBin = findNativeBinary();
132
+ if (nativeBin) {
133
+ return { mode: 'native', binary: nativeBin, java: null, jar: null };
134
+ }
135
+
136
+ // Tier 2 & 3: JAR + Java
137
+ const jarPath = await findOrDownloadJar();
138
+ const javaPath = findJava();
139
+
140
+ return { mode: 'jar', binary: null, java: javaPath, jar: jarPath };
141
+ }
142
+
143
+ module.exports = { findNativeBinary, findOrDownloadJar, findJava, resolve };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@krema-build/krema",
3
+ "version": "0.1.0-rc.10",
4
+ "description": "Lightweight desktop apps with system webviews — CLI tool",
5
+ "bin": {
6
+ "krema": "bin/krema.js"
7
+ },
8
+ "scripts": {
9
+ "postinstall": "node postinstall.js"
10
+ },
11
+ "optionalDependencies": {
12
+ "@krema-build/cli-darwin-arm64": "0.1.0-rc.10",
13
+ "@krema-build/cli-linux-x64": "0.1.0-rc.10",
14
+ "@krema-build/cli-linux-arm64": "0.1.0-rc.10",
15
+ "@krema-build/cli-win32-x64": "0.1.0-rc.10"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "lib/",
20
+ "postinstall.js"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "keywords": [
26
+ "krema",
27
+ "desktop",
28
+ "webview",
29
+ "gui",
30
+ "native",
31
+ "cli"
32
+ ],
33
+ "license": "BSL-1.1",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/krema-build/krema.git",
37
+ "directory": "krema-npm/packages/krema"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }
package/postinstall.js ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Postinstall script for the krema npm package.
4
+ * Non-interactive: detects the best execution mode and caches the result.
5
+ * Always exits 0 to never break npm install.
6
+ */
7
+
8
+ const { findNativeBinary, findJava } = require('./lib/resolve');
9
+ const { download } = require('./lib/download');
10
+ const { read, write } = require('./lib/cache');
11
+ const { VERSION, LIB_DIR, JAR_NAME, jarUrl } = require('./lib/constants');
12
+ const path = require('node:path');
13
+ const fs = require('node:fs');
14
+
15
+ async function main() {
16
+ // 1. Check for native binary
17
+ const nativeBin = findNativeBinary();
18
+ if (nativeBin) {
19
+ write({ mode: 'native', path: nativeBin, version: VERSION });
20
+ return;
21
+ }
22
+
23
+ // 2. No native binary — download JAR
24
+ const jarPath = path.join(LIB_DIR, JAR_NAME);
25
+ if (!fs.existsSync(jarPath)) {
26
+ try {
27
+ await download(jarUrl(VERSION), jarPath, { quiet: true });
28
+ } catch {
29
+ // Non-fatal: bin/krema.js will retry at runtime
30
+ }
31
+ }
32
+
33
+ // Only cache if the JAR was actually downloaded
34
+ if (!fs.existsSync(jarPath)) {
35
+ return;
36
+ }
37
+
38
+ // 3. Check for Java 25
39
+ const javaPath = findJava();
40
+ if (javaPath) {
41
+ write({ mode: 'jar', java: javaPath, jar: jarPath, version: VERSION });
42
+ } else {
43
+ write({ mode: 'jar-no-java', jar: jarPath, version: VERSION });
44
+ }
45
+ }
46
+
47
+ main().catch(() => {
48
+ // Never break npm install
49
+ process.exit(0);
50
+ });