electron-cli 0.2.8 → 0.3.0-alpha.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,101 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const https = require("node:https");
5
+ const os = require("node:os");
6
+ const path = require("node:path");
7
+ const { spawnSync } = require("node:child_process");
8
+
9
+ const root = path.resolve(__dirname, "..");
10
+ const packageJson = require(path.join(root, "package.json"));
11
+ const version = packageJson.version;
12
+ const installDir = path.join(root, "bin", "downloaded");
13
+ const exe = process.platform === "win32" ? "electron-cli.exe" : "electron-cli";
14
+ const destination = path.join(installDir, exe);
15
+ const target = resolveTarget();
16
+
17
+ if (process.env.ELECTRON_CLI_SKIP_DOWNLOAD === "1") {
18
+ process.exit(0);
19
+ }
20
+
21
+ if (!target) {
22
+ warn(`No prebuilt binary is available for ${process.platform}/${process.arch}; Cargo fallback remains available.`);
23
+ process.exit(0);
24
+ }
25
+
26
+ const assetName = `electron-cli-v${version}-${target}${process.platform === "win32" ? ".exe" : ""}`;
27
+ const baseUrl = process.env.ELECTRON_CLI_DOWNLOAD_BASE_URL || "https://github.com/Ikana/electron-cli/releases/download";
28
+ const url = `${baseUrl}/v${version}/${assetName}`;
29
+ const tempFile = path.join(os.tmpdir(), `${assetName}.${process.pid}.tmp`);
30
+
31
+ main().catch((error) => {
32
+ warn(`Could not install prebuilt binary: ${error.message}`);
33
+
34
+ if (process.env.ELECTRON_CLI_STRICT_INSTALL === "1") {
35
+ process.exit(1);
36
+ }
37
+
38
+ warn("Install completed with Cargo fallback enabled.");
39
+ });
40
+
41
+ async function main() {
42
+ fs.mkdirSync(installDir, { recursive: true });
43
+
44
+ await download(url, tempFile);
45
+ fs.renameSync(tempFile, destination);
46
+
47
+ if (process.platform !== "win32") {
48
+ fs.chmodSync(destination, 0o755);
49
+ }
50
+
51
+ const result = spawnSync(destination, ["--version"], { encoding: "utf8" });
52
+ if (result.error || result.status !== 0) {
53
+ fs.rmSync(destination, { force: true });
54
+ throw new Error(result.error ? result.error.message : result.stderr.trim() || "downloaded binary failed verification");
55
+ }
56
+
57
+ console.error(`electron-cli installed prebuilt binary ${assetName}`);
58
+ }
59
+
60
+ function resolveTarget() {
61
+ const key = `${process.platform}-${process.arch}`;
62
+
63
+ return {
64
+ "darwin-arm64": "darwin-arm64",
65
+ "darwin-x64": "darwin-x64",
66
+ "linux-x64": "linux-x64",
67
+ "win32-x64": "win32-x64",
68
+ }[key];
69
+ }
70
+
71
+ function download(url, destinationPath) {
72
+ return new Promise((resolve, reject) => {
73
+ const request = https.get(url, (response) => {
74
+ if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
75
+ response.resume();
76
+ download(response.headers.location, destinationPath).then(resolve, reject);
77
+ return;
78
+ }
79
+
80
+ if (response.statusCode !== 200) {
81
+ response.resume();
82
+ reject(new Error(`download failed with HTTP ${response.statusCode}: ${url}`));
83
+ return;
84
+ }
85
+
86
+ const file = fs.createWriteStream(destinationPath, { mode: 0o755 });
87
+ response.pipe(file);
88
+ file.on("finish", () => file.close(resolve));
89
+ file.on("error", reject);
90
+ });
91
+
92
+ request.on("error", reject);
93
+ request.setTimeout(30_000, () => {
94
+ request.destroy(new Error("download timed out"));
95
+ });
96
+ });
97
+ }
98
+
99
+ function warn(message) {
100
+ console.error(`electron-cli postinstall: ${message}`);
101
+ }
package/src/cli.rs ADDED
@@ -0,0 +1,36 @@
1
+ use std::path::PathBuf;
2
+
3
+ use clap::{Args, Parser, Subcommand};
4
+
5
+ #[derive(Debug, Parser)]
6
+ #[command(
7
+ name = "electron-cli",
8
+ version,
9
+ about = "Experimental Rust CLI for Electron project diagnostics and workflow automation",
10
+ long_about = "electron-cli is an independent learning project for exploring Rust-native Electron tooling. It is not affiliated with Electron or Electron Forge."
11
+ )]
12
+ pub struct Cli {
13
+ #[command(subcommand)]
14
+ pub command: Commands,
15
+ }
16
+
17
+ #[derive(Debug, Subcommand)]
18
+ pub enum Commands {
19
+ /// Check whether the current project looks ready for Electron development.
20
+ Doctor(CommandArgs),
21
+ /// Print a structured snapshot of the current JavaScript/Electron project.
22
+ Inspect(CommandArgs),
23
+ /// Recommend next commands and risks from the project snapshot.
24
+ Plan(CommandArgs),
25
+ }
26
+
27
+ #[derive(Debug, Clone, Args)]
28
+ pub struct CommandArgs {
29
+ /// Project directory to inspect.
30
+ #[arg(long, default_value = ".", value_name = "PATH")]
31
+ pub cwd: PathBuf,
32
+
33
+ /// Emit machine-readable JSON.
34
+ #[arg(long)]
35
+ pub json: bool,
36
+ }
@@ -0,0 +1,355 @@
1
+ use std::process::Command;
2
+
3
+ use anyhow::Result;
4
+ use serde::Serialize;
5
+
6
+ use crate::{cli::CommandArgs, output, project::ProjectSnapshot};
7
+
8
+ #[derive(Debug, Serialize)]
9
+ struct DoctorReport {
10
+ project: ProjectSnapshot,
11
+ summary: DoctorSummary,
12
+ checks: Vec<Check>,
13
+ }
14
+
15
+ #[derive(Debug, Serialize)]
16
+ struct DoctorSummary {
17
+ pass: usize,
18
+ warn: usize,
19
+ fail: usize,
20
+ info: usize,
21
+ }
22
+
23
+ #[derive(Debug, Serialize)]
24
+ struct Check {
25
+ id: &'static str,
26
+ level: CheckLevel,
27
+ message: String,
28
+ detail: Option<String>,
29
+ remedy: Option<String>,
30
+ }
31
+
32
+ #[derive(Debug, Serialize, PartialEq, Eq)]
33
+ #[serde(rename_all = "lowercase")]
34
+ enum CheckLevel {
35
+ Pass,
36
+ Warn,
37
+ Fail,
38
+ Info,
39
+ }
40
+
41
+ pub fn run(args: CommandArgs) -> Result<()> {
42
+ let snapshot = crate::project::inspect(&args.cwd)?;
43
+ let report = build_report(snapshot);
44
+
45
+ if args.json {
46
+ output::json(&report)
47
+ } else {
48
+ print_report(&report);
49
+ Ok(())
50
+ }
51
+ }
52
+
53
+ fn build_report(snapshot: ProjectSnapshot) -> DoctorReport {
54
+ let mut checks = Vec::new();
55
+
56
+ checks.push(match &snapshot.package_json {
57
+ Some(path) => Check::pass(
58
+ "package-json",
59
+ "Found package.json",
60
+ Some(format!("Using {path}")),
61
+ ),
62
+ None => Check::fail(
63
+ "package-json",
64
+ "No package.json found",
65
+ Some("Run this command inside an Electron or JavaScript project.".to_string()),
66
+ Some("Create a package.json or pass --cwd PATH to an existing project.".to_string()),
67
+ ),
68
+ });
69
+
70
+ checks.push(match &snapshot.electron_dependency {
71
+ Some(version) => Check::pass(
72
+ "electron-dependency",
73
+ "Electron dependency is declared",
74
+ Some(format!("electron {version}")),
75
+ ),
76
+ None if snapshot.package_json.is_some() => Check::warn(
77
+ "electron-dependency",
78
+ "Electron dependency is not declared",
79
+ Some(
80
+ "This may be a generic JavaScript project, or Electron may be installed elsewhere."
81
+ .to_string(),
82
+ ),
83
+ Some(
84
+ "Install Electron with your package manager if this is an Electron app."
85
+ .to_string(),
86
+ ),
87
+ ),
88
+ None => Check::info(
89
+ "electron-dependency",
90
+ "Electron dependency could not be checked",
91
+ Some("No package.json was found.".to_string()),
92
+ ),
93
+ });
94
+
95
+ checks.push(match &snapshot.main {
96
+ Some(main) => Check::pass(
97
+ "main-entry",
98
+ "Main process entry is declared",
99
+ Some(format!("package.json main: {main}")),
100
+ ),
101
+ None if snapshot.electron_dependency.is_some() => Check::warn(
102
+ "main-entry",
103
+ "No package.json main field found",
104
+ Some("Electron apps usually need a main process entry.".to_string()),
105
+ Some(
106
+ "Add a main field, or document why your tooling supplies it another way."
107
+ .to_string(),
108
+ ),
109
+ ),
110
+ None => Check::info(
111
+ "main-entry",
112
+ "Main process entry is not declared",
113
+ Some("This only matters for Electron apps.".to_string()),
114
+ ),
115
+ });
116
+
117
+ checks.push(if snapshot.scripts.contains_key("start") || snapshot.scripts.contains_key("dev") {
118
+ Check::pass(
119
+ "dev-script",
120
+ "Development script is declared",
121
+ snapshot
122
+ .scripts
123
+ .get("start")
124
+ .or_else(|| snapshot.scripts.get("dev"))
125
+ .map(|script| format!("script: {script}")),
126
+ )
127
+ } else if snapshot.package_json.is_some() {
128
+ Check::warn(
129
+ "dev-script",
130
+ "No start or dev script found",
131
+ Some("A predictable development script makes the project easier for people and agents to run.".to_string()),
132
+ Some("Add a start or dev script to package.json.".to_string()),
133
+ )
134
+ } else {
135
+ Check::info(
136
+ "dev-script",
137
+ "Development scripts could not be checked",
138
+ Some("No package.json was found.".to_string()),
139
+ )
140
+ });
141
+
142
+ checks.push(if snapshot.package_manager.is_some() {
143
+ Check::pass(
144
+ "lockfile",
145
+ "Package manager lockfile detected",
146
+ snapshot.package_manager.clone(),
147
+ )
148
+ } else if snapshot.package_json.is_some() && snapshot.has_javascript_dependencies() {
149
+ Check::warn(
150
+ "lockfile",
151
+ "No package manager lockfile detected",
152
+ Some("Installs may not be reproducible without a lockfile.".to_string()),
153
+ Some("Run npm install, pnpm install, yarn install, or bun install.".to_string()),
154
+ )
155
+ } else if snapshot.package_json.is_some() {
156
+ Check::info(
157
+ "lockfile",
158
+ "No package manager lockfile detected",
159
+ Some("No JavaScript dependencies are declared, so a lockfile is optional.".to_string()),
160
+ )
161
+ } else {
162
+ Check::info(
163
+ "lockfile",
164
+ "Lockfile could not be checked",
165
+ Some("No package.json was found.".to_string()),
166
+ )
167
+ });
168
+
169
+ checks.push(if snapshot.forge_dependencies.is_empty() {
170
+ Check::info(
171
+ "forge",
172
+ "Electron Forge is not declared",
173
+ Some(
174
+ "That is fine; electron-cli can inspect projects that use other tooling."
175
+ .to_string(),
176
+ ),
177
+ )
178
+ } else {
179
+ Check::pass(
180
+ "forge",
181
+ "Electron Forge dependency detected",
182
+ Some(
183
+ snapshot
184
+ .forge_dependencies
185
+ .keys()
186
+ .cloned()
187
+ .collect::<Vec<_>>()
188
+ .join(", "),
189
+ ),
190
+ )
191
+ });
192
+
193
+ checks.push(command_check(
194
+ "node",
195
+ &["--version"],
196
+ "node-runtime",
197
+ "Node.js is available",
198
+ ));
199
+ checks.push(command_check(
200
+ "npm",
201
+ &["--version"],
202
+ "npm-cli",
203
+ "npm is available",
204
+ ));
205
+
206
+ let summary = DoctorSummary {
207
+ pass: checks
208
+ .iter()
209
+ .filter(|check| check.level == CheckLevel::Pass)
210
+ .count(),
211
+ warn: checks
212
+ .iter()
213
+ .filter(|check| check.level == CheckLevel::Warn)
214
+ .count(),
215
+ fail: checks
216
+ .iter()
217
+ .filter(|check| check.level == CheckLevel::Fail)
218
+ .count(),
219
+ info: checks
220
+ .iter()
221
+ .filter(|check| check.level == CheckLevel::Info)
222
+ .count(),
223
+ };
224
+
225
+ DoctorReport {
226
+ project: snapshot,
227
+ summary,
228
+ checks,
229
+ }
230
+ }
231
+
232
+ fn command_check(
233
+ command: &'static str,
234
+ args: &[&str],
235
+ id: &'static str,
236
+ success_message: &'static str,
237
+ ) -> Check {
238
+ match Command::new(command).args(args).output() {
239
+ Ok(output) if output.status.success() => {
240
+ let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
241
+ Check::pass(id, success_message, Some(format!("{command} {version}")))
242
+ }
243
+ Ok(output) => Check::fail(
244
+ id,
245
+ format!("{command} returned a non-zero exit code"),
246
+ Some(format!("exit status: {}", output.status)),
247
+ Some(format!("Install or repair {command}.")),
248
+ ),
249
+ Err(error) => Check::fail(
250
+ id,
251
+ format!("{command} could not be executed"),
252
+ Some(error.to_string()),
253
+ Some(format!("Install {command} and make sure it is on PATH.")),
254
+ ),
255
+ }
256
+ }
257
+
258
+ fn print_report(report: &DoctorReport) {
259
+ println!("electron-cli doctor");
260
+ println!();
261
+ println!("Project");
262
+ println!(" root: {}", report.project.root);
263
+ match report.project.package_label() {
264
+ Some(label) => println!(" package: {label}"),
265
+ None => println!(" package: not found"),
266
+ }
267
+
268
+ println!();
269
+ println!(
270
+ "Summary: {} pass, {} warn, {} fail, {} info",
271
+ report.summary.pass, report.summary.warn, report.summary.fail, report.summary.info
272
+ );
273
+ println!();
274
+ println!("Checks");
275
+
276
+ for check in &report.checks {
277
+ println!(
278
+ " {:<4} {:<20} {}",
279
+ check.level.as_str(),
280
+ check.id,
281
+ check.message
282
+ );
283
+
284
+ if let Some(detail) = &check.detail {
285
+ println!(" detail: {detail}");
286
+ }
287
+
288
+ if let Some(remedy) = &check.remedy {
289
+ println!(" remedy: {remedy}");
290
+ }
291
+ }
292
+ }
293
+
294
+ impl Check {
295
+ fn pass(id: &'static str, message: impl Into<String>, detail: Option<String>) -> Self {
296
+ Self {
297
+ id,
298
+ level: CheckLevel::Pass,
299
+ message: message.into(),
300
+ detail,
301
+ remedy: None,
302
+ }
303
+ }
304
+
305
+ fn warn(
306
+ id: &'static str,
307
+ message: impl Into<String>,
308
+ detail: Option<String>,
309
+ remedy: Option<String>,
310
+ ) -> Self {
311
+ Self {
312
+ id,
313
+ level: CheckLevel::Warn,
314
+ message: message.into(),
315
+ detail,
316
+ remedy,
317
+ }
318
+ }
319
+
320
+ fn fail(
321
+ id: &'static str,
322
+ message: impl Into<String>,
323
+ detail: Option<String>,
324
+ remedy: Option<String>,
325
+ ) -> Self {
326
+ Self {
327
+ id,
328
+ level: CheckLevel::Fail,
329
+ message: message.into(),
330
+ detail,
331
+ remedy,
332
+ }
333
+ }
334
+
335
+ fn info(id: &'static str, message: impl Into<String>, detail: Option<String>) -> Self {
336
+ Self {
337
+ id,
338
+ level: CheckLevel::Info,
339
+ message: message.into(),
340
+ detail,
341
+ remedy: None,
342
+ }
343
+ }
344
+ }
345
+
346
+ impl CheckLevel {
347
+ fn as_str(&self) -> &'static str {
348
+ match self {
349
+ CheckLevel::Pass => "PASS",
350
+ CheckLevel::Warn => "WARN",
351
+ CheckLevel::Fail => "FAIL",
352
+ CheckLevel::Info => "INFO",
353
+ }
354
+ }
355
+ }
@@ -0,0 +1,63 @@
1
+ use anyhow::Result;
2
+
3
+ use crate::{cli::CommandArgs, output, project};
4
+
5
+ pub fn run(args: CommandArgs) -> Result<()> {
6
+ let snapshot = project::inspect(&args.cwd)?;
7
+
8
+ if args.json {
9
+ output::json(&snapshot)
10
+ } else {
11
+ println!("electron-cli inspect");
12
+ println!();
13
+ println!("Project");
14
+ println!(" root: {}", snapshot.root);
15
+
16
+ match &snapshot.package_json {
17
+ Some(path) => println!(" package.json: {path}"),
18
+ None => println!(" package.json: not found"),
19
+ }
20
+
21
+ if let Some(package) = snapshot.package_label() {
22
+ println!(" package: {package}");
23
+ }
24
+
25
+ if let Some(package_manager) = &snapshot.package_manager {
26
+ println!(" package manager: {package_manager}");
27
+ }
28
+
29
+ println!();
30
+ println!("Electron");
31
+ match &snapshot.electron_dependency {
32
+ Some(version) => println!(" electron: {version}"),
33
+ None => println!(" electron: not declared"),
34
+ }
35
+
36
+ if snapshot.forge_dependencies.is_empty() {
37
+ println!(" forge: not declared");
38
+ } else {
39
+ println!(" forge:");
40
+ for (name, version) in &snapshot.forge_dependencies {
41
+ println!(" {name}: {version}");
42
+ }
43
+ }
44
+
45
+ if !snapshot.scripts.is_empty() {
46
+ println!();
47
+ println!("Scripts");
48
+ for (name, script) in &snapshot.scripts {
49
+ println!(" {name}: {script}");
50
+ }
51
+ }
52
+
53
+ if !snapshot.signals.is_empty() {
54
+ println!();
55
+ println!("Signals");
56
+ for signal in &snapshot.signals {
57
+ println!(" {signal}");
58
+ }
59
+ }
60
+
61
+ Ok(())
62
+ }
63
+ }
@@ -0,0 +1,3 @@
1
+ pub mod doctor;
2
+ pub mod inspect;
3
+ pub mod plan;