@engramresearch/srun 0.1.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.
- package/Cargo.lock +632 -0
- package/Cargo.toml +19 -0
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/npm/postinstall.js +18 -0
- package/npm/srun.js +36 -0
- package/package.json +39 -0
- package/src/cli.rs +135 -0
- package/src/detect.rs +232 -0
- package/src/exec.rs +80 -0
- package/src/lib.rs +10 -0
- package/src/main.rs +13 -0
- package/src/manifest.rs +81 -0
- package/src/model.rs +262 -0
- package/src/resolve.rs +269 -0
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# srun
|
|
2
|
+
|
|
3
|
+
`srun` is a Universal Smart Project Runner. It translates a developer intent into the concrete command for the current project.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
srun dev
|
|
7
|
+
srun build
|
|
8
|
+
srun installer
|
|
9
|
+
srun test
|
|
10
|
+
srun lint
|
|
11
|
+
srun format
|
|
12
|
+
srun info
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The goal is to reduce cognitive load when switching between projects, package managers, frameworks, and custom script names.
|
|
16
|
+
|
|
17
|
+
## Install locally
|
|
18
|
+
|
|
19
|
+
With Cargo:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cargo install --path .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
With npm from the project directory:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
After publishing:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g @engramresearch/srun
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Note: the npm package currently builds the Rust binary during install, so Rust/Cargo must be available on the target machine.
|
|
38
|
+
|
|
39
|
+
Or run during development:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cargo run -- info
|
|
43
|
+
cargo run -- dev --dry-run
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## What it detects
|
|
47
|
+
|
|
48
|
+
Package managers:
|
|
49
|
+
|
|
50
|
+
- `pnpm-lock.yaml` -> `pnpm`
|
|
51
|
+
- `bun.lockb` or `bun.lock` -> `bun`
|
|
52
|
+
- `yarn.lock` -> `yarn`
|
|
53
|
+
- `package-lock.json` -> `npm`
|
|
54
|
+
- `package.json` without lockfile -> `npm` with warning
|
|
55
|
+
|
|
56
|
+
If multiple lockfiles exist, priority is:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
pnpm > bun > yarn > npm
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Project markers:
|
|
63
|
+
|
|
64
|
+
- Electron: `electron`, `electron-builder`, `electron-vite`, `electron/`, `electron.vite.config.*`
|
|
65
|
+
- Tauri: `src-tauri/`, `tauri.conf.json`, tauri scripts/dependencies
|
|
66
|
+
- Next.js: `next.config.*` or `next`
|
|
67
|
+
- Vite: `vite.config.*` or `vite`
|
|
68
|
+
- TurboRepo: `turbo.json`
|
|
69
|
+
- NX: `nx.json`
|
|
70
|
+
- Monorepo: `apps/`, `packages/`, TurboRepo or NX markers
|
|
71
|
+
- Cargo-only: `Cargo.toml` without `package.json`
|
|
72
|
+
|
|
73
|
+
## Resolution examples
|
|
74
|
+
|
|
75
|
+
Next.js:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"scripts": {
|
|
80
|
+
"dev": "next dev"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
srun dev --dry-run
|
|
87
|
+
# pnpm run dev
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Electron:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"scripts": {
|
|
95
|
+
"dev": "vite",
|
|
96
|
+
"dev:electron": "electron-vite dev"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
srun dev --dry-run
|
|
103
|
+
# pnpm run dev:electron
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Tauri:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"scripts": {
|
|
111
|
+
"tauri:dev": "tauri dev"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
srun dev --dry-run
|
|
118
|
+
# pnpm run tauri:dev
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Cargo-only:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
srun dev --dry-run
|
|
125
|
+
# cargo run
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Info and verbose mode
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
srun info
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Prints project type, package manager, warnings, and resolved commands.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
srun dev --verbose --dry-run
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Shows detection and resolution phases before printing the command.
|
|
141
|
+
|
|
142
|
+
## Current limitations
|
|
143
|
+
|
|
144
|
+
- Monorepo scopes such as `srun dev web` are detected as a future extension but not fully implemented yet.
|
|
145
|
+
- Interactive fallback for custom scripts is not implemented; `srun` reports candidates instead of guessing.
|
|
146
|
+
- Colors and shell integration are intentionally omitted from the MVP.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { spawnSync } = require("node:child_process");
|
|
2
|
+
const { resolve } = require("node:path");
|
|
3
|
+
|
|
4
|
+
const root = resolve(__dirname, "..");
|
|
5
|
+
|
|
6
|
+
const result = spawnSync("cargo", ["build", "--release"], {
|
|
7
|
+
cwd: root,
|
|
8
|
+
stdio: "inherit",
|
|
9
|
+
shell: process.platform === "win32",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
if (result.error) {
|
|
13
|
+
console.error("srun: cargo is required to build the npm package binary.");
|
|
14
|
+
console.error(`srun: ${result.error.message}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
process.exit(result.status ?? 1);
|
package/npm/srun.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawnSync } = require("node:child_process");
|
|
3
|
+
const { existsSync } = require("node:fs");
|
|
4
|
+
const { join, resolve } = require("node:path");
|
|
5
|
+
|
|
6
|
+
const root = resolve(__dirname, "..");
|
|
7
|
+
const binary = join(root, "target", "release", process.platform === "win32" ? "srun.exe" : "srun");
|
|
8
|
+
|
|
9
|
+
if (!existsSync(binary)) {
|
|
10
|
+
const result = spawnSync("cargo", ["build", "--release"], {
|
|
11
|
+
cwd: root,
|
|
12
|
+
stdio: "inherit",
|
|
13
|
+
shell: process.platform === "win32",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (result.error) {
|
|
17
|
+
console.error(`srun: failed to build Rust binary: ${result.error.message}`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (result.status !== 0) {
|
|
22
|
+
process.exit(result.status ?? 1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = spawnSync(binary, process.argv.slice(2), {
|
|
27
|
+
cwd: process.cwd(),
|
|
28
|
+
stdio: "inherit",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (result.error) {
|
|
32
|
+
console.error(`srun: failed to execute binary: ${result.error.message}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
process.exit(result.status ?? 1);
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@engramresearch/srun",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Universal Smart Project Runner",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"srun": "npm/srun.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node npm/postinstall.js",
|
|
11
|
+
"build:binary": "cargo build --release",
|
|
12
|
+
"dev": "cargo run",
|
|
13
|
+
"build": "cargo build",
|
|
14
|
+
"test": "cargo test",
|
|
15
|
+
"lint": "cargo clippy",
|
|
16
|
+
"format": "cargo fmt"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"Cargo.toml",
|
|
20
|
+
"Cargo.lock",
|
|
21
|
+
"src",
|
|
22
|
+
"npm",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"cli",
|
|
28
|
+
"runner",
|
|
29
|
+
"devtools",
|
|
30
|
+
"rust",
|
|
31
|
+
"npm",
|
|
32
|
+
"pnpm",
|
|
33
|
+
"electron",
|
|
34
|
+
"tauri"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/cli.rs
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
use crate::detect::detect_project;
|
|
2
|
+
use crate::exec::run_command;
|
|
3
|
+
use crate::model::{Intent, ProjectInfo, ResolvedCommand, SrunError};
|
|
4
|
+
use crate::resolve::resolve_intent;
|
|
5
|
+
use clap::{Args, Parser, Subcommand};
|
|
6
|
+
use std::path::PathBuf;
|
|
7
|
+
|
|
8
|
+
#[derive(Debug, Parser)]
|
|
9
|
+
#[command(name = "srun", about = "Universal Smart Project Runner")]
|
|
10
|
+
pub struct Cli {
|
|
11
|
+
#[arg(long, global = true, default_value = ".")]
|
|
12
|
+
pub root: PathBuf,
|
|
13
|
+
|
|
14
|
+
#[command(subcommand)]
|
|
15
|
+
pub command: Command,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[derive(Debug, Subcommand)]
|
|
19
|
+
pub enum Command {
|
|
20
|
+
Dev(RunArgs),
|
|
21
|
+
Build(RunArgs),
|
|
22
|
+
Installer(RunArgs),
|
|
23
|
+
Test(RunArgs),
|
|
24
|
+
Lint(RunArgs),
|
|
25
|
+
Format(RunArgs),
|
|
26
|
+
Info,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[derive(Debug, Args, Clone, Copy)]
|
|
30
|
+
pub struct RunArgs {
|
|
31
|
+
#[arg(long)]
|
|
32
|
+
pub dry_run: bool,
|
|
33
|
+
|
|
34
|
+
#[arg(long)]
|
|
35
|
+
pub verbose: bool,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub fn run(cli: Cli) -> Result<i32, SrunError> {
|
|
39
|
+
let project = detect_project(&cli.root)?;
|
|
40
|
+
|
|
41
|
+
match cli.command {
|
|
42
|
+
Command::Info => {
|
|
43
|
+
print_info(&project);
|
|
44
|
+
Ok(0)
|
|
45
|
+
}
|
|
46
|
+
Command::Dev(args) => run_intent(&project, Intent::Dev, args),
|
|
47
|
+
Command::Build(args) => run_intent(&project, Intent::Build, args),
|
|
48
|
+
Command::Installer(args) => run_intent(&project, Intent::Installer, args),
|
|
49
|
+
Command::Test(args) => run_intent(&project, Intent::Test, args),
|
|
50
|
+
Command::Lint(args) => run_intent(&project, Intent::Lint, args),
|
|
51
|
+
Command::Format(args) => run_intent(&project, Intent::Format, args),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn run_intent(project: &ProjectInfo, intent: Intent, args: RunArgs) -> Result<i32, SrunError> {
|
|
56
|
+
if args.verbose {
|
|
57
|
+
print_detect(project);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let resolved = resolve_intent(project, intent)?;
|
|
61
|
+
|
|
62
|
+
if args.verbose {
|
|
63
|
+
println!("[resolve]");
|
|
64
|
+
println!("selected: {}", resolved.command.display());
|
|
65
|
+
println!("reason: {}", resolved.reason);
|
|
66
|
+
println!();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if args.dry_run {
|
|
70
|
+
println!("{}", resolved.command.display());
|
|
71
|
+
return Ok(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if args.verbose {
|
|
75
|
+
println!("[exec]");
|
|
76
|
+
println!("{}", resolved.command.display());
|
|
77
|
+
println!();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let status = run_command(&resolved.command, &project.root)?;
|
|
81
|
+
Ok(status.code().unwrap_or(1))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pub fn print_info(project: &ProjectInfo) {
|
|
85
|
+
println!("Project Type: {}", project.project_type());
|
|
86
|
+
println!(
|
|
87
|
+
"Package Manager: {}",
|
|
88
|
+
project
|
|
89
|
+
.package_manager
|
|
90
|
+
.map(|package_manager| package_manager.label())
|
|
91
|
+
.unwrap_or("none")
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if !project.warnings.is_empty() {
|
|
95
|
+
println!();
|
|
96
|
+
println!("Warnings:");
|
|
97
|
+
for warning in &project.warnings {
|
|
98
|
+
println!(" - {}", warning);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
println!();
|
|
103
|
+
println!("Resolved Commands:");
|
|
104
|
+
for intent in Intent::EXECUTABLE {
|
|
105
|
+
print_resolved_line(project, intent);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn print_resolved_line(project: &ProjectInfo, intent: Intent) {
|
|
110
|
+
match resolve_intent(project, intent) {
|
|
111
|
+
Ok(resolved) => println!("{}:\n {}", intent.label(), resolved.command.display()),
|
|
112
|
+
Err(error) => println!("{}:\n unavailable ({})", intent.label(), error),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn print_detect(project: &ProjectInfo) {
|
|
117
|
+
println!("[detect]");
|
|
118
|
+
for trace in &project.traces {
|
|
119
|
+
println!("{}", trace);
|
|
120
|
+
}
|
|
121
|
+
for warning in &project.warnings {
|
|
122
|
+
println!("warning: {}", warning);
|
|
123
|
+
}
|
|
124
|
+
println!("project type: {}", project.project_type());
|
|
125
|
+
println!();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[allow(dead_code)]
|
|
129
|
+
fn _format_resolved(resolved: &ResolvedCommand) -> String {
|
|
130
|
+
format!(
|
|
131
|
+
"{} -> {}",
|
|
132
|
+
resolved.intent.label(),
|
|
133
|
+
resolved.command.display()
|
|
134
|
+
)
|
|
135
|
+
}
|
package/src/detect.rs
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
use crate::manifest::read_package_json;
|
|
2
|
+
use crate::model::{Framework, PackageManager, ProjectInfo, SrunError};
|
|
3
|
+
use std::collections::BTreeSet;
|
|
4
|
+
use std::path::Path;
|
|
5
|
+
|
|
6
|
+
pub fn detect_project(root: &Path) -> Result<ProjectInfo, SrunError> {
|
|
7
|
+
let package_json = read_package_json(root)?;
|
|
8
|
+
let has_package_json = package_json.is_some();
|
|
9
|
+
let has_cargo_toml = root.join("Cargo.toml").exists();
|
|
10
|
+
let (package_manager, mut warnings, mut traces) =
|
|
11
|
+
detect_package_manager(root, has_package_json);
|
|
12
|
+
let mut frameworks = BTreeSet::new();
|
|
13
|
+
|
|
14
|
+
if let Some(package_json) = &package_json {
|
|
15
|
+
let deps = &package_json.dependencies;
|
|
16
|
+
let scripts = package_json
|
|
17
|
+
.scripts
|
|
18
|
+
.keys()
|
|
19
|
+
.map(String::as_str)
|
|
20
|
+
.collect::<BTreeSet<_>>();
|
|
21
|
+
|
|
22
|
+
if deps.contains("electron")
|
|
23
|
+
|| deps.contains("electron-builder")
|
|
24
|
+
|| deps.contains("electron-vite")
|
|
25
|
+
|| root.join("electron").is_dir()
|
|
26
|
+
|| has_file_prefix(root, "electron.vite.config.")
|
|
27
|
+
{
|
|
28
|
+
frameworks.insert(Framework::Electron);
|
|
29
|
+
traces.push("found electron markers".to_string());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if root.join("src-tauri").is_dir()
|
|
33
|
+
|| root.join("tauri.conf.json").exists()
|
|
34
|
+
|| deps.iter().any(|dep| dep.contains("tauri"))
|
|
35
|
+
|| scripts.iter().any(|script| script.contains("tauri"))
|
|
36
|
+
{
|
|
37
|
+
frameworks.insert(Framework::Tauri);
|
|
38
|
+
traces.push("found tauri markers".to_string());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if deps.contains("next") || has_file_prefix(root, "next.config.") {
|
|
42
|
+
frameworks.insert(Framework::Next);
|
|
43
|
+
traces.push("found next markers".to_string());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if deps.contains("vite") || has_file_prefix(root, "vite.config.") {
|
|
47
|
+
frameworks.insert(Framework::Vite);
|
|
48
|
+
traces.push("found vite markers".to_string());
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
if root.join("src-tauri").is_dir() || root.join("tauri.conf.json").exists() {
|
|
52
|
+
frameworks.insert(Framework::Tauri);
|
|
53
|
+
traces.push("found tauri markers".to_string());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if root.join("turbo.json").exists() {
|
|
58
|
+
frameworks.insert(Framework::Turbo);
|
|
59
|
+
traces.push("found turbo.json".to_string());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if root.join("nx.json").exists() {
|
|
63
|
+
frameworks.insert(Framework::Nx);
|
|
64
|
+
traces.push("found nx.json".to_string());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let is_monorepo = root.join("apps").is_dir()
|
|
68
|
+
|| root.join("packages").is_dir()
|
|
69
|
+
|| frameworks.contains(&Framework::Turbo)
|
|
70
|
+
|| frameworks.contains(&Framework::Nx);
|
|
71
|
+
|
|
72
|
+
if is_monorepo {
|
|
73
|
+
traces.push("found monorepo markers".to_string());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
warnings.retain(|warning| !warning.is_empty());
|
|
77
|
+
|
|
78
|
+
Ok(ProjectInfo {
|
|
79
|
+
root: root.to_path_buf(),
|
|
80
|
+
package_manager,
|
|
81
|
+
package_json,
|
|
82
|
+
frameworks,
|
|
83
|
+
has_cargo_toml,
|
|
84
|
+
has_package_json,
|
|
85
|
+
is_monorepo,
|
|
86
|
+
warnings,
|
|
87
|
+
traces,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fn detect_package_manager(
|
|
92
|
+
root: &Path,
|
|
93
|
+
has_package_json: bool,
|
|
94
|
+
) -> (Option<PackageManager>, Vec<String>, Vec<String>) {
|
|
95
|
+
let mut found = Vec::new();
|
|
96
|
+
let mut traces = Vec::new();
|
|
97
|
+
|
|
98
|
+
if root.join("pnpm-lock.yaml").exists() {
|
|
99
|
+
found.push(PackageManager::Pnpm);
|
|
100
|
+
traces.push("found pnpm-lock.yaml".to_string());
|
|
101
|
+
}
|
|
102
|
+
if root.join("bun.lockb").exists() || root.join("bun.lock").exists() {
|
|
103
|
+
found.push(PackageManager::Bun);
|
|
104
|
+
traces.push("found bun lockfile".to_string());
|
|
105
|
+
}
|
|
106
|
+
if root.join("yarn.lock").exists() {
|
|
107
|
+
found.push(PackageManager::Yarn);
|
|
108
|
+
traces.push("found yarn.lock".to_string());
|
|
109
|
+
}
|
|
110
|
+
if root.join("package-lock.json").exists() {
|
|
111
|
+
found.push(PackageManager::Npm);
|
|
112
|
+
traces.push("found package-lock.json".to_string());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let selected = [
|
|
116
|
+
PackageManager::Pnpm,
|
|
117
|
+
PackageManager::Bun,
|
|
118
|
+
PackageManager::Yarn,
|
|
119
|
+
PackageManager::Npm,
|
|
120
|
+
]
|
|
121
|
+
.into_iter()
|
|
122
|
+
.find(|package_manager| found.contains(package_manager))
|
|
123
|
+
.or_else(|| has_package_json.then_some(PackageManager::Npm));
|
|
124
|
+
|
|
125
|
+
let mut warnings = Vec::new();
|
|
126
|
+
if found.len() > 1 {
|
|
127
|
+
if let Some(selected) = selected {
|
|
128
|
+
warnings.push(format!(
|
|
129
|
+
"Multiple package managers detected. Using {}.",
|
|
130
|
+
selected.label()
|
|
131
|
+
));
|
|
132
|
+
}
|
|
133
|
+
} else if found.is_empty() && has_package_json {
|
|
134
|
+
warnings.push("No lockfile found. Using npm.".to_string());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
(selected, warnings, traces)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn has_file_prefix(root: &Path, prefix: &str) -> bool {
|
|
141
|
+
let Ok(entries) = std::fs::read_dir(root) else {
|
|
142
|
+
return false;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
entries.filter_map(Result::ok).any(|entry| {
|
|
146
|
+
entry
|
|
147
|
+
.file_name()
|
|
148
|
+
.to_str()
|
|
149
|
+
.map(|file_name| file_name.starts_with(prefix))
|
|
150
|
+
.unwrap_or(false)
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#[cfg(test)]
|
|
155
|
+
mod tests {
|
|
156
|
+
use super::detect_project;
|
|
157
|
+
use crate::model::{Framework, PackageManager};
|
|
158
|
+
use std::fs;
|
|
159
|
+
use tempfile::tempdir;
|
|
160
|
+
|
|
161
|
+
#[test]
|
|
162
|
+
fn selects_pnpm_over_other_lockfiles() {
|
|
163
|
+
let dir = tempdir().expect("tempdir");
|
|
164
|
+
fs::write(dir.path().join("package.json"), "{}").expect("package");
|
|
165
|
+
fs::write(dir.path().join("pnpm-lock.yaml"), "").expect("pnpm");
|
|
166
|
+
fs::write(dir.path().join("package-lock.json"), "").expect("npm");
|
|
167
|
+
|
|
168
|
+
let project = detect_project(dir.path()).expect("project");
|
|
169
|
+
|
|
170
|
+
assert_eq!(project.package_manager, Some(PackageManager::Pnpm));
|
|
171
|
+
assert!(project
|
|
172
|
+
.warnings
|
|
173
|
+
.iter()
|
|
174
|
+
.any(|warning| warning.contains("Multiple")));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn falls_back_to_npm_for_package_json_without_lock() {
|
|
179
|
+
let dir = tempdir().expect("tempdir");
|
|
180
|
+
fs::write(dir.path().join("package.json"), "{}").expect("package");
|
|
181
|
+
|
|
182
|
+
let project = detect_project(dir.path()).expect("project");
|
|
183
|
+
|
|
184
|
+
assert_eq!(project.package_manager, Some(PackageManager::Npm));
|
|
185
|
+
assert!(project
|
|
186
|
+
.warnings
|
|
187
|
+
.iter()
|
|
188
|
+
.any(|warning| warning.contains("No lockfile")));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn detects_electron_and_vite() {
|
|
193
|
+
let dir = tempdir().expect("tempdir");
|
|
194
|
+
fs::write(
|
|
195
|
+
dir.path().join("package.json"),
|
|
196
|
+
r#"{"dependencies":{"electron":"latest","vite":"latest"}}"#,
|
|
197
|
+
)
|
|
198
|
+
.expect("package");
|
|
199
|
+
|
|
200
|
+
let project = detect_project(dir.path()).expect("project");
|
|
201
|
+
|
|
202
|
+
assert!(project.frameworks.contains(&Framework::Electron));
|
|
203
|
+
assert!(project.frameworks.contains(&Framework::Vite));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#[test]
|
|
207
|
+
fn detects_tauri_next_and_monorepo() {
|
|
208
|
+
let dir = tempdir().expect("tempdir");
|
|
209
|
+
fs::write(
|
|
210
|
+
dir.path().join("package.json"),
|
|
211
|
+
r#"{"dependencies":{"next":"latest"},"scripts":{"tauri:dev":"tauri dev"}}"#,
|
|
212
|
+
)
|
|
213
|
+
.expect("package");
|
|
214
|
+
fs::create_dir(dir.path().join("apps")).expect("apps");
|
|
215
|
+
|
|
216
|
+
let project = detect_project(dir.path()).expect("project");
|
|
217
|
+
|
|
218
|
+
assert!(project.frameworks.contains(&Framework::Tauri));
|
|
219
|
+
assert!(project.frameworks.contains(&Framework::Next));
|
|
220
|
+
assert!(project.is_monorepo);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#[test]
|
|
224
|
+
fn detects_cargo_only() {
|
|
225
|
+
let dir = tempdir().expect("tempdir");
|
|
226
|
+
fs::write(dir.path().join("Cargo.toml"), "[package]\nname='x'").expect("cargo");
|
|
227
|
+
|
|
228
|
+
let project = detect_project(dir.path()).expect("project");
|
|
229
|
+
|
|
230
|
+
assert!(project.is_cargo_only());
|
|
231
|
+
}
|
|
232
|
+
}
|
package/src/exec.rs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
use crate::model::{CommandSpec, SrunError};
|
|
2
|
+
use std::io;
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
use std::process::{Command, ExitStatus, Stdio};
|
|
5
|
+
|
|
6
|
+
pub fn run_command(command: &CommandSpec, cwd: &Path) -> Result<ExitStatus, SrunError> {
|
|
7
|
+
spawn_command(command, cwd).map_err(|source| SrunError::ProcessSpawn {
|
|
8
|
+
command: command.display(),
|
|
9
|
+
source,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fn spawn_command(command: &CommandSpec, cwd: &Path) -> io::Result<ExitStatus> {
|
|
14
|
+
match spawn_program(&command.program, &command.args, cwd) {
|
|
15
|
+
Ok(status) => Ok(status),
|
|
16
|
+
Err(error) if should_try_windows_cmd_shim(&command.program, &error) => {
|
|
17
|
+
spawn_program(&format!("{}.cmd", command.program), &command.args, cwd)
|
|
18
|
+
}
|
|
19
|
+
Err(error) => Err(error),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn spawn_program(program: &str, args: &[String], cwd: &Path) -> io::Result<ExitStatus> {
|
|
24
|
+
Command::new(program)
|
|
25
|
+
.args(args)
|
|
26
|
+
.current_dir(cwd)
|
|
27
|
+
.stdin(Stdio::inherit())
|
|
28
|
+
.stdout(Stdio::inherit())
|
|
29
|
+
.stderr(Stdio::inherit())
|
|
30
|
+
.status()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn should_try_windows_cmd_shim(program: &str, error: &io::Error) -> bool {
|
|
34
|
+
cfg!(windows)
|
|
35
|
+
&& error.kind() == io::ErrorKind::NotFound
|
|
36
|
+
&& matches!(program, "npm" | "pnpm" | "yarn" | "bun" | "npx")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[cfg(test)]
|
|
40
|
+
mod tests {
|
|
41
|
+
use super::should_try_windows_cmd_shim;
|
|
42
|
+
use crate::model::{CommandSpec, PackageManager};
|
|
43
|
+
use std::io;
|
|
44
|
+
|
|
45
|
+
#[test]
|
|
46
|
+
fn builds_package_manager_commands_without_shell() {
|
|
47
|
+
assert_eq!(
|
|
48
|
+
PackageManager::Pnpm.script_command("dev").display(),
|
|
49
|
+
"pnpm run dev"
|
|
50
|
+
);
|
|
51
|
+
assert_eq!(
|
|
52
|
+
PackageManager::Npm.script_command("dev").display(),
|
|
53
|
+
"npm run dev"
|
|
54
|
+
);
|
|
55
|
+
assert_eq!(
|
|
56
|
+
PackageManager::Bun.script_command("dev").display(),
|
|
57
|
+
"bun run dev"
|
|
58
|
+
);
|
|
59
|
+
assert_eq!(
|
|
60
|
+
PackageManager::Yarn.script_command("dev").display(),
|
|
61
|
+
"yarn dev"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn displays_program_without_args() {
|
|
67
|
+
assert_eq!(
|
|
68
|
+
CommandSpec::new("srun", std::iter::empty::<&str>()).display(),
|
|
69
|
+
"srun"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[test]
|
|
74
|
+
fn windows_shim_fallback_only_applies_to_package_managers() {
|
|
75
|
+
let error = io::Error::new(io::ErrorKind::NotFound, "missing");
|
|
76
|
+
|
|
77
|
+
assert_eq!(should_try_windows_cmd_shim("npm", &error), cfg!(windows));
|
|
78
|
+
assert!(!should_try_windows_cmd_shim("cargo", &error));
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/lib.rs
ADDED