electron-cli 0.3.0-alpha.0 → 0.3.0-alpha.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/Cargo.lock +1 -1
- package/Cargo.toml +2 -2
- package/README.md +14 -5
- package/bin/electron-cli.js +6 -3
- package/package.json +7 -5
- package/scripts/install.js +101 -0
- package/src/cli.rs +66 -1
- package/src/commands/init.rs +398 -0
- package/src/commands/mod.rs +1 -0
- package/src/main.rs +1 -0
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "electron-cli"
|
|
3
|
-
version = "0.3.0-alpha.
|
|
3
|
+
version = "0.3.0-alpha.2"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "Experimental Rust CLI for Electron project diagnostics and workflow automation"
|
|
6
6
|
license = "MIT"
|
|
7
|
-
repository = "https://github.com/
|
|
7
|
+
repository = "https://github.com/Ikana/electron-cli"
|
|
8
8
|
|
|
9
9
|
[dependencies]
|
|
10
10
|
anyhow = "1.0"
|
package/README.md
CHANGED
|
@@ -14,17 +14,18 @@ Current commands:
|
|
|
14
14
|
electron-cli inspect
|
|
15
15
|
electron-cli doctor
|
|
16
16
|
electron-cli plan
|
|
17
|
+
electron-cli init my-app --dry-run
|
|
17
18
|
electron-cli inspect --json
|
|
18
19
|
electron-cli doctor --json
|
|
19
20
|
electron-cli plan --json
|
|
21
|
+
electron-cli init my-app --dry-run --json
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
Planned commands:
|
|
23
25
|
|
|
24
26
|
```sh
|
|
25
|
-
electron-cli dev
|
|
26
|
-
electron-cli init
|
|
27
27
|
electron-cli package
|
|
28
|
+
electron-cli dev
|
|
28
29
|
electron-cli make
|
|
29
30
|
```
|
|
30
31
|
|
|
@@ -32,13 +33,19 @@ The planned workflow commands may start by wrapping Electron Forge or other esta
|
|
|
32
33
|
|
|
33
34
|
## Install
|
|
34
35
|
|
|
35
|
-
During the experimental phase, the npm package
|
|
36
|
+
During the experimental phase, the npm package downloads a prebuilt binary from GitHub Releases when one is available. If a prebuilt binary is not available for your platform, it falls back to running from Rust source.
|
|
36
37
|
|
|
37
38
|
```sh
|
|
38
|
-
npm install -g electron-cli
|
|
39
|
+
npm install -g electron-cli@alpha
|
|
39
40
|
electron-cli doctor
|
|
40
41
|
```
|
|
41
42
|
|
|
43
|
+
To skip binary download and use the Cargo fallback:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
ELECTRON_CLI_SKIP_DOWNLOAD=1 npm install -g electron-cli@alpha
|
|
47
|
+
```
|
|
48
|
+
|
|
42
49
|
For local development:
|
|
43
50
|
|
|
44
51
|
```sh
|
|
@@ -53,6 +60,7 @@ Or use Cargo directly:
|
|
|
53
60
|
```sh
|
|
54
61
|
cargo run -- doctor
|
|
55
62
|
cargo run -- inspect --json
|
|
63
|
+
cargo run -- init my-app --dry-run
|
|
56
64
|
```
|
|
57
65
|
|
|
58
66
|
## Design Goals
|
|
@@ -71,8 +79,9 @@ cargo run -- inspect --json
|
|
|
71
79
|
|
|
72
80
|
## JSON Output
|
|
73
81
|
|
|
74
|
-
|
|
82
|
+
The inspection and planning commands support `--json` so agents and scripts can consume project state without scraping terminal output.
|
|
75
83
|
`plan` is designed around that workflow: it recommends stable commands and reports missing project conventions as structured data.
|
|
84
|
+
`init --dry-run --json` shows the exact create command before running any networked tooling.
|
|
76
85
|
|
|
77
86
|
```sh
|
|
78
87
|
electron-cli plan --json
|
package/bin/electron-cli.js
CHANGED
|
@@ -7,11 +7,14 @@ const path = require("node:path");
|
|
|
7
7
|
const root = path.resolve(__dirname, "..");
|
|
8
8
|
const exe = process.platform === "win32" ? "electron-cli.exe" : "electron-cli";
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
|
+
const envBinary = process.env.ELECTRON_CLI_BINARY;
|
|
10
11
|
|
|
11
12
|
const candidates = [
|
|
13
|
+
envBinary,
|
|
14
|
+
path.join(root, "bin", "downloaded", exe),
|
|
12
15
|
path.join(root, "target", "release", exe),
|
|
13
16
|
path.join(root, "target", "debug", exe),
|
|
14
|
-
];
|
|
17
|
+
].filter(Boolean);
|
|
15
18
|
|
|
16
19
|
const binary = candidates.find((candidate) => fs.existsSync(candidate));
|
|
17
20
|
|
|
@@ -28,8 +31,8 @@ if (!fs.existsSync(manifest)) {
|
|
|
28
31
|
process.exit(1);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
console.error("electron-cli
|
|
32
|
-
console.error("Building/running through
|
|
34
|
+
console.error("electron-cli could not find a prebuilt binary for this install.");
|
|
35
|
+
console.error("Building/running through Cargo fallback; install Rust from https://rustup.rs if this fails.");
|
|
33
36
|
|
|
34
37
|
exitWith(
|
|
35
38
|
spawnSync(cargo, ["run", "--quiet", "--manifest-path", manifest, "--", ...args], {
|
package/package.json
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "electron-cli",
|
|
3
|
-
"version": "0.3.0-alpha.
|
|
3
|
+
"version": "0.3.0-alpha.2",
|
|
4
4
|
"description": "Experimental Rust CLI for Electron project diagnostics and workflow automation",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/
|
|
8
|
+
"url": "git+https://github.com/Ikana/electron-cli.git"
|
|
9
9
|
},
|
|
10
|
-
"homepage": "https://github.com/
|
|
10
|
+
"homepage": "https://github.com/Ikana/electron-cli#readme",
|
|
11
11
|
"bugs": {
|
|
12
|
-
"url": "https://github.com/
|
|
12
|
+
"url": "https://github.com/Ikana/electron-cli/issues"
|
|
13
13
|
},
|
|
14
14
|
"bin": {
|
|
15
15
|
"electron-cli": "bin/electron-cli.js"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
-
"bin",
|
|
18
|
+
"bin/electron-cli.js",
|
|
19
|
+
"scripts",
|
|
19
20
|
"src",
|
|
20
21
|
"tests",
|
|
21
22
|
"Cargo.toml",
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"format": "cargo fmt",
|
|
31
32
|
"lint": "cargo fmt --check && cargo clippy --all-targets -- -D warnings",
|
|
32
33
|
"test": "cargo test",
|
|
34
|
+
"postinstall": "node scripts/install.js",
|
|
33
35
|
"prepack": "cargo build --release"
|
|
34
36
|
},
|
|
35
37
|
"engines": {
|
|
@@ -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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use std::path::PathBuf;
|
|
2
2
|
|
|
3
|
-
use clap::{Args, Parser, Subcommand};
|
|
3
|
+
use clap::{Args, Parser, Subcommand, ValueEnum};
|
|
4
4
|
|
|
5
5
|
#[derive(Debug, Parser)]
|
|
6
6
|
#[command(
|
|
@@ -18,6 +18,8 @@ pub struct Cli {
|
|
|
18
18
|
pub enum Commands {
|
|
19
19
|
/// Check whether the current project looks ready for Electron development.
|
|
20
20
|
Doctor(CommandArgs),
|
|
21
|
+
/// Bootstrap a new Electron app through the Electron Forge create tooling.
|
|
22
|
+
Init(InitArgs),
|
|
21
23
|
/// Print a structured snapshot of the current JavaScript/Electron project.
|
|
22
24
|
Inspect(CommandArgs),
|
|
23
25
|
/// Recommend next commands and risks from the project snapshot.
|
|
@@ -34,3 +36,66 @@ pub struct CommandArgs {
|
|
|
34
36
|
#[arg(long)]
|
|
35
37
|
pub json: bool,
|
|
36
38
|
}
|
|
39
|
+
|
|
40
|
+
#[derive(Debug, Clone, Args)]
|
|
41
|
+
pub struct InitArgs {
|
|
42
|
+
/// Directory to initialize. Defaults to the current directory.
|
|
43
|
+
#[arg(default_value = ".", value_name = "DIR")]
|
|
44
|
+
pub dir: PathBuf,
|
|
45
|
+
|
|
46
|
+
/// Directory to run the create command from.
|
|
47
|
+
#[arg(long, default_value = ".", value_name = "PATH")]
|
|
48
|
+
pub cwd: PathBuf,
|
|
49
|
+
|
|
50
|
+
/// Forge template to use.
|
|
51
|
+
#[arg(long, short = 't', default_value = "vite-typescript")]
|
|
52
|
+
pub template: String,
|
|
53
|
+
|
|
54
|
+
/// Package manager command strategy to use.
|
|
55
|
+
#[arg(long, value_enum)]
|
|
56
|
+
pub package_manager: Option<PackageManager>,
|
|
57
|
+
|
|
58
|
+
/// Set a specific Electron version, or use latest/beta/nightly.
|
|
59
|
+
#[arg(long, value_name = "VERSION")]
|
|
60
|
+
pub electron_version: Option<String>,
|
|
61
|
+
|
|
62
|
+
/// Copy template CI files when the Forge template supports them.
|
|
63
|
+
#[arg(long)]
|
|
64
|
+
pub copy_ci_files: bool,
|
|
65
|
+
|
|
66
|
+
/// Overwrite an existing target directory.
|
|
67
|
+
#[arg(long)]
|
|
68
|
+
pub force: bool,
|
|
69
|
+
|
|
70
|
+
/// Skip initializing a git repository in the created project.
|
|
71
|
+
#[arg(long)]
|
|
72
|
+
pub skip_git: bool,
|
|
73
|
+
|
|
74
|
+
/// Print the command and metadata without creating files.
|
|
75
|
+
#[arg(long)]
|
|
76
|
+
pub dry_run: bool,
|
|
77
|
+
|
|
78
|
+
/// Emit machine-readable JSON.
|
|
79
|
+
#[arg(long)]
|
|
80
|
+
pub json: bool,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#[derive(Debug, Clone, Copy, ValueEnum)]
|
|
84
|
+
#[value(rename_all = "lower")]
|
|
85
|
+
pub enum PackageManager {
|
|
86
|
+
Npm,
|
|
87
|
+
Pnpm,
|
|
88
|
+
Yarn,
|
|
89
|
+
Bun,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
impl PackageManager {
|
|
93
|
+
pub fn as_str(self) -> &'static str {
|
|
94
|
+
match self {
|
|
95
|
+
PackageManager::Npm => "npm",
|
|
96
|
+
PackageManager::Pnpm => "pnpm",
|
|
97
|
+
PackageManager::Yarn => "yarn",
|
|
98
|
+
PackageManager::Bun => "bun",
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
fs,
|
|
3
|
+
path::{Path, PathBuf},
|
|
4
|
+
process::Command,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
use anyhow::{bail, Context, Result};
|
|
8
|
+
use camino::Utf8PathBuf;
|
|
9
|
+
use serde::Serialize;
|
|
10
|
+
|
|
11
|
+
use crate::{
|
|
12
|
+
cli::{InitArgs, PackageManager},
|
|
13
|
+
output,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
#[derive(Debug, Serialize)]
|
|
17
|
+
struct InitReport {
|
|
18
|
+
cwd: Utf8PathBuf,
|
|
19
|
+
target_dir: Utf8PathBuf,
|
|
20
|
+
target_arg: String,
|
|
21
|
+
template: String,
|
|
22
|
+
package_manager: String,
|
|
23
|
+
dry_run: bool,
|
|
24
|
+
command: Vec<String>,
|
|
25
|
+
command_display: String,
|
|
26
|
+
post_create_files: Vec<String>,
|
|
27
|
+
next_steps: Vec<String>,
|
|
28
|
+
warnings: Vec<String>,
|
|
29
|
+
status: InitStatus,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[derive(Debug, Serialize)]
|
|
33
|
+
#[serde(rename_all = "kebab-case")]
|
|
34
|
+
enum InitStatus {
|
|
35
|
+
Planned,
|
|
36
|
+
Created,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[derive(Debug, Serialize)]
|
|
40
|
+
struct ElectronCliConfig {
|
|
41
|
+
version: &'static str,
|
|
42
|
+
generator: &'static str,
|
|
43
|
+
template: String,
|
|
44
|
+
package_manager: String,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub fn run(args: InitArgs) -> Result<()> {
|
|
48
|
+
let plan = build_plan(&args)?;
|
|
49
|
+
|
|
50
|
+
if args.dry_run {
|
|
51
|
+
return print_report(&plan, args.json);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ensure_target_can_be_created(&plan, args.force)?;
|
|
55
|
+
execute_plan(&plan)?;
|
|
56
|
+
write_project_config(&plan)?;
|
|
57
|
+
|
|
58
|
+
let report = InitReport {
|
|
59
|
+
status: InitStatus::Created,
|
|
60
|
+
..plan
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
print_report(&report, args.json)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn build_plan(args: &InitArgs) -> Result<InitReport> {
|
|
67
|
+
let cwd = args
|
|
68
|
+
.cwd
|
|
69
|
+
.canonicalize()
|
|
70
|
+
.with_context(|| format!("Could not resolve {}", args.cwd.display()))?;
|
|
71
|
+
|
|
72
|
+
let target_dir = if args.dir.is_absolute() {
|
|
73
|
+
args.dir.clone()
|
|
74
|
+
} else {
|
|
75
|
+
cwd.join(&args.dir)
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
let target_arg = path_arg(&args.dir);
|
|
79
|
+
let package_manager = args
|
|
80
|
+
.package_manager
|
|
81
|
+
.unwrap_or_else(|| detect_package_manager(&cwd));
|
|
82
|
+
let command = create_command(package_manager, &target_arg, args);
|
|
83
|
+
let command_display = display_command(&command);
|
|
84
|
+
let target_label = target_arg.clone();
|
|
85
|
+
|
|
86
|
+
let mut warnings = Vec::new();
|
|
87
|
+
if target_dir.exists() {
|
|
88
|
+
warnings.push(format!(
|
|
89
|
+
"Target directory already exists: {}",
|
|
90
|
+
target_dir.display()
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if target_dir.exists() && !args.force {
|
|
95
|
+
warnings
|
|
96
|
+
.push("Use --force to allow create-electron-app to overwrite the target.".to_string());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Ok(InitReport {
|
|
100
|
+
cwd: utf8_path(cwd)?,
|
|
101
|
+
target_dir: utf8_path(target_dir)?,
|
|
102
|
+
target_arg,
|
|
103
|
+
template: args.template.clone(),
|
|
104
|
+
package_manager: package_manager.as_str().to_string(),
|
|
105
|
+
dry_run: args.dry_run,
|
|
106
|
+
command,
|
|
107
|
+
command_display,
|
|
108
|
+
post_create_files: vec![".electron-cli.json".to_string()],
|
|
109
|
+
next_steps: vec![
|
|
110
|
+
format!("cd {target_label}"),
|
|
111
|
+
start_command(package_manager),
|
|
112
|
+
"electron-cli doctor --json".to_string(),
|
|
113
|
+
],
|
|
114
|
+
warnings,
|
|
115
|
+
status: InitStatus::Planned,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn create_command(
|
|
120
|
+
package_manager: PackageManager,
|
|
121
|
+
target_arg: &str,
|
|
122
|
+
args: &InitArgs,
|
|
123
|
+
) -> Vec<String> {
|
|
124
|
+
let mut command = match package_manager {
|
|
125
|
+
PackageManager::Npm => vec![
|
|
126
|
+
"npx".to_string(),
|
|
127
|
+
"-y".to_string(),
|
|
128
|
+
"create-electron-app@latest".to_string(),
|
|
129
|
+
target_arg.to_string(),
|
|
130
|
+
],
|
|
131
|
+
PackageManager::Pnpm => vec![
|
|
132
|
+
"pnpm".to_string(),
|
|
133
|
+
"dlx".to_string(),
|
|
134
|
+
"create-electron-app@latest".to_string(),
|
|
135
|
+
target_arg.to_string(),
|
|
136
|
+
],
|
|
137
|
+
PackageManager::Yarn => vec![
|
|
138
|
+
"yarn".to_string(),
|
|
139
|
+
"dlx".to_string(),
|
|
140
|
+
"create-electron-app@latest".to_string(),
|
|
141
|
+
target_arg.to_string(),
|
|
142
|
+
],
|
|
143
|
+
PackageManager::Bun => vec![
|
|
144
|
+
"bunx".to_string(),
|
|
145
|
+
"create-electron-app@latest".to_string(),
|
|
146
|
+
target_arg.to_string(),
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
command.push("--template".to_string());
|
|
151
|
+
command.push(args.template.clone());
|
|
152
|
+
|
|
153
|
+
if args.copy_ci_files {
|
|
154
|
+
command.push("--copy-ci-files".to_string());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if args.force {
|
|
158
|
+
command.push("--force".to_string());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if args.skip_git {
|
|
162
|
+
command.push("--skip-git".to_string());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if let Some(electron_version) = &args.electron_version {
|
|
166
|
+
command.push("--electron-version".to_string());
|
|
167
|
+
command.push(electron_version.clone());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
command
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fn execute_plan(plan: &InitReport) -> Result<()> {
|
|
174
|
+
let (program, args) = plan
|
|
175
|
+
.command
|
|
176
|
+
.split_first()
|
|
177
|
+
.context("Init command could not be constructed")?;
|
|
178
|
+
|
|
179
|
+
let status = Command::new(program)
|
|
180
|
+
.args(args)
|
|
181
|
+
.current_dir(&plan.cwd)
|
|
182
|
+
.status()
|
|
183
|
+
.with_context(|| format!("Could not execute {}", plan.command_display))?;
|
|
184
|
+
|
|
185
|
+
if !status.success() {
|
|
186
|
+
bail!(
|
|
187
|
+
"Init command failed with {status}: {}",
|
|
188
|
+
plan.command_display
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
Ok(())
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn ensure_target_can_be_created(plan: &InitReport, force: bool) -> Result<()> {
|
|
196
|
+
let target = Path::new(plan.target_dir.as_str());
|
|
197
|
+
|
|
198
|
+
if target.exists() && !force {
|
|
199
|
+
bail!(
|
|
200
|
+
"Target directory already exists: {}. Use --force to overwrite it.",
|
|
201
|
+
plan.target_dir
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
Ok(())
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fn write_project_config(plan: &InitReport) -> Result<()> {
|
|
209
|
+
let config = ElectronCliConfig {
|
|
210
|
+
version: env!("CARGO_PKG_VERSION"),
|
|
211
|
+
generator: "create-electron-app@latest",
|
|
212
|
+
template: plan.template.clone(),
|
|
213
|
+
package_manager: plan.package_manager.clone(),
|
|
214
|
+
};
|
|
215
|
+
let config_path = Path::new(plan.target_dir.as_str()).join(".electron-cli.json");
|
|
216
|
+
let json = serde_json::to_string_pretty(&config)?;
|
|
217
|
+
|
|
218
|
+
fs::write(&config_path, format!("{json}\n"))
|
|
219
|
+
.with_context(|| format!("Could not write {}", config_path.display()))?;
|
|
220
|
+
|
|
221
|
+
Ok(())
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
fn print_report(report: &InitReport, json: bool) -> Result<()> {
|
|
225
|
+
if json {
|
|
226
|
+
return output::json(report);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
println!("electron-cli init");
|
|
230
|
+
println!();
|
|
231
|
+
println!("Project");
|
|
232
|
+
println!(" cwd: {}", report.cwd);
|
|
233
|
+
println!(" target: {}", report.target_dir);
|
|
234
|
+
println!(" template: {}", report.template);
|
|
235
|
+
println!(" package manager: {}", report.package_manager);
|
|
236
|
+
println!(" status: {}", report.status.as_str());
|
|
237
|
+
|
|
238
|
+
println!();
|
|
239
|
+
println!("Command");
|
|
240
|
+
println!(" {}", report.command_display);
|
|
241
|
+
|
|
242
|
+
if !report.post_create_files.is_empty() {
|
|
243
|
+
println!();
|
|
244
|
+
println!("Post-Create Files");
|
|
245
|
+
for file in &report.post_create_files {
|
|
246
|
+
println!(" {file}");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if !report.next_steps.is_empty() {
|
|
251
|
+
println!();
|
|
252
|
+
println!("Next Steps");
|
|
253
|
+
for step in &report.next_steps {
|
|
254
|
+
println!(" {step}");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if !report.warnings.is_empty() {
|
|
259
|
+
println!();
|
|
260
|
+
println!("Warnings");
|
|
261
|
+
for warning in &report.warnings {
|
|
262
|
+
println!(" {warning}");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
Ok(())
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fn detect_package_manager(cwd: &Path) -> PackageManager {
|
|
270
|
+
if cwd.join("pnpm-lock.yaml").exists() {
|
|
271
|
+
PackageManager::Pnpm
|
|
272
|
+
} else if cwd.join("yarn.lock").exists() {
|
|
273
|
+
PackageManager::Yarn
|
|
274
|
+
} else if cwd.join("bun.lock").exists() || cwd.join("bun.lockb").exists() {
|
|
275
|
+
PackageManager::Bun
|
|
276
|
+
} else {
|
|
277
|
+
PackageManager::Npm
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fn start_command(package_manager: PackageManager) -> String {
|
|
282
|
+
match package_manager {
|
|
283
|
+
PackageManager::Npm => "npm start".to_string(),
|
|
284
|
+
PackageManager::Pnpm => "pnpm start".to_string(),
|
|
285
|
+
PackageManager::Yarn => "yarn start".to_string(),
|
|
286
|
+
PackageManager::Bun => "bun run start".to_string(),
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
fn display_command(command: &[String]) -> String {
|
|
291
|
+
command
|
|
292
|
+
.iter()
|
|
293
|
+
.map(|arg| shell_quote(arg))
|
|
294
|
+
.collect::<Vec<_>>()
|
|
295
|
+
.join(" ")
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fn shell_quote(value: &str) -> String {
|
|
299
|
+
if value
|
|
300
|
+
.chars()
|
|
301
|
+
.all(|char| char.is_ascii_alphanumeric() || matches!(char, '.' | '/' | '-' | '_' | '@'))
|
|
302
|
+
{
|
|
303
|
+
value.to_string()
|
|
304
|
+
} else {
|
|
305
|
+
format!("'{}'", value.replace('\'', "'\\''"))
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fn path_arg(path: &Path) -> String {
|
|
310
|
+
path.to_string_lossy().to_string()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
fn utf8_path(path: PathBuf) -> Result<Utf8PathBuf> {
|
|
314
|
+
Utf8PathBuf::from_path_buf(path).map_err(|path| {
|
|
315
|
+
anyhow::anyhow!(
|
|
316
|
+
"Path contains invalid UTF-8 and cannot be represented in JSON: {}",
|
|
317
|
+
path.display()
|
|
318
|
+
)
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
impl InitStatus {
|
|
323
|
+
fn as_str(&self) -> &'static str {
|
|
324
|
+
match self {
|
|
325
|
+
InitStatus::Planned => "planned",
|
|
326
|
+
InitStatus::Created => "created",
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#[cfg(test)]
|
|
332
|
+
mod tests {
|
|
333
|
+
use super::*;
|
|
334
|
+
|
|
335
|
+
#[test]
|
|
336
|
+
fn builds_default_npm_init_plan() {
|
|
337
|
+
let args = InitArgs {
|
|
338
|
+
dir: PathBuf::from("my-app"),
|
|
339
|
+
cwd: PathBuf::from(env!("CARGO_MANIFEST_DIR")),
|
|
340
|
+
template: "vite-typescript".to_string(),
|
|
341
|
+
package_manager: Some(PackageManager::Npm),
|
|
342
|
+
electron_version: None,
|
|
343
|
+
copy_ci_files: false,
|
|
344
|
+
force: false,
|
|
345
|
+
skip_git: true,
|
|
346
|
+
dry_run: true,
|
|
347
|
+
json: true,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
let plan = build_plan(&args).expect("plan should build");
|
|
351
|
+
|
|
352
|
+
assert_eq!(
|
|
353
|
+
plan.command,
|
|
354
|
+
vec![
|
|
355
|
+
"npx",
|
|
356
|
+
"-y",
|
|
357
|
+
"create-electron-app@latest",
|
|
358
|
+
"my-app",
|
|
359
|
+
"--template",
|
|
360
|
+
"vite-typescript",
|
|
361
|
+
"--skip-git",
|
|
362
|
+
]
|
|
363
|
+
);
|
|
364
|
+
assert_eq!(plan.package_manager, "npm");
|
|
365
|
+
assert_eq!(
|
|
366
|
+
plan.next_steps.first().map(String::as_str),
|
|
367
|
+
Some("cd my-app")
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
#[test]
|
|
372
|
+
fn includes_optional_create_flags() {
|
|
373
|
+
let args = InitArgs {
|
|
374
|
+
dir: PathBuf::from("desktop app"),
|
|
375
|
+
cwd: PathBuf::from(env!("CARGO_MANIFEST_DIR")),
|
|
376
|
+
template: "webpack".to_string(),
|
|
377
|
+
package_manager: Some(PackageManager::Pnpm),
|
|
378
|
+
electron_version: Some("latest".to_string()),
|
|
379
|
+
copy_ci_files: true,
|
|
380
|
+
force: true,
|
|
381
|
+
skip_git: true,
|
|
382
|
+
dry_run: true,
|
|
383
|
+
json: false,
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
let plan = build_plan(&args).expect("plan should build");
|
|
387
|
+
|
|
388
|
+
assert_eq!(plan.command[0], "pnpm");
|
|
389
|
+
assert!(plan.command.contains(&"--copy-ci-files".to_string()));
|
|
390
|
+
assert!(plan.command.contains(&"--force".to_string()));
|
|
391
|
+
assert!(plan.command.contains(&"--electron-version".to_string()));
|
|
392
|
+
assert!(plan.command_display.contains("'desktop app'"));
|
|
393
|
+
assert_eq!(
|
|
394
|
+
plan.next_steps.get(1).map(String::as_str),
|
|
395
|
+
Some("pnpm start")
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
package/src/commands/mod.rs
CHANGED
package/src/main.rs
CHANGED
|
@@ -19,6 +19,7 @@ fn run() -> Result<()> {
|
|
|
19
19
|
|
|
20
20
|
match cli.command {
|
|
21
21
|
Commands::Doctor(args) => commands::doctor::run(args),
|
|
22
|
+
Commands::Init(args) => commands::init::run(args),
|
|
22
23
|
Commands::Inspect(args) => commands::inspect::run(args),
|
|
23
24
|
Commands::Plan(args) => commands::plan::run(args),
|
|
24
25
|
}
|