lively-electron 0.1.1 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf19bfa8b454abba3dde67bb6e1267bf868fa68439dd167c689c52a51e7c0fd6
4
- data.tar.gz: 0ab6ff6ea8a4ccf8d47c4097fa486074c12fc70e7fa343dd1ecee476fdfac2bc
3
+ metadata.gz: 268db954369a5f37db622d68406bb23be76ba11233dd518ac0cb27015624945f
4
+ data.tar.gz: 884719a50f2f34b8cdde22b694cdfceb2b206fde2bd478f6dcc1f42a159074e0
5
5
  SHA512:
6
- metadata.gz: 42b648927ee2f83336fd533b5ffe33de54be204e7f16a8c45e667304045a3605aba110ad3ce1044a9994a9ef6600c54cf0b2e38f6ef368230560343d749d4518
7
- data.tar.gz: d3986882840225c81d4e8de8cbe3c7613698567f8534a546d4e94a7fc965c51d5acfefc999f7b5f4ce0e95b3c0209f8ff9a79e1d4ea6010e728029107a8c55e9
6
+ metadata.gz: a05c6e4d19c06836aba2b030b5bed3fa0294e4eaafeb161c0975784871c447804dbd890b419792f6b1899ab501a2491a8c13d6870ef397c0a4deec2f846c6a45
7
+ data.tar.gz: f188d608d575029e0894d7d5a70afce1f15d5ea07359069c47b30ffa08ca94533fdf1e92e108b48c4e7702bd7a0a57f84072b71afaf620d46801244280ea1d0e
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ # Creates +package.json+ in the project root with +pnpm init+ or +npm init+ when it is
7
+ # missing, then runs the appropriate install command for {Lively::Electron::Packager}.
8
+ def install
9
+ require "console"
10
+ require "lively/electron/packager"
11
+
12
+ root = context.root
13
+ package_json_path = File.join(root, "package.json")
14
+ packager = Lively::Electron::Packager.detect(root, ::ENV)
15
+
16
+ unless File.file?(package_json_path)
17
+ packager.setup!(root)
18
+ Console.info(self, "Created", package_json_path, "using", packager)
19
+ end
20
+
21
+ packager.run_install_in!(root)
22
+ end
data/bin/lively-electron CHANGED
@@ -8,5 +8,18 @@ if development_mode
8
8
  ENV["CONSOLE_LEVEL"] = "debug"
9
9
  end
10
10
 
11
+ require_relative "../lib/lively/electron/packager"
12
+
13
+ # Look for node_modules in the app’s working directory first; the published gem does not
14
+ # ship node_modules, so that is where users install electron. Fall back to the gem’s own
15
+ # directory for development/CI (e.g. pnpm in the repo root while cwd is examples/*).
16
+ project_root = File.expand_path(Dir.pwd)
17
+ gem_root = File.expand_path("..", __dir__)
18
+ packager = Lively::Electron::Packager.detect(project_root, ENV)
19
+
20
+ electron_path = Lively::Electron::Packager.resolve_electron_executable(
21
+ packager, [project_root, gem_root], ENV
22
+ )
23
+
11
24
  main_js = File.join(__dir__, "..", "src", "main.js")
12
- Process.exec("npx", "electron", main_js, *ARGV)
25
+ Process.exec(electron_path, main_js, *ARGV)
@@ -11,7 +11,7 @@ end
11
11
  configuration = Async::Service::Configuration.build do
12
12
  service "lively-electron" do
13
13
  include Lively::Electron::Environment::Application
14
- end
14
+ end
15
15
  end
16
16
 
17
17
  Async::Service::Controller.run(configuration)
@@ -10,10 +10,10 @@ Add the gem to your project:
10
10
  $ bundle add lively-electron
11
11
  ~~~
12
12
 
13
- Also install the npm package for the Electron wrapper:
13
+ Also install the Node dependencies for the Electron binary (and create `package.json` if it is missing) using the task from the gem:
14
14
 
15
15
  ~~~ bash
16
- $ npm install lively-electron
16
+ $ bundle exec bake lively:electron:install
17
17
  ~~~
18
18
 
19
19
  ## Core Concepts
@@ -13,23 +13,27 @@ module Lively
13
13
  module Electron
14
14
  # @namespace
15
15
  module Environment
16
- # Represents the environment configuration for a Lively Electron application server.
17
- #
18
- # This module provides server configuration for Electron apps, using TCP
19
- # localhost binding for direct connection from Electron/Chromium.
16
+ # The Lively environment for a desktop Electron shell that connects to this process over a local HTTP server.
17
+ # The server uses TCP and may inherit a bound socket passed via `LIVELY_SERVER_DESCRIPTOR`.
20
18
  module Application
21
19
  include Lively::Environment::Application
22
20
 
21
+ # The base URL the Electron shell uses to reach the Lively server.
22
+ # Reads `LIVELY_URL` from the environment, or defaults to `http://localhost:0/`.
23
+ # @returns [String]
23
24
  def url
24
- "http://localhost:0/"
25
+ ENV.fetch("LIVELY_URL", "http://localhost:0/")
25
26
  end
26
27
 
28
+ # The HTTP endpoint the server listens on.
29
+ # When `LIVELY_SERVER_DESCRIPTOR` is set, reuses the inherited bound socket instead of binding a new one.
30
+ # @returns [Async::HTTP::Endpoint]
27
31
  def endpoint
28
32
  if descriptor = ENV["LIVELY_SERVER_DESCRIPTOR"]
29
33
  Console.info(self, "Using inherited file descriptor.", descriptor: descriptor)
30
34
  bound_socket = Socket.for_fd(descriptor.to_i)
31
35
 
32
- # Ensure that the socket is non-blocking:
36
+ # Ensure the inherited socket is non-blocking:
33
37
  bound_socket.nonblock = true
34
38
 
35
39
  endpoint = IO::Endpoint::BoundEndpoint.new(nil, [bound_socket])
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "lively"
7
+
8
+ # @namespace
9
+ module Lively
10
+ # @namespace
11
+ module Electron
12
+ # @namespace
13
+ module Packager
14
+ # Raised when {Packager.resolve_electron_executable} cannot produce a usable path from any candidate root, or when a custom {Generic#electron_executable_path} signals failure for every root.
15
+ class NotFoundError < StandardError; end
16
+
17
+ # A tool-agnostic Node project layout. Both {Npm} and {Pnpm} place CLI shims in `node_modules/.bin` after install. Subclasses implement the concrete {install_command} and {setup!} flows.
18
+ class Generic
19
+ # @attribute [String] Semver range for the `electron` dependency written by {setup!}.
20
+ ELECTRON_VERSION_RANGE = "^41.0.0"
21
+
22
+ # The `argv` for a top-level install with the current packager (e.g. `pnpm install` or `npm install`).
23
+ # @returns [Array(String)] The program name and arguments, ready for `Process.spawn`.
24
+ # @raises [NotImplementedError] When the concrete packager has not overridden this method.
25
+ # @abstract
26
+ def install_command
27
+ raise NotImplementedError, "#{self.class} must implement #install_command"
28
+ end
29
+
30
+ # Runs {install_command} inside `package_root` (e.g. from a bake or CI task).
31
+ # @parameter package_root [String] The directory passed as `chdir` to the subprocess.
32
+ # @raises [RuntimeError] If the install command exits with a non-zero status.
33
+ def run_install_in!(package_root)
34
+ args = install_command
35
+ unless system(*args, chdir: File.expand_path(package_root), exception: false)
36
+ raise "Command failed: #{args.join(" ")} (in #{package_root})"
37
+ end
38
+ end
39
+
40
+ # Creates a `package.json` when none exists using the tool's `init` flow, then adds {ELECTRON_VERSION_RANGE} as a dependency.
41
+ # @parameter package_root [String] The project directory to initialise.
42
+ # @raises [NotImplementedError] When the concrete packager has not overridden this method.
43
+ # @abstract
44
+ def setup!(package_root)
45
+ raise NotImplementedError, "#{self.class} must implement #setup!"
46
+ end
47
+
48
+ # Resolves a filesystem path to the `electron` binary. A non-empty `ELECTRON` entry in `environment` wins outright; otherwise looks for an executable `node_modules/.bin/electron` shim under `package_root`.
49
+ # @parameter package_root [String] A directory that may contain a local `node_modules`.
50
+ # @parameter environment [Hash] The process environment. Defaults to `::ENV`.
51
+ # @returns [String] The absolute path to the `electron` binary.
52
+ # @raises [NotFoundError] If no usable binary is found under `package_root`.
53
+ def electron_executable_path(package_root, environment = ::ENV)
54
+ explicit = environment["ELECTRON"]&.to_s
55
+ if explicit && !explicit.empty?
56
+ return File.expand_path(explicit)
57
+ end
58
+
59
+ path = local_electron_path(package_root)
60
+ return path if File.executable?(path)
61
+
62
+ raise NotFoundError, "Could not find electron in #{package_root}."
63
+ end
64
+
65
+ # A human-readable suggestion for running the install command in `root`. Useful when `electron` is absent from both `node_modules` and `PATH`.
66
+ # @parameter root [String] The path shown in the hint.
67
+ # @returns [String] A one-line string of the form `Run: ... in <absolute_path>`.
68
+ def install_hint(root)
69
+ "Run: #{install_command.join(' ')} in #{File.expand_path(root)}"
70
+ end
71
+
72
+ private
73
+
74
+ # @returns [String] The expanded path to the `node_modules/.bin/electron` shim under `root`.
75
+ def local_electron_path(root)
76
+ root = File.expand_path(root)
77
+ bin = File.join(root, "node_modules", ".bin", "electron")
78
+ if Gem.win_platform? && !File.exist?(bin)
79
+ bin = File.join(root, "node_modules", ".bin", "electron.cmd")
80
+ end
81
+ File.expand_path(bin)
82
+ end
83
+
84
+ # Runs a subprocess in `package_root` and raises on failure.
85
+ # @parameter argv [Array(String)] The program and its arguments.
86
+ # @parameter package_root [String] Working directory for the command.
87
+ # @parameter label [String] A short description used in failure messages.
88
+ # @raises [RuntimeError] If the process exits with a non-zero status.
89
+ def run_subprocess!(argv, package_root, label)
90
+ root = File.expand_path(package_root)
91
+ unless system(*argv, chdir: root, exception: false)
92
+ raise "Command failed: #{label} – #{argv.join(" ")} (in #{root})"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "generic"
7
+
8
+ # @namespace
9
+ module Lively
10
+ # @namespace
11
+ module Electron
12
+ # @namespace
13
+ module Packager
14
+ # An npm (`package-lock.json`) project layout with the corresponding `npm` command line.
15
+ class Npm < Generic
16
+ # @returns [String] The user-visible packager name, `"npm"`, used in messages and hints.
17
+ def to_s = "npm"
18
+
19
+ # @returns [Array(String)] `["npm", "install"]` for a full dependency install.
20
+ def install_command
21
+ %w[npm install]
22
+ end
23
+
24
+ # Creates a `package.json` and installs {ELECTRON_VERSION_RANGE} when no manifest exists yet; see {Generic#setup!}.
25
+ # @parameter package_root [String] The project directory.
26
+ # @raises [RuntimeError] If any subprocess step fails.
27
+ def setup!(package_root)
28
+ manifest = File.join(File.expand_path(package_root), "package.json")
29
+ return if File.file?(manifest)
30
+
31
+ run_subprocess!(%w[npm init -y], package_root, "npm init -y")
32
+ run_subprocess!(
33
+ [
34
+ "npm", "install", "--save-prod",
35
+ "electron@#{ELECTRON_VERSION_RANGE}"
36
+ ],
37
+ package_root,
38
+ "npm install electron"
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "json"
7
+ require_relative "generic"
8
+
9
+ # @namespace
10
+ module Lively
11
+ # @namespace
12
+ module Electron
13
+ # @namespace
14
+ module Packager
15
+ # A pnpm (`pnpm-lock.yaml`) project layout with the corresponding `pnpm` command line.
16
+ class Pnpm < Generic
17
+ # @returns [String] The user-visible packager name, `"pnpm"`, used in messages and hints.
18
+ def to_s = "pnpm"
19
+
20
+ # @returns [Array(String)] `["pnpm", "install"]` for a full dependency install.
21
+ def install_command
22
+ %w[pnpm install]
23
+ end
24
+
25
+ # Creates a `package.json` and installs {ELECTRON_VERSION_RANGE} when no manifest exists yet; see {Generic#setup!}. Also writes the `onlyBuiltDependencies` policy required by pnpm 10+.
26
+ # @parameter package_root [String] The project directory.
27
+ # @raises [RuntimeError] If any subprocess step fails.
28
+ def setup!(package_root)
29
+ manifest = File.join(File.expand_path(package_root), "package.json")
30
+ return if File.file?(manifest)
31
+
32
+ # pnpm 10 requires --bare and --init-package-manager (see `pnpm help init`):
33
+ run_subprocess!(%w[pnpm init --bare --init-package-manager], package_root, "pnpm init")
34
+ run_subprocess!(
35
+ [
36
+ "pnpm", "add",
37
+ "electron@#{ELECTRON_VERSION_RANGE}"
38
+ ],
39
+ package_root,
40
+ "pnpm add electron"
41
+ )
42
+ merge_electron_postinstall_policy!(package_root)
43
+ end
44
+
45
+ private
46
+
47
+ # Ensures `electron` is listed under `pnpm.onlyBuiltDependencies` in `package.json`. Without this entry, pnpm 10+ blocks `electron`'s `postinstall` script.
48
+ # @parameter package_root [String] The project directory containing `package.json`.
49
+ def merge_electron_postinstall_policy!(package_root)
50
+ path = File.join(File.expand_path(package_root), "package.json")
51
+ data = JSON.parse(File.read(path, encoding: Encoding::UTF_8))
52
+ pnode = data["pnpm"] || {}
53
+ list = pnode["onlyBuiltDependencies"] || []
54
+ pnode["onlyBuiltDependencies"] = (["electron"] + Array(list)).uniq
55
+ data["pnpm"] = pnode
56
+ File.write(path, JSON.pretty_generate(data) + "\n")
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "json"
7
+
8
+ require_relative "packager/generic"
9
+ require_relative "packager/npm"
10
+ require_relative "packager/pnpm"
11
+
12
+ # @namespace
13
+ module Lively
14
+ # @namespace
15
+ module Electron
16
+ # Detects the Node packager in use (npm or pnpm) and resolves the `electron` binary path. Both tools place CLI shims in `node_modules/.bin` after install; see {Generic#electron_executable_path}.
17
+ module Packager
18
+ # @attribute [String] Environment variable for explicitly selecting `npm` or `pnpm`.
19
+ ENV_KEY = "LIVELY_ELECTRON_PACKAGER"
20
+
21
+ class << self
22
+ # Picks a packager for a project root.
23
+ #
24
+ # Resolution order:
25
+ # 1. `LIVELY_ELECTRON_PACKAGER` env var with value `npm` or `pnpm` (case-insensitive);
26
+ # 2. `package.json` `packageManager` field (e.g. `pnpm@10.0.0` or `npm@10.0.0`);
27
+ # 3. a lone lock file (`pnpm-lock.yaml` or `package-lock.json`);
28
+ # 4. if both lock files are present, `pnpm` (matches this gem's default);
29
+ # 5. if there is no clear signal, `pnpm`.
30
+ #
31
+ # @parameter package_root [String] The directory to search for `package.json` and lock files.
32
+ # @parameter environment [Hash] The process environment. Defaults to `::ENV`.
33
+ # @returns [Lively::Electron::Packager::Npm | Lively::Electron::Packager::Pnpm] A concrete {Npm} or {Pnpm} instance.
34
+ # @raises [ArgumentError] If `LIVELY_ELECTRON_PACKAGER` is set to something other than `npm` or `pnpm`.
35
+ def detect(package_root, environment = ::ENV)
36
+ root = File.expand_path(package_root)
37
+
38
+ if (resolved = from_environment(environment[ENV_KEY]))
39
+ return resolved
40
+ end
41
+
42
+ if (resolved = from_package_json(root))
43
+ return resolved
44
+ end
45
+
46
+ has_pnpm = File.file?(File.join(root, "pnpm-lock.yaml"))
47
+ has_npm = File.file?(File.join(root, "package-lock.json"))
48
+
49
+ if has_pnpm && has_npm
50
+ return Pnpm.new
51
+ elsif has_pnpm
52
+ return Pnpm.new
53
+ elsif has_npm
54
+ return Npm.new
55
+ else
56
+ return Pnpm.new
57
+ end
58
+ end
59
+
60
+ # Calls {Generic#electron_executable_path} for each of `search_roots` in order (e.g. the app's working directory, then the gem root in development). The first concrete path wins. If no root yields a local binary, falls back to the bare program name `"electron"` for the OS to find on `PATH`.
61
+ # @parameter packager [Lively::Electron::Packager::Generic] A concrete {Npm} or {Pnpm} instance.
62
+ # @parameter search_roots [Array(String)] Candidate directories; first match wins.
63
+ # @parameter environment [Hash] The process environment. Defaults to `::ENV`.
64
+ # @returns [String] An absolute path, or `"electron"` to resolve via `PATH`.
65
+ def resolve_electron_executable(packager, search_roots, environment = ::ENV)
66
+ search_roots
67
+ .compact
68
+ .map {|path| File.expand_path(path)}
69
+ .uniq
70
+ .each do |search_root|
71
+ begin
72
+ return packager.electron_executable_path(search_root, environment)
73
+ rescue NotFoundError
74
+ next
75
+ end
76
+ end
77
+
78
+ # No local binary found in any root; rely on the OS to find `electron` on PATH:
79
+ "electron"
80
+ end
81
+
82
+ private
83
+
84
+ def from_environment(value)
85
+ value = value&.to_s&.strip
86
+ return nil if value.nil? || value.empty?
87
+
88
+ case value.downcase
89
+ when "npm" then Npm.new
90
+ when "pnpm" then Pnpm.new
91
+ else
92
+ raise ArgumentError, "Invalid #{ENV_KEY} #{value.inspect} (use 'npm' or 'pnpm')."
93
+ end
94
+ end
95
+
96
+ def from_package_json(root)
97
+ package_json = read_package_json(root) or return nil
98
+ package_manager = package_json["packageManager"].to_s
99
+ return Pnpm.new if package_manager.start_with?("pnpm@")
100
+ return Npm.new if package_manager.start_with?("npm@")
101
+
102
+ nil
103
+ end
104
+
105
+ def read_package_json(root)
106
+ path = File.join(root, "package.json")
107
+ return nil unless File.readable?(path)
108
+
109
+ ::JSON.parse(File.read(path, encoding: Encoding::UTF_8))
110
+ rescue ::JSON::ParserError
111
+ nil
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -7,7 +7,8 @@
7
7
  module Lively
8
8
  # @namespace
9
9
  module Electron
10
- VERSION = "0.1.1"
10
+ # @attribute [String] The published SemVer of this gem.
11
+ VERSION = "0.2.0"
11
12
  end
12
13
  end
13
14
 
@@ -4,5 +4,7 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require "lively"
7
+
7
8
  require_relative "electron/version"
8
9
  require_relative "electron/environment"
10
+ require_relative "electron/packager"
data/readme.md CHANGED
@@ -14,6 +14,11 @@ Please see the [project documentation](https://github.com/socketry/lively-electr
14
14
 
15
15
  Please see the [project releases](https://github.com/socketry/lively-electron/releases/index) for all releases.
16
16
 
17
+ ### v0.2.0
18
+
19
+ - Add support for `pnpm` as well as `npm`.
20
+ - Add `bake lively:electron:install` that installs electron locally.
21
+
17
22
  ### v0.1.0
18
23
 
19
24
  - Initial implementation.
data/releases.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Releases
2
2
 
3
+ ## v0.2.0
4
+
5
+ - Add support for `pnpm` as well as `npm`.
6
+ - Add `bake lively:electron:install` that installs electron locally.
7
+
3
8
  ## v0.1.0
4
9
 
5
10
  - Initial implementation.
data/src/main.js CHANGED
@@ -1,155 +1,155 @@
1
1
  const {app, BrowserWindow, ipcMain} = require('electron');
2
- const {spawn} = require('child_process');
2
+ const {spawn, spawnSync} = require('child_process');
3
3
  const http = require('http');
4
4
  const net = require('net');
5
5
  const path = require('path');
6
6
  const fs = require('fs');
7
7
 
8
8
  class LivelyElectronApp {
9
- constructor() {
10
- this.rubyProcess = null;
11
- this.mainWindow = null;
12
- this.serverUrl = null;
13
-
14
- // Store the working directory (should be preserved by direct CLI)
15
- this.originalCwd = process.cwd();
16
- console.log('💾 Working directory:', this.originalCwd);
17
- }
18
-
19
- isDevelopment() {
20
- const variant = process.env.LIVELY_VARIANT || process.env.VARIANT;
21
- return variant === "development";
22
- }
23
-
24
- async start() {
25
- try {
26
- // 1. Start Ruby Lively server on TCP localhost
27
- await this.startLivelyServer();
28
-
29
- // 2. Create Electron window (connects directly to Ruby server)
30
- await this.createWindow();
31
-
32
- console.log(`Lively Electron started - Ruby server: ${this.serverUrl}`);
33
- } catch (error) {
34
- console.error('Failed to start Lively Electron:', error);
35
- app.quit();
36
- }
37
- }
38
-
39
- async startLivelyServer() {
40
- return new Promise((resolve, reject) => {
41
- // Forward all CLI args to the Ruby server; let the Ruby script decide the application file
42
- const args = process.argv.slice(2);
43
-
44
- // Create server and let it bind+listen, then pass handle to child
45
- const server = net.createServer();
46
- server.listen(0, '127.0.0.1', () => {
47
- const port = server.address().port;
48
- this.serverUrl = `http://localhost:${port}`;
49
-
50
- const fd = server._handle.fd;
51
-
52
- // Start Ruby process with FD passed via stdio and forward all CLI args
53
- const livelyElectronScript = path.join(__dirname, '..', 'bin', 'lively-electron-server');
54
- const childArgs = args.slice();
55
-
56
- const child = spawn(livelyElectronScript, childArgs, {
57
- stdio: [
58
- 'inherit', // stdin
59
- 'inherit', // stdout
60
- 'inherit', // stderr
61
- fd // Pass socket FD as stdio[3]
62
- ],
63
- env: {
64
- ...process.env,
65
- LIVELY_SERVER_DESCRIPTOR: '3' // Tell child it's on FD 3
66
- }
67
- });
68
-
69
- child.on('spawn', () => {
70
- this.rubyProcess = child;
71
- server.close((error) => {
72
- if (error) {
73
- reject(error);
74
- } else {
75
- console.log(`✅ Ruby server should be ready: ${this.serverUrl}`);
76
- resolve();
77
- }
78
- });
79
- });
80
-
81
- child.on('error', (error) => {
82
- server.close();
83
- console.error('Failed to spawn Ruby process:', error);
84
- reject(error);
85
- });
86
-
87
- child.on('close', (code) => {
88
- this.rubyProcess = null;
89
- console.log(`Ruby process exited with code ${code}`);
90
- if (code !== 0) {
91
- reject(new Error(`Ruby process failed with code ${code}`));
92
- }
93
- });
94
- });
95
- });
96
- }
97
-
98
- async createWindow() {
99
- this.mainWindow = new BrowserWindow({
100
- width: 1200,
101
- height: 800,
102
- webPreferences: {
103
- nodeIntegration: false,
104
- contextIsolation: true,
105
- enableRemoteModule: false
106
- },
107
- titleBarStyle: 'hiddenInset'
108
- });
109
-
110
- // Load the Lively app directly
111
- await this.mainWindow.loadURL(this.serverUrl);
112
-
113
- // Open DevTools in development mode
114
- if (this.isDevelopment()) {
115
- this.mainWindow.webContents.openDevTools();
116
- }
117
-
118
- this.mainWindow.on('closed', () => {
119
- this.cleanup();
120
- });
121
- }
122
-
123
- cleanup() {
124
- if (this.rubyProcess) {
125
- this.rubyProcess.kill();
126
- }
127
- }
9
+ constructor() {
10
+ this.rubyProcess = null;
11
+ this.mainWindow = null;
12
+ this.serverUrl = null;
13
+
14
+ // Store the working directory (should be preserved by direct CLI)
15
+ this.originalCwd = process.cwd();
16
+ console.log('💾 Working directory:', this.originalCwd);
17
+ }
18
+
19
+ isDevelopment() {
20
+ const variant = process.env.LIVELY_VARIANT || process.env.VARIANT;
21
+ return variant === "development";
22
+ }
23
+
24
+ async start() {
25
+ try {
26
+ // 1. Start Ruby Lively server on TCP localhost
27
+ await this.startLivelyServer();
28
+
29
+ // 2. Create Electron window (connects directly to Ruby server)
30
+ await this.createWindow();
31
+
32
+ console.log(`Lively Electron started - Ruby server: ${this.serverUrl}`);
33
+ } catch (error) {
34
+ console.error('Failed to start Lively Electron:', error);
35
+ app.quit();
36
+ }
37
+ }
38
+
39
+ async startLivelyServer() {
40
+ return new Promise((resolve, reject) => {
41
+ // Forward all CLI args to the Ruby server; let the Ruby script decide the application file
42
+ const args = process.argv.slice(2);
43
+
44
+ // Create server and let it bind+listen, then pass handle to child
45
+ const server = net.createServer();
46
+ server.listen(0, '127.0.0.1', () => {
47
+ const port = server.address().port;
48
+ this.serverUrl = `http://localhost:${port}`;
49
+
50
+ const fd = server._handle.fd;
51
+
52
+ // Start Ruby process with FD passed via stdio and forward all CLI args
53
+ const livelyElectronScript = path.join(__dirname, '..', 'bin', 'lively-electron-server');
54
+ const childArgs = args.slice();
55
+
56
+ const child = spawn(livelyElectronScript, childArgs, {
57
+ stdio: [
58
+ 'inherit', // stdin
59
+ 'inherit', // stdout
60
+ 'inherit', // stderr
61
+ fd // Pass socket FD as stdio[3]
62
+ ],
63
+ env: {
64
+ ...process.env,
65
+ LIVELY_SERVER_DESCRIPTOR: '3' // Tell child it's on FD 3
66
+ }
67
+ });
68
+
69
+ child.on('spawn', () => {
70
+ this.rubyProcess = child;
71
+ server.close((error) => {
72
+ if (error) {
73
+ reject(error);
74
+ } else {
75
+ console.log(`✅ Ruby server should be ready: ${this.serverUrl}`);
76
+ resolve();
77
+ }
78
+ });
79
+ });
80
+
81
+ child.on('error', (error) => {
82
+ server.close();
83
+ console.error('Failed to spawn Ruby process:', error);
84
+ reject(error);
85
+ });
86
+
87
+ child.on('close', (code) => {
88
+ this.rubyProcess = null;
89
+ console.log(`Ruby process exited with code ${code}`);
90
+ if (code !== 0) {
91
+ reject(new Error(`Ruby process failed with code ${code}`));
92
+ }
93
+ });
94
+ });
95
+ });
96
+ }
97
+
98
+ async createWindow() {
99
+ this.mainWindow = new BrowserWindow({
100
+ width: 1200,
101
+ height: 800,
102
+ webPreferences: {
103
+ nodeIntegration: false,
104
+ contextIsolation: true,
105
+ enableRemoteModule: false
106
+ },
107
+ titleBarStyle: 'hiddenInset'
108
+ });
109
+
110
+ // Load the Lively app directly
111
+ await this.mainWindow.loadURL(this.serverUrl);
112
+
113
+ // Open DevTools in development mode
114
+ if (this.isDevelopment()) {
115
+ this.mainWindow.webContents.openDevTools();
116
+ }
117
+
118
+ this.mainWindow.on('closed', () => {
119
+ this.cleanup();
120
+ });
121
+ }
122
+
123
+ cleanup() {
124
+ if (this.rubyProcess) {
125
+ this.rubyProcess.kill();
126
+ }
127
+ }
128
128
  }
129
129
 
130
130
  // Electron app lifecycle
131
131
  app.whenReady().then(() => {
132
- console.log('Electron ready, starting Lively app...');
133
- const livelyApp = new LivelyElectronApp();
134
- livelyApp.start().catch(error => {
135
- console.error('Failed to start Lively app:', error);
136
- app.quit();
137
- });
132
+ console.log('Electron ready, starting Lively app...');
133
+ const livelyApp = new LivelyElectronApp();
134
+ livelyApp.start().catch(error => {
135
+ console.error('Failed to start Lively app:', error);
136
+ app.quit();
137
+ });
138
138
  });
139
139
 
140
140
  app.on('window-all-closed', () => {
141
- if (process.platform !== 'darwin') {
142
- app.quit();
143
- }
141
+ if (process.platform !== 'darwin') {
142
+ app.quit();
143
+ }
144
144
  });
145
145
 
146
146
  app.on('activate', () => {
147
- if (BrowserWindow.getAllWindows().length === 0) {
148
- const livelyApp = new LivelyElectronApp();
149
- livelyApp.start();
150
- }
147
+ if (BrowserWindow.getAllWindows().length === 0) {
148
+ const livelyApp = new LivelyElectronApp();
149
+ livelyApp.start();
150
+ }
151
151
  });
152
152
 
153
153
  app.on('before-quit', () => {
154
- // Cleanup will be handled by window close event
154
+ // Cleanup will be handled by window close event
155
155
  });
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lively-electron
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -100,12 +100,17 @@ executables:
100
100
  extensions: []
101
101
  extra_rdoc_files: []
102
102
  files:
103
+ - bake/lively/electron/install.rb
103
104
  - bin/lively-electron
104
105
  - bin/lively-electron-server
105
106
  - context/getting-started.md
106
107
  - context/index.yaml
107
108
  - lib/lively/electron.rb
108
109
  - lib/lively/electron/environment.rb
110
+ - lib/lively/electron/packager.rb
111
+ - lib/lively/electron/packager/generic.rb
112
+ - lib/lively/electron/packager/npm.rb
113
+ - lib/lively/electron/packager/pnpm.rb
109
114
  - lib/lively/electron/version.rb
110
115
  - license.md
111
116
  - readme.md
@@ -132,7 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
137
  - !ruby/object:Gem::Version
133
138
  version: '0'
134
139
  requirements: []
135
- rubygems_version: 3.6.9
140
+ rubygems_version: 4.0.6
136
141
  specification_version: 4
137
142
  summary: Electron wrapper for Lively Ruby applications
138
143
  test_files: []
metadata.gz.sig CHANGED
Binary file