@lotus-gui/dev 0.2.1 → 0.2.2

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 CHANGED
@@ -65,36 +65,29 @@ lotus dev main.js # Custom entry point
65
65
 
66
66
  ### `lotus build`
67
67
 
68
- Build your application into a distributable installer package.
68
+ Build your application into a native, single-executable distributable installer package using Node SEA and CrabNebula.
69
69
 
70
70
  ```bash
71
- lotus build --platform linux --target deb
72
- lotus build --platform linux --target rpm
71
+ lotus build --target appimage
72
+ lotus build --target deb
73
+ lotus build --target exe
73
74
  ```
74
75
 
75
76
  | Flag | Values | Default | Description |
76
77
  |------|--------|---------|-------------|
77
- | `--platform` | `linux`, `win32` | Current OS | Target platform (Windows support is experimental/WIP) |
78
- | `--target` | `deb`, `rpm` | `deb` | Installer format (Linux only) |
78
+ | `--target` | `deb`, `appimage`, `pacman`, `msi`, `exe`, `dmg`, `app` | `deb` | Target installer format. Note: Windows targets (`msi`, `exe`) require building on a Windows host or properly configured cross-compilation environment. |
79
79
 
80
80
  **What it does:**
81
- 1. Reads `lotus.config.json` from the current directory
82
- 2. Copies your app files into a staging directory (`dist/app/resources/app/`)
83
- 3. Copies `node_modules` (preserving package structure)
84
- 4. Generates a wrapper shell script as the executable
85
- 5. Packages everything into a `.deb` or `.rpm` installer
86
- 6. Output goes to `dist/installers/`
81
+ 1. Reads `lotus.config.json` from the current directory.
82
+ 2. Recursively bundles your application JS using `esbuild`.
83
+ 3. Discovers native `.node` modules and copies them out of the bundle.
84
+ 4. Generates a Node Single Executable Application (SEA) blob and injects it into a Node.js binary.
85
+ 5. Invokes `@crabnebula/packager` to wrap the executable and native modules into the final OS-specific installer.
86
+ 6. Build artifacts go to `dist/app/` and final installers go to `dist/installers/`.
87
87
 
88
88
  **System Requirements:**
89
89
  - `lotus.config.json` in the current directory
90
- - For RPM targets (Fedora/RHEL):
91
- ```bash
92
- sudo dnf install rpm-build
93
- ```
94
- - For DEB targets (Ubuntu/Debian):
95
- ```bash
96
- sudo apt install dpkg-dev fakeroot
97
- ```
90
+ - Modern Node.js (v20+ with SEA support)
98
91
 
99
92
  ### `lotus clean`
100
93
 
@@ -159,19 +152,13 @@ After running `lotus build`, the `dist/` directory contains:
159
152
 
160
153
  ```
161
154
  dist/
162
- ├── app/ # Staged application
163
- │ ├── resources/app/ # Your app files + node_modules
164
- │ ├── my-app # Wrapper shell script
165
- ├── version # Version file
166
- │ └── LICENSE # License file
155
+ ├── app/ # Staged application components
156
+ │ ├── my-app # Node SEA Single Executable User Binary
157
+ │ ├── lotus.linux-x64-gnu.node # Extracted native bindings
158
+ └── msgpackr-renderer.js
167
159
  └── installers/
168
- └── my-app-1.0.0-1.x86_64.rpm # (or .deb)
169
- ```
170
-
171
- The generated wrapper script runs your app with Node.js:
172
- ```bash
173
- #!/bin/sh
174
- exec node "/usr/lib/my-app/resources/app/main.js" "$@"
160
+ ├── my-app-1.0.0-x86_64.AppImage
161
+ └── my-app_1.0.0_amd64.deb
175
162
  ```
176
163
 
177
164
  ## Project Setup Example
@@ -208,8 +195,8 @@ my-lotus-app/
208
195
  # Run with hot-reload
209
196
  npx lotus dev main.js
210
197
 
211
- # Build an RPM
212
- npx lotus build --platform linux --target rpm
198
+ # Build an AppImage for Linux
199
+ npx lotus build --target appimage
213
200
 
214
201
  # Clean build artifacts
215
202
  npx lotus clean
@@ -218,23 +205,21 @@ npx lotus clean
218
205
  ### Install the Built Package
219
206
 
220
207
  ```bash
221
- # RPM (Fedora/RHEL)
222
- sudo dnf install ./dist/installers/my-app-1.0.0-1.x86_64.rpm
223
-
224
208
  # DEB (Ubuntu/Debian)
225
- sudo dpkg -i ./dist/installers/my-app_1.0.0_amd64.deb
209
+ sudo apt install ./dist/installers/my-app_1.0.0_amd64.deb
226
210
 
227
211
  # Run it
228
212
  my-app
213
+
214
+ # Or use the portable AppImage directly!
215
+ ./dist/installers/my-app-1.0.0-x86_64.AppImage
229
216
  ```
230
217
 
231
218
  ## Architecture
232
219
 
233
220
  ```
234
221
  @lotus-gui/dev
235
- ├── bin/lotus.js # CLI entry point (commander-based)
236
- ├── lib/templates/
237
- │ └── spec.ejs # Custom RPM spec template
222
+ ├── bin/lotus.js # CLI entry point (commander-based build pipeline)
238
223
  ├── index.js # Package entry (exports CLI path)
239
224
  └── package.json
240
225
  ```
@@ -245,9 +230,9 @@ my-app
245
230
  |---------|---------|
246
231
  | `commander` | CLI argument parsing |
247
232
  | `chokidar` | File watching for hot-reload |
248
- | `electron-installer-debian` | `.deb` package generation |
249
- | `electron-installer-redhat` | `.rpm` package generation |
250
- | `electron-winstaller` | Windows installer generation (planned) |
233
+ | `esbuild` | Code bundling and __dirname proxying |
234
+ | `@crabnebula/packager` | OS installation package generation (`.deb`, `.msi`, etc.) |
235
+ | `postject` | Injecting payloads into Node.js SEA binaries |
251
236
 
252
237
  ## License
253
238
 
package/bin/lotus.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { Command } = require('commander');
4
- const { spawn } = require('child_process');
4
+ const { spawn, execSync } = require('child_process');
5
5
  const chokidar = require('chokidar');
6
6
  const path = require('path');
7
7
  const fs = require('fs');
8
8
  const prompts = require('prompts');
9
+ const os = require('os');
10
+ const { Jimp } = require('jimp');
9
11
 
10
12
  const program = new Command();
11
13
 
@@ -70,7 +72,7 @@ program
70
72
  .command('build')
71
73
  .description('Build the application for production')
72
74
  .option('--platform <platform>', 'Target platform (linux, win32)', process.platform)
73
- .option('--target <target>', 'Target format (deb, rpm)', 'deb')
75
+ .option('--target <target>', 'Target format (deb, appimage, msi, nsis)', 'deb')
74
76
  .action(async (cmdOptions) => {
75
77
  const platform = cmdOptions.platform;
76
78
  const target = cmdOptions.target;
@@ -85,173 +87,464 @@ program
85
87
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
86
88
  const distDir = path.resolve('dist');
87
89
  const appDir = path.join(distDir, 'app');
88
- const resourcesDir = path.join(appDir, 'resources', 'app');
89
90
 
90
91
  // Clean dist
91
92
  if (fs.existsSync(distDir)) {
92
93
  fs.rmSync(distDir, { recursive: true, force: true });
93
94
  }
94
- fs.mkdirSync(resourcesDir, { recursive: true });
95
-
96
- console.log('Copying application files...');
97
-
98
- // For copying app's own files — skip build artifacts, dev dirs
99
- const copyAppFiles = (src, dest) => {
100
- if (fs.statSync(src).isDirectory()) {
101
- if (!fs.existsSync(dest)) fs.mkdirSync(dest);
102
- fs.readdirSync(src).forEach(child => {
103
- if (child === 'dist' || child === '.git' || child === '.github' || child === 'node_modules' || child === 'packages' || child === '.DS_Store' || child === 'target' || child === 'servo') return;
104
- copyAppFiles(path.join(src, child), path.join(dest, child));
105
- });
106
- } else {
107
- fs.copyFileSync(src, dest);
95
+ fs.mkdirSync(appDir, { recursive: true });
96
+
97
+ // Determine entry point: lotus.config.json > package.json > index.js
98
+ const appPackagePath = path.resolve('package.json');
99
+ let entryPoint = 'index.js';
100
+ if (fs.existsSync(appPackagePath)) {
101
+ const appPackageCtx = JSON.parse(fs.readFileSync(appPackagePath, 'utf8'));
102
+ entryPoint = config.main || appPackageCtx.main || 'index.js';
103
+ }
104
+
105
+ // 1. Bundle JS with esbuild
106
+ console.log('Bundling application with esbuild...');
107
+ const bundlePath = path.join(appDir, 'bundle.js');
108
+ const shimPath = path.join(appDir, 'esbuild-shim.js');
109
+
110
+ try {
111
+ // Write a shim to intercept __dirname logic for native modules
112
+ fs.writeFileSync(shimPath, `
113
+ import fs from 'fs';
114
+ import path from 'path';
115
+ const execDir = path.dirname(process.execPath);
116
+ const libDir = path.join('/usr/lib', path.basename(process.execPath));
117
+ const flatpakLibDir = path.join('/app/lib', path.basename(process.execPath));
118
+ let macroDir = execDir;
119
+ if (fs.existsSync(path.join(libDir, 'msgpackr-renderer.js'))) {
120
+ macroDir = libDir;
121
+ } else if (fs.existsSync(path.join(flatpakLibDir, 'msgpackr-renderer.js'))) {
122
+ macroDir = flatpakLibDir;
123
+ }
124
+ export const __dirname_macro = macroDir;
125
+ `.trim());
126
+
127
+ // We use esbuild to bundle, and specifically override __dirname so native modules
128
+ // can resolve their .node files sitting next to the final executable.
129
+ execSync(`npx esbuild "${entryPoint}" --bundle --platform=node --outfile="${bundlePath}" --external:*.node --inject:"${shimPath}" --define:__dirname=__dirname_macro`, { stdio: 'inherit' });
130
+
131
+ fs.unlinkSync(shimPath); // Clean up shim
132
+
133
+ // Node SEA fails to resolve built-in `require("./file.node")` relative to the VFS.
134
+ // It also forbids requiring external modules via the global `require` wrapper.
135
+ // We patch the bundle to force an absolute module path relative to our execPath,
136
+ // loaded using `module.createRequire` which escapes the SEA sandbox constraint.
137
+ // When installed via .deb or .rpm, resources are in /usr/lib/<appName>/ instead of /usr/bin/
138
+ let bundleContent = fs.readFileSync(bundlePath, 'utf8');
139
+ bundleContent = bundleContent.replace(/require\(['"]\.\/([^'"]+\.node)['"]\)/g, "require('module').createRequire(process.execPath)(require('path').join(__dirname_macro, '$1'))");
140
+ fs.writeFileSync(bundlePath, bundleContent);
141
+ } catch (err) {
142
+ console.error('esbuild failed');
143
+ process.exit(1);
144
+ }
145
+
146
+ // 2. Crawl and copy .node files
147
+ console.log('Extracting native .node modules...');
148
+ const findNodeFiles = (dir, fileList = [], visited = new Set()) => {
149
+ if (!fs.existsSync(dir)) return fileList;
150
+ const realDir = fs.realpathSync(dir);
151
+ if (visited.has(realDir)) return fileList;
152
+ visited.add(realDir);
153
+
154
+ let files = [];
155
+ try {
156
+ files = fs.readdirSync(dir);
157
+ } catch (err) {
158
+ if (err.code === 'EACCES' || err.code === 'EPERM') return fileList;
159
+ throw err;
160
+ }
161
+
162
+ for (const file of files) {
163
+ const fullPath = path.join(dir, file);
164
+ if (file === '.git' || file === '.github' || file === '.flatpak-builder') continue;
165
+
166
+ try {
167
+ if (fs.statSync(fullPath).isDirectory()) {
168
+ findNodeFiles(fullPath, fileList, visited);
169
+ } else if (file.endsWith('.node')) {
170
+ fileList.push(fullPath);
171
+ }
172
+ } catch (err) {
173
+ if (err.code !== 'EACCES' && err.code !== 'EPERM') throw err;
174
+ }
108
175
  }
176
+ return fileList;
109
177
  };
110
- copyAppFiles(process.cwd(), resourcesDir);
111
-
112
- // For copying node_modules don't skip dist, only skip .git
113
- const copyModuleFiles = (src, dest) => {
114
- if (fs.statSync(src).isDirectory()) {
115
- if (!fs.existsSync(dest)) fs.mkdirSync(dest);
116
- fs.readdirSync(src).forEach(child => {
117
- if (child === '.git') return;
118
- copyModuleFiles(path.join(src, child), path.join(dest, child));
119
- });
120
- } else {
121
- fs.copyFileSync(src, dest);
178
+
179
+ const nodeFiles = findNodeFiles(path.resolve('node_modules'));
180
+ for (const file of nodeFiles) {
181
+ const fileName = path.basename(file);
182
+ fs.copyFileSync(file, path.join(appDir, fileName));
183
+ console.log(`Copied ${fileName}`);
184
+ }
185
+
186
+ console.log('Extracting msgpackr renderer script...');
187
+ let msgpackrRendererPath;
188
+ try {
189
+ msgpackrRendererPath = require.resolve('msgpackr/dist/index.min.js', { paths: [process.cwd()] });
190
+ } catch (e) {
191
+ try {
192
+ msgpackrRendererPath = path.join(path.dirname(require.resolve('msgpackr', { paths: [process.cwd()] })), 'index.min.js');
193
+ } catch (e2) {
194
+ console.warn('Could not locate msgpackr in node_modules! IPC may fail.');
122
195
  }
196
+ }
197
+ if (msgpackrRendererPath && fs.existsSync(msgpackrRendererPath)) {
198
+ fs.copyFileSync(msgpackrRendererPath, path.join(appDir, 'msgpackr-renderer.js'));
199
+ console.log('Copied msgpackr-renderer.js');
200
+ }
201
+
202
+ // 3. Node SEA Generation
203
+ console.log('Generating Node SEA...');
204
+ const binName = config.executableName || config.name.toLowerCase().replace(/ /g, '-');
205
+ const isWindows = platform === 'win32';
206
+ const binPath = isWindows ? path.join(appDir, `${binName}.exe`) : path.join(appDir, binName);
207
+
208
+ const seaConfigPath = path.join(appDir, 'sea-config.json');
209
+ const seaConfig = {
210
+ main: bundlePath,
211
+ output: path.join(appDir, 'sea-prep.blob'),
212
+ disableExperimentalSEAWarning: true
123
213
  };
214
+ fs.writeFileSync(seaConfigPath, JSON.stringify(seaConfig, null, 2));
124
215
 
125
- const copyNodeModules = (src, dest) => {
126
- if (!fs.existsSync(src)) return;
127
- if (!fs.existsSync(dest)) fs.mkdirSync(dest);
128
- fs.readdirSync(src).forEach(child => {
129
- if (child.startsWith('.')) return;
130
- copyModuleFiles(path.join(src, child), path.join(dest, child));
131
- });
216
+ try {
217
+ execSync(`node --experimental-sea-config "${seaConfigPath}"`, { stdio: 'inherit' });
218
+
219
+ // Copy Node binary
220
+ fs.copyFileSync(process.execPath, binPath);
221
+ if (!isWindows) fs.chmodSync(binPath, 0o755);
222
+
223
+ // Inject blob
224
+ // The sentinel fuse is hardcoded per Node.js documentation for SEA
225
+ execSync(`npx postject "${binPath}" NODE_SEA_BLOB "${seaConfig.output}" --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`, { env: { ...process.env }, stdio: 'inherit' });
226
+
227
+ // Cleanup temp SEA files
228
+ fs.unlinkSync(bundlePath);
229
+ fs.unlinkSync(seaConfig.output);
230
+ fs.unlinkSync(seaConfigPath);
231
+ } catch (err) {
232
+ console.error('SEA generation failed', err);
233
+ process.exit(1);
234
+ }
235
+
236
+ // 4. CrabNebula Packaging
237
+ console.log('Packaging with CrabNebula...');
238
+ const packagerConfigPath = path.join(distDir, 'packager.json');
239
+
240
+ const packagerConfig = {
241
+ productName: config.name,
242
+ version: config.version,
243
+ description: config.description || config.name,
244
+ identifier: `com.lotus.${binName}`,
245
+ authors: [config.author || 'Lotus Dev'],
246
+ outDir: path.resolve(distDir, 'installers'),
247
+ // CrabNebula requires paths relative to the current directory where it runs
248
+ binariesDir: path.relative(distDir, appDir),
249
+ binaries: [
250
+ {
251
+ path: isWindows ? binName + '.exe' : binName,
252
+ main: true
253
+ }
254
+ ],
255
+ // Instruct packager to copy our native `.node` modules together into the final installation directory
256
+ resources: [
257
+ path.join(path.relative(distDir, appDir), '*.node'),
258
+ path.join(path.relative(distDir, appDir), 'msgpackr-renderer.js')
259
+ ],
260
+ deb: {
261
+ depends: []
262
+ }
132
263
  };
133
- copyNodeModules(path.join(process.cwd(), 'node_modules'), path.join(resourcesDir, 'node_modules'));
134
264
 
135
- // Create version file for electron-installer-debian in the ROOT of the appDir (it expects it there)
136
- fs.writeFileSync(path.join(appDir, 'version'), config.version || '0.1.0');
265
+ if (config.resources && Array.isArray(config.resources)) {
266
+ for (const res of config.resources) {
267
+ const resPath = path.resolve(process.cwd(), res);
268
+ if (fs.existsSync(resPath)) {
269
+ packagerConfig.resources.push(resPath);
270
+ }
271
+ }
272
+ }
137
273
 
138
- // Handle LICENSE file (required by electron-installer-debian)
139
- const licenseSrc = ['LICENSE', 'LICENSE.md', 'LICENSE.txt'].find(f => fs.existsSync(f));
140
- if (licenseSrc) {
141
- fs.copyFileSync(licenseSrc, path.join(appDir, 'LICENSE'));
142
- } else {
143
- // Create a placeholder license if none exists
144
- fs.writeFileSync(path.join(appDir, 'LICENSE'), `Copyright (c) ${new Date().getFullYear()} ${config.author || 'Lotus App Developer'}. All rights reserved.`);
274
+ if (config.icon) {
275
+ packagerConfig.icons = [path.resolve(config.icon)];
145
276
  }
146
277
 
147
- // Verify @lotus-gui/core binary
148
- // ...
149
-
150
- if (platform === 'linux') {
151
- // Determine binary name
152
- const binName = config.executableName || config.name.toLowerCase().replace(/ /g, '-');
153
- const wmClass = config.build?.linux?.wmClass || binName;
154
-
155
- // Use platform-appropriate arch and dependency names
156
- const isRpm = target === 'rpm';
157
- const arch = isRpm ? 'x86_64' : 'amd64';
158
- const deps = isRpm
159
- ? ['nodejs', 'openssl-libs', 'gtk3', 'webkit2gtk4.0']
160
- : ['nodejs', 'libssl-dev', 'libgtk-3-0', 'libwebkit2gtk-4.0-37'];
161
-
162
- const options = {
163
- src: appDir,
164
- dest: path.join(distDir, 'installers'),
165
- arch: arch,
166
- name: binName,
167
- productName: config.name,
168
- genericName: config.name,
169
- version: config.version,
170
- description: config.description,
171
- productDescription: config.description || config.name,
172
- icon: config.icon ? path.resolve(config.icon) : undefined,
173
- section: config.build?.linux?.section || 'utils',
174
- categories: config.build?.linux?.categories || ['Utility'],
175
- bin: binName,
176
- depends: deps,
177
- maintainer: config.author || 'Lotus App Developer',
178
- homepage: config.homepage,
179
- priority: 'optional',
180
- license: config.license || 'Proprietary'
181
- };
182
-
183
- // Determine entry point: lotus.config.json > package.json > index.js
184
- const appPackageCtx = JSON.parse(fs.readFileSync(path.join(resourcesDir, 'package.json'), 'utf8'));
185
- const entryPoint = config.main || appPackageCtx.main || 'index.js';
186
-
187
- // Create Wrapper Script at the ROOT of appDir (which will be installed to /usr/lib/APPNAME/)
188
- // The script needs to execute node on the file in resources/app
189
- const binScriptPath = path.join(appDir, binName);
190
-
191
- // NOTE: electron-installer-debian installs 'src' content into '/usr/lib/<options.name>/'
192
- // So our resources are at '/usr/lib/<options.name>/resources/app'
193
- // And our binary (this script) is at '/usr/lib/<options.name>/<binName>'
194
-
195
- const wrapperScript = `#!/bin/sh
196
- exec node "/usr/lib/${options.name}/resources/app/${entryPoint}" "$@"
197
- `;
198
- fs.writeFileSync(binScriptPath, wrapperScript, { mode: 0o755 });
278
+ fs.writeFileSync(packagerConfigPath, JSON.stringify(packagerConfig, null, 2));
279
+ const setupAppDir = async (targetBuildSystem = target) => {
280
+ const appDirName = path.join(distDir, 'AppDir');
281
+ if (fs.existsSync(appDirName)) return appDirName;
199
282
 
200
- try {
201
- if (target === 'rpm') {
202
- console.log('Creating RPM package...');
203
- const { Installer } = require('electron-installer-redhat');
204
- const common = require('electron-installer-common');
205
-
206
- class RPMInstaller extends Installer {
207
- async createSpec() {
208
- // Point to our custom template in packages/lotus-dev/lib/templates/spec.ejs
209
- const templatePath = path.resolve(__dirname, '../lib/templates/spec.ejs');
210
- this.options.logger(`Creating spec file at ${this.specPath} using custom template`);
211
- return common.wrapError('creating spec file', async () => this.createTemplatedFile(templatePath, this.specPath));
283
+ const appId = config.appId || `org.lotus.${binName}`;
284
+ const desktopIconName = targetBuildSystem === 'flatpak' ? appId : binName;
285
+
286
+ const libDir = path.join(appDirName, 'usr', 'lib', binName);
287
+ fs.mkdirSync(path.join(appDirName, 'usr', 'bin'), { recursive: true });
288
+ fs.mkdirSync(libDir, { recursive: true });
289
+
290
+ // Copy the binary into usr/bin/
291
+ fs.copyFileSync(binPath, path.join(appDirName, 'usr', 'bin', binName));
292
+ if (!isWindows) fs.chmodSync(path.join(appDirName, 'usr', 'bin', binName), 0o755);
293
+
294
+ // Copy .node files and msgpackr into usr/lib/<binName>/ (FHS-compliant, matches __dirname_macro)
295
+ for (const file of nodeFiles) {
296
+ fs.copyFileSync(file, path.join(libDir, path.basename(file)));
297
+ }
298
+ const localMsgpackr = path.join(appDir, 'msgpackr-renderer.js');
299
+ if (fs.existsSync(localMsgpackr)) {
300
+ fs.copyFileSync(localMsgpackr, path.join(libDir, 'msgpackr-renderer.js'));
301
+ }
302
+
303
+ // Copy extra resources into usr/lib/<binName>/
304
+ if (config.resources && Array.isArray(config.resources)) {
305
+ for (const res of config.resources) {
306
+ const resPath = path.resolve(process.cwd(), res);
307
+ if (fs.existsSync(resPath)) {
308
+ const destPath = path.join(libDir, path.basename(res));
309
+ if (fs.statSync(resPath).isDirectory()) {
310
+ fs.cpSync(resPath, destPath, { recursive: true });
311
+ } else {
312
+ fs.copyFileSync(resPath, destPath);
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ // Create AppRun (for AppImage — launches via the wrapper)
319
+ const appRunPath = path.join(appDirName, 'AppRun');
320
+ fs.writeFileSync(appRunPath, `#!/bin/sh\nHERE="$(dirname "$(readlink -f "\${0}")")"\nexport LD_LIBRARY_PATH="\${HERE}/usr/lib:\${LD_LIBRARY_PATH}"\nexec "\${HERE}/usr/bin/${binName}" "$@"\n`);
321
+ fs.chmodSync(appRunPath, 0o755);
322
+
323
+ // Create .desktop
324
+ const desktopContent = `[Desktop Entry]\nName=${config.name}\nExec=${binName}\nIcon=${desktopIconName}\nType=Application\nCategories=Utility;\n`;
325
+ fs.writeFileSync(path.join(appDirName, `${binName}.desktop`), desktopContent);
326
+
327
+ // Create icon
328
+ if (config.icon && fs.existsSync(path.resolve(config.icon))) {
329
+ const iconPath = path.resolve(config.icon);
330
+ const ext = path.extname(iconPath) || '.png';
331
+ fs.copyFileSync(iconPath, path.join(appDirName, `${binName}${ext}`));
332
+ fs.copyFileSync(iconPath, path.join(appDirName, `.DirIcon`));
333
+ } else {
334
+ fs.writeFileSync(path.join(appDirName, `${binName}.png`), 'iVBO... (empty icon placeholder)');
335
+ fs.writeFileSync(path.join(appDirName, `.DirIcon`), 'empty');
336
+ }
337
+
338
+ // Expose standard FreeDesktop paths for RPM and Flatpak
339
+ const appsDir = path.join(appDirName, 'usr', 'share', 'applications');
340
+ const iconsDir = path.join(appDirName, 'usr', 'share', 'icons', 'hicolor', '512x512', 'apps');
341
+ fs.mkdirSync(appsDir, { recursive: true });
342
+ fs.mkdirSync(iconsDir, { recursive: true });
343
+ fs.writeFileSync(path.join(appsDir, `${binName}.desktop`), desktopContent);
344
+ if (config.icon && fs.existsSync(path.resolve(config.icon))) {
345
+ const ext = path.extname(path.resolve(config.icon)) || '.png';
346
+ try {
347
+ const image = await Jimp.read(path.resolve(config.icon));
348
+ await image.resize({ w: 512, h: 512 });
349
+ await image.write(path.join(iconsDir, `${binName}${ext}`));
350
+ } catch (e) {
351
+ console.error("Failed to resize icon for Flatpak. Skipping...", e);
352
+ fs.copyFileSync(path.resolve(config.icon), path.join(iconsDir, `${binName}${ext}`));
353
+ }
354
+ }
355
+
356
+ return appDirName;
357
+ };
358
+
359
+ try {
360
+ if (target === 'appimage') {
361
+ console.log('Building AppImage natively...');
362
+
363
+ const toolsDir = path.join(os.homedir(), '.lotus-gui', 'tools');
364
+ fs.mkdirSync(toolsDir, { recursive: true });
365
+ const appImageToolPath = path.join(toolsDir, 'appimagetool');
366
+
367
+ if (!fs.existsSync(appImageToolPath)) {
368
+ console.log('Downloading appimagetool... (this only happens once)');
369
+ try {
370
+ execSync(`wget -qO "${appImageToolPath}" https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage`, { stdio: 'inherit' });
371
+ fs.chmodSync(appImageToolPath, 0o755);
372
+ } catch (e) {
373
+ try {
374
+ execSync(`curl -sL -o "${appImageToolPath}" https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage`, { stdio: 'inherit' });
375
+ fs.chmodSync(appImageToolPath, 0o755);
376
+ } catch (e2) {
377
+ console.error('Failed to download appimagetool. Please install curl or wget.');
378
+ process.exit(1);
212
379
  }
213
380
  }
381
+ }
382
+
383
+ const appDirName = await setupAppDir();
384
+
385
+ // Run appimagetool
386
+ console.log('Running appimagetool...');
387
+ const installersDir = path.join(distDir, 'installers');
388
+ fs.mkdirSync(installersDir, { recursive: true });
389
+ const outPath = path.join(installersDir, `${binName}-${config.version}-x86_64.AppImage`);
390
+ execSync(`"${appImageToolPath}" "${appDirName}" "${outPath}"`, { stdio: 'inherit', env: { ...process.env, ARCH: 'x86_64' } });
391
+ console.log(`Successfully created packages in ${installersDir}`);
214
392
 
215
- // Replicate module.exports logic from electron-installer-redhat
216
- const buildRpm = async (data) => {
217
- // Mock logger
218
- data.logger = data.logger || ((msg) => console.log(msg));
219
-
220
- // Mock rename function (default from electron-installer-redhat)
221
- data.rename = data.rename || function (dest, src) {
222
- return path.join(dest, '<%= name %>-<%= version %>-<%= revision %>.<%= arch %>.rpm');
223
- };
224
-
225
- const installer = new RPMInstaller(data);
226
- await installer.generateDefaults();
227
- await installer.generateOptions();
228
- await installer.generateScripts();
229
- await installer.createStagingDir();
230
- await installer.createContents();
231
- await installer.createPackage();
232
- await installer.movePackage();
233
- return installer.options;
393
+ } else {
394
+ // Provide format argument to crabnebula packager
395
+ // CrabNebula formats: deb, appimage, pacman (Linux); wix, nsis (Windows); app, dmg (Mac).
396
+ // Default to target if specified, else crabnebula chooses.
397
+ let formatArg = '';
398
+ if (target === 'deb' || target === 'appimage' || target === 'nsis' || target === 'wix' || target === 'msi' || target === 'exe' || target === 'pacman') {
399
+ const crabTarget = target === 'msi' ? 'wix' : target === 'exe' ? 'nsis' : target;
400
+ formatArg = `-f ${crabTarget}`;
401
+ } else if (target === 'rpm') {
402
+ console.log('Building manual RPM via rpmbuild...');
403
+ const installersDir = path.join(distDir, 'installers');
404
+ fs.mkdirSync(installersDir, { recursive: true });
405
+
406
+ const appDirName = await setupAppDir();
407
+
408
+ const rpmBuildDir = path.join(distDir, 'rpmbuild');
409
+ const rpmBuildRoot = path.join(rpmBuildDir, 'BUILDROOT');
410
+ fs.mkdirSync(path.join(rpmBuildDir, 'SPECS'), { recursive: true });
411
+ fs.mkdirSync(path.join(rpmBuildDir, 'RPMS'), { recursive: true });
412
+
413
+ let filesList = '/usr/bin/*\n';
414
+ filesList += `/usr/lib/${binName}/*\n`; // .node files live here
415
+ if (fs.existsSync(path.join(appDirName, 'usr', 'share', 'applications'))) {
416
+ filesList += '/usr/share/applications/*\n';
417
+ }
418
+ if (fs.existsSync(path.join(appDirName, 'usr', 'share', 'icons'))) {
419
+ filesList += '/usr/share/icons/*\n';
420
+ }
421
+
422
+ const specContent = `
423
+ %define _unpackaged_files_terminate_build 0
424
+ %define __os_install_post %{nil}
425
+ %define debug_package %{nil}
426
+ %global __strip /bin/true
427
+ %global _build_id_links none
428
+
429
+ Name: ${config.name}
430
+ Version: ${config.version}
431
+ Release: 1%{?dist}
432
+ Summary: ${config.description || config.name}
433
+ License: ${config.license || 'Proprietary'}
434
+ ${config.homepage ? `URL: ${config.homepage}` : ''}
435
+ BuildArch: x86_64
436
+ AutoReqProv: no
437
+
438
+ %description
439
+ ${config.description || config.name}
440
+
441
+ %install
442
+ rm -rf %{buildroot}
443
+ mkdir -p %{buildroot}
444
+ cp -a ${path.join(distDir, 'AppDir')}/. %{buildroot}/
445
+
446
+ %files
447
+ ${filesList}
448
+
449
+ %clean
450
+ rm -rf %{buildroot}
451
+ `;
452
+ const specPath = path.join(rpmBuildDir, 'SPECS', `${binName}.spec`);
453
+ fs.writeFileSync(specPath, specContent);
454
+
455
+ try {
456
+ execSync(`rpmbuild -bb --define "_topdir ${rpmBuildDir}" "${specPath}"`, { stdio: 'inherit' });
457
+ const rpmFile = fs.readdirSync(path.join(rpmBuildDir, 'RPMS', 'x86_64'))[0];
458
+ if (rpmFile) {
459
+ fs.copyFileSync(
460
+ path.join(rpmBuildDir, 'RPMS', 'x86_64', rpmFile),
461
+ path.join(installersDir, rpmFile)
462
+ );
463
+ console.log(`Successfully created packages in ${installersDir}`);
464
+ }
465
+ } catch (e) {
466
+ console.error('Failed to build RPM. Is rpmbuild installed?');
467
+ process.exit(1);
468
+ }
469
+
470
+ return; // exit the block instead of hitting crabnebula
471
+ } else if (target === 'flatpak') {
472
+ console.log('Building manual Flatpak via flatpak-builder...');
473
+ const installersDir = path.join(distDir, 'installers');
474
+ fs.mkdirSync(installersDir, { recursive: true });
475
+
476
+ const appDirName = await setupAppDir('flatpak');
477
+
478
+ const flatpakBuildDir = path.join(distDir, 'flatpakbuild');
479
+ const flatpakRepoDir = path.join(distDir, 'flatpakrepo');
480
+ fs.mkdirSync(flatpakBuildDir, { recursive: true });
481
+
482
+ const appId = config.appId || `org.lotus.${binName}`;
483
+
484
+ const manifest = {
485
+ "app-id": appId,
486
+ "runtime": "org.freedesktop.Platform",
487
+ "runtime-version": "24.08",
488
+ "sdk": "org.freedesktop.Sdk",
489
+ "command": binName,
490
+ "build-options": {
491
+ "strip": false,
492
+ "no-debuginfo": true
493
+ },
494
+ "finish-args": [
495
+ "--share=network",
496
+ "--share=ipc",
497
+ "--socket=x11",
498
+ "--socket=wayland",
499
+ "--device=dri",
500
+ "--filesystem=host"
501
+ ],
502
+ "modules": [
503
+ {
504
+ "name": binName,
505
+ "buildsystem": "simple",
506
+ "build-commands": [
507
+ "cp -a AppDir/usr/* /app/",
508
+ `mv /app/share/applications/${binName}.desktop /app/share/applications/\${FLATPAK_ID}.desktop`,
509
+ `mv /app/share/icons/hicolor/512x512/apps/${binName}.png /app/share/icons/hicolor/512x512/apps/\${FLATPAK_ID}.png`
510
+ ],
511
+ "sources": [
512
+ {
513
+ "type": "dir",
514
+ "path": appDirName,
515
+ "dest": "AppDir"
516
+ }
517
+ ]
518
+ }
519
+ ]
234
520
  };
235
521
 
236
- // RPM specific adjustments
237
- options.requires = options.depends; // RPM uses 'requires', not 'depends'
238
- delete options.depends;
522
+ const manifestPath = path.join(flatpakBuildDir, 'manifest.json');
523
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
524
+
525
+ try {
526
+ execSync(`flatpak-builder --repo=${flatpakRepoDir} --force-clean ${path.join(flatpakBuildDir, 'build')} ${manifestPath}`, { stdio: 'inherit' });
527
+ const flatpakFile = path.join(installersDir, `${binName}.flatpak`);
528
+ execSync(`flatpak build-bundle ${flatpakRepoDir} ${flatpakFile} ${appId}`, { stdio: 'inherit' });
529
+ console.log(`Successfully created packages in ${installersDir}`);
530
+ } catch (e) {
531
+ console.error('Failed to build Flatpak. Is flatpak-builder installed?');
532
+ process.exit(1);
533
+ }
239
534
 
240
- await buildRpm(options);
241
- } else { // Default to debian
242
- console.log('Creating Debian package...');
243
- const installer = require('electron-installer-debian');
244
- await installer(options);
535
+ return; // exit the block instead of hitting crabnebula
245
536
  }
246
- console.log(`Successfully created package at ${options.dest}`);
247
- } catch (err) {
248
- console.error(err, err.stack);
249
- process.exit(1);
537
+
538
+ // We pass the stringified JSON configuration directly to avoid file resolving bugs in CrabNebula CLI
539
+ const safeConfigJson = JSON.stringify(packagerConfig).replace(/'/g, "'\\''");
540
+ execSync(`npx @crabnebula/packager -c '${safeConfigJson}' ${formatArg}`, { stdio: 'inherit', cwd: distDir });
541
+
542
+ console.log(`Successfully created packages in ${path.join(distDir, 'installers')}`);
250
543
  }
251
- } else {
252
- console.log('Packager for this platform not fully implemented yet.');
544
+ } catch (err) {
545
+ console.error('CrabNebula Packaging failed', err);
546
+ process.exit(1);
253
547
  }
254
-
255
548
  });
256
549
 
257
550
  program
@@ -287,7 +580,7 @@ program
287
580
  const { overwrite } = await prompts({
288
581
  type: 'confirm',
289
582
  name: 'overwrite',
290
- message: `Directory ${targetDir} already exists. Overwrite?`,
583
+ message: `Directory ${targetDir} already exists.Overwrite ? `,
291
584
  initial: false
292
585
  });
293
586
  if (!overwrite) {
@@ -394,28 +687,28 @@ program
394
687
 
395
688
  // main.js
396
689
  const mainJs = `const { ServoWindow, app, ipcMain } = require('@lotus-gui/core');
397
- const path = require('path');
398
-
399
- app.warmup();
400
-
401
- const win = new ServoWindow({
402
- id: 'main-window',
403
- root: path.join(__dirname, 'ui'),
404
- index: 'index.html',
405
- width: 1024,
406
- height: 768,
407
- title: "${metadata.name}",
408
- transparent: true,
409
- visible: false
410
- });
411
-
412
- win.once('frame-ready', () => win.show());
413
-
414
- ipcMain.on('hello', (data) => {
415
- console.log('Received from renderer:', data);
416
- ipcMain.send('reply', { message: 'Hello from Node.js!' });
417
- });
418
- `;
690
+ const path = require('path');
691
+
692
+ app.warmup();
693
+
694
+ const win = new ServoWindow({
695
+ id: 'main-window',
696
+ root: path.join(__dirname, 'ui'),
697
+ index: 'index.html',
698
+ width: 1024,
699
+ height: 768,
700
+ title: "${metadata.name}",
701
+ transparent: true,
702
+ visible: false
703
+ });
704
+
705
+ win.once('frame-ready', () => win.show());
706
+
707
+ ipcMain.on('hello', (data) => {
708
+ console.log('Received from renderer:', data);
709
+ ipcMain.send('reply', { message: 'Hello from Node.js!' });
710
+ });
711
+ `;
419
712
  fs.writeFileSync(path.join(projectPath, 'main.js'), mainJs);
420
713
 
421
714
  // UI Directory
@@ -423,58 +716,58 @@ ipcMain.on('hello', (data) => {
423
716
  fs.mkdirSync(uiDir);
424
717
 
425
718
  // ui/index.html
426
- const indexHtml = `<!DOCTYPE html>
427
- <html>
428
- <head>
429
- <title>${metadata.name}</title>
430
- <style>
431
- body { margin: 0; padding: 0; background: transparent; font-family: sans-serif; }
432
- .app {
433
- background: rgba(30, 30, 30, 0.95);
434
- color: white;
435
- height: 100vh;
436
- display: flex;
437
- flex-direction: column;
438
- align-items: center;
439
- justify-content: center;
440
- border-radius: 8px; /* Optional rounded corners for the view */
719
+ const indexHtml = `< !DOCTYPE html >
720
+ <html>
721
+ <head>
722
+ <title>${metadata.name}</title>
723
+ <style>
724
+ body {margin: 0; padding: 0; background: transparent; font-family: sans-serif; }
725
+ .app {
726
+ background: rgba(30, 30, 30, 0.95);
727
+ color: white;
728
+ height: 100vh;
729
+ display: flex;
730
+ flex-direction: column;
731
+ align-items: center;
732
+ justify-content: center;
733
+ border-radius: 8px; /* Optional rounded corners for the view */
441
734
  }
442
- button {
443
- padding: 10px 20px;
444
- font-size: 16px;
445
- cursor: pointer;
446
- background: #646cff;
447
- color: white;
448
- border: none;
449
- border-radius: 4px;
735
+ button {
736
+ padding: 10px 20px;
737
+ font-size: 16px;
738
+ cursor: pointer;
739
+ background: #646cff;
740
+ color: white;
741
+ border: none;
742
+ border-radius: 4px;
450
743
  }
451
- button:hover { background: #535bf2; }
452
- </style>
453
- </head>
454
- <body>
455
- <div class="app">
456
- <h1>Welcome to ${metadata.name} 🪷</h1>
457
- <p>Powered by Lotus (Servo + Node.js)</p>
458
- <button onclick="sendMessage()">Ping Node.js</button>
459
- <p id="response"></p>
460
- </div>
461
-
462
- <script>
463
- function sendMessage() {
464
- window.lotus.send('hello', { timestamp: Date.now() });
744
+ button:hover {background: #535bf2; }
745
+ </style>
746
+ </head>
747
+ <body>
748
+ <div class="app">
749
+ <h1>Welcome to ${metadata.name} 🪷</h1>
750
+ <p>Powered by Lotus (Servo + Node.js)</p>
751
+ <button onclick="sendMessage()">Ping Node.js</button>
752
+ <p id="response"></p>
753
+ </div>
754
+
755
+ <script>
756
+ function sendMessage() {
757
+ window.lotus.send('hello', { timestamp: Date.now() });
465
758
  }
466
759
 
467
760
  window.lotus.on('reply', (data) => {
468
- document.getElementById('response').innerText = data.message;
761
+ document.getElementById('response').innerText = data.message;
469
762
  });
470
- </script>
471
- </body>
472
- </html>`;
763
+ </script>
764
+ </body>
765
+ </html>`;
473
766
  fs.writeFileSync(path.join(uiDir, 'index.html'), indexHtml);
474
767
 
475
- console.log(`\n✅ Project initialized in ${projectPath}`);
476
- console.log(`\nNext steps:`);
477
- console.log(` cd ${targetDir}`);
768
+ console.log(`\n✅ Project initialized in ${projectPath} `);
769
+ console.log(`\nNext steps: `);
770
+ console.log(` cd ${targetDir} `);
478
771
  console.log(` npm install`);
479
772
  console.log(` npx lotus dev\n`);
480
773
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotus-gui/dev",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "The Lotus Toolkit",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -13,13 +13,12 @@
13
13
  "author": "",
14
14
  "license": "ISC",
15
15
  "dependencies": {
16
+ "@crabnebula/packager": "^0.11.2",
16
17
  "chokidar": "^3.5.3",
17
18
  "commander": "^11.1.0",
18
- "electron-winstaller": "^5.4.0",
19
+ "esbuild": "^0.27.3",
20
+ "jimp": "^1.6.0",
21
+ "postject": "^1.0.0-alpha.6",
19
22
  "prompts": "^2.4.2"
20
- },
21
- "optionalDependencies": {
22
- "electron-installer-debian": "^3.2.0",
23
- "electron-installer-redhat": "^3.4.0"
24
23
  }
25
- }
24
+ }
@@ -0,0 +1 @@
1
+ {"main":"test-sea-bundle.js","output":"test-sea.blob"}
package/shim.js ADDED
@@ -0,0 +1 @@
1
+ import { dirname } from "path"; export const __dirname_macro = dirname(process.execPath);