@engramresearch/srun 0.1.0 → 0.1.3

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/src/cli.rs DELETED
@@ -1,135 +0,0 @@
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 DELETED
@@ -1,232 +0,0 @@
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 DELETED
@@ -1,80 +0,0 @@
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 DELETED
@@ -1,10 +0,0 @@
1
- pub mod cli;
2
- pub mod detect;
3
- pub mod exec;
4
- pub mod manifest;
5
- pub mod model;
6
- pub mod resolve;
7
-
8
- pub use detect::detect_project;
9
- pub use model::{Intent, ProjectInfo, ResolvedCommand, SrunError};
10
- pub use resolve::resolve_intent;
package/src/main.rs DELETED
@@ -1,13 +0,0 @@
1
- use clap::Parser;
2
- use srun::cli::{run, Cli};
3
-
4
- fn main() {
5
- let cli = Cli::parse();
6
- match run(cli) {
7
- Ok(code) => std::process::exit(code),
8
- Err(error) => {
9
- eprintln!("error: {}", error);
10
- std::process::exit(1);
11
- }
12
- }
13
- }
package/src/manifest.rs DELETED
@@ -1,81 +0,0 @@
1
- use crate::model::{PackageJson, SrunError};
2
- use serde::Deserialize;
3
- use std::collections::{BTreeMap, BTreeSet};
4
- use std::fs;
5
- use std::path::Path;
6
-
7
- #[derive(Debug, Deserialize, Default)]
8
- struct RawPackageJson {
9
- #[serde(default)]
10
- scripts: BTreeMap<String, String>,
11
- #[serde(default)]
12
- dependencies: BTreeMap<String, serde_json::Value>,
13
- #[serde(default, rename = "devDependencies")]
14
- dev_dependencies: BTreeMap<String, serde_json::Value>,
15
- }
16
-
17
- pub fn read_package_json(root: &Path) -> Result<Option<PackageJson>, SrunError> {
18
- let path = root.join("package.json");
19
- if !path.exists() {
20
- return Ok(None);
21
- }
22
-
23
- let content = fs::read_to_string(&path).map_err(|source| SrunError::Io {
24
- path: path.clone(),
25
- source,
26
- })?;
27
- let raw: RawPackageJson = serde_json::from_str(&content).map_err(|source| SrunError::Json {
28
- path: path.clone(),
29
- source,
30
- })?;
31
-
32
- let dependencies = raw
33
- .dependencies
34
- .into_keys()
35
- .chain(raw.dev_dependencies.into_keys())
36
- .collect::<BTreeSet<_>>();
37
-
38
- Ok(Some(PackageJson {
39
- scripts: raw.scripts,
40
- dependencies,
41
- }))
42
- }
43
-
44
- #[cfg(test)]
45
- mod tests {
46
- use super::read_package_json;
47
- use std::fs;
48
- use tempfile::tempdir;
49
-
50
- #[test]
51
- fn reads_scripts_and_dependencies() {
52
- let dir = tempdir().expect("tempdir");
53
- fs::write(
54
- dir.path().join("package.json"),
55
- r#"{
56
- "scripts": { "dev": "vite" },
57
- "dependencies": { "vite": "latest" },
58
- "devDependencies": { "electron": "latest" }
59
- }"#,
60
- )
61
- .expect("write package");
62
-
63
- let package = read_package_json(dir.path())
64
- .expect("read package")
65
- .expect("package");
66
-
67
- assert_eq!(package.scripts.get("dev"), Some(&"vite".to_string()));
68
- assert!(package.dependencies.contains("vite"));
69
- assert!(package.dependencies.contains("electron"));
70
- }
71
-
72
- #[test]
73
- fn reports_invalid_json() {
74
- let dir = tempdir().expect("tempdir");
75
- fs::write(dir.path().join("package.json"), "{").expect("write package");
76
-
77
- let error = read_package_json(dir.path()).expect_err("invalid json should fail");
78
-
79
- assert!(error.to_string().contains("failed to parse"));
80
- }
81
- }