Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/iii-worker/src/cli/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub struct AddArgs {
}

#[derive(Parser, Debug)]
#[command(name = "iii-worker", version, about = "iii managed worker runtime")]
#[command(name = "iii worker", bin_name = "iii worker", version, about = "iii managed worker runtime")]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
Expand Down
12 changes: 11 additions & 1 deletion crates/iii-worker/src/cli/local_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ pub async fn handle_local_add(path: &str, force: bool, reset_config: bool, brief
}

// 3. Detect language / project type
let _project = match load_project_info(&project_path) {
let project = match load_project_info(&project_path) {
Some(p) => p,
None => {
eprintln!(
Expand All @@ -296,6 +296,11 @@ pub async fn handle_local_add(path: &str, force: bool, reset_config: bool, brief
}
};

if let Err(msg) = project.validate() {
eprintln!("{} {}", "error:".red(), msg);
return 1;
}

// 4. Resolve worker name
let worker_name = resolve_worker_name(&project_path);

Expand Down Expand Up @@ -424,6 +429,11 @@ pub async fn start_local_worker(worker_name: &str, worker_path: &str, port: u16)
}
};

if let Err(msg) = project.validate() {
eprintln!("{} {}", "error:".red(), msg);
return 1;
}

let language = project.language.as_deref().unwrap_or("typescript");

// 3. Ensure libkrunfw available
Expand Down
123 changes: 122 additions & 1 deletion crates/iii-worker/src/cli/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@

//! Project auto-detection and manifest loading for worker dev sessions.

use colored::Colorize;
use std::collections::HashMap;

pub const WORKER_MANIFEST: &str = "iii.worker.yaml";

// Keep in sync with infer_scripts() match arms
const SUPPORTED_LANGUAGES: &[&str] = &["typescript", "python", "rust"];

pub struct ProjectInfo {
pub name: String,
pub language: Option<String>,
Expand All @@ -19,6 +23,28 @@ pub struct ProjectInfo {
pub env: HashMap<String, String>,
}

impl ProjectInfo {
pub fn validate(&self) -> Result<(), String> {
if let Some(ref lang) = self.language {
if !lang.is_empty() && !SUPPORTED_LANGUAGES.contains(&lang.as_str()) {
return Err(format!(
"unrecognized language '{}' in {} — supported: {}",
lang,
WORKER_MANIFEST,
SUPPORTED_LANGUAGES.join(", ")
));
}
}
if self.run_cmd.is_empty() {
return Err(format!(
"no run command could be determined — check {} for missing `scripts.start` or `runtime` section",
WORKER_MANIFEST
));
}
Ok(())
}
}

pub fn infer_scripts(
language: &str,
package_manager: &str,
Expand All @@ -30,11 +56,30 @@ pub fn infer_scripts(
"export PATH=$HOME/.bun/bin:$PATH && bun install".to_string(),
format!("export PATH=$HOME/.bun/bin:$PATH && bun {}", entry),
),
("typescript", "npm") | ("typescript", "yarn") | ("typescript", "pnpm") => (
("typescript", "npm") => (
"command -v node >/dev/null || (curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs)".to_string(),
"npm install".to_string(),
format!("npx tsx {}", entry),
),
// Note: pnpm exec and yarn exec require tsx in devDependencies (unlike npx which auto-installs)
("typescript", "pnpm") => (
"command -v node >/dev/null || (curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs)".to_string(),
"pnpm install".to_string(),
format!("pnpm exec tsx {}", entry),
),
("typescript", "yarn") => (
"command -v node >/dev/null || (curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs)".to_string(),
"yarn install".to_string(),
format!("yarn exec tsx {}", entry),
),
("typescript", other) => {
eprintln!(
"{} unrecognized package_manager '{}' for typescript — supported: npm, yarn, pnpm, bun",
"error:".red(),
other
);
(String::new(), String::new(), String::new())
},
("python", _) => (
"command -v python3 >/dev/null || (apt-get update && apt-get install -y python3-venv python3-pip)".to_string(),
"python3 -m venv .venv && .venv/bin/pip install -e .".to_string(),
Expand Down Expand Up @@ -98,6 +143,16 @@ pub fn load_from_manifest(manifest_path: &std::path::Path) -> Option<ProjectInfo
.to_string();
(setup, install, start)
} else {
if !language.is_empty() && package_manager.is_empty() {
eprintln!(
"{} missing `package_manager` in {}\n\n runtime:\n language: {}\n package_manager: npm <-- add this line\n entry: {}\n\nhint: valid options are: npm, yarn, pnpm, bun",
"error:".red(),
manifest_path.display(),
language,
entry
);
return None;
}
Comment on lines +146 to +155
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

package_manager is being required too broadly here.

At Line 146, !language.is_empty() forces package_manager for all runtime languages. That breaks valid Python/Rust manifests without scripts, and can mask unknown-language errors as “missing package_manager”.

💡 Proposed fix
-        if !language.is_empty() && package_manager.is_empty() {
+        if language == "typescript" && package_manager.is_empty() {
             eprintln!(
                 "{} missing `package_manager` in {}\n\n  runtime:\n    language: {}\n    package_manager: npm    <-- add this line\n    entry: {}\n\nhint: valid options are: npm, yarn, pnpm, bun",
                 "error:".red(),
                 manifest_path.display(),
                 language,
                 entry
             );
             return None;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/iii-worker/src/cli/project.rs` around lines 146 - 155, The current
check mistakenly requires package_manager whenever language is set (if
!language.is_empty() && package_manager.is_empty()), which forces
package_manager for non-JS runtimes; change this to only require package_manager
for JS-style runtimes. Replace the condition with a membership test (e.g.,
known_js_langs.contains(&language) && package_manager.is_empty()) or an explicit
equality check for the JS languages you support (e.g., language == "node" ||
language == "javascript" || language == "typescript"), and keep the existing
eprintln block (using manifest_path, language, entry) only for that JS-specific
branch; leave other languages to fall through or produce a separate
unknown-language message.

infer_scripts(language, package_manager, entry)
};

Expand Down Expand Up @@ -326,4 +381,70 @@ runtime:
let info = load_project_info(dir.path()).unwrap();
assert_eq!(info.name, "manifest-worker");
}

#[test]
fn infer_scripts_pnpm() {
let (setup, install, run) = infer_scripts("typescript", "pnpm", "src/index.ts");
assert!(setup.contains("nodejs") || setup.contains("nodesource"));
assert!(install.contains("pnpm install"));
assert!(run.contains("pnpm exec tsx src/index.ts"));
}

#[test]
fn infer_scripts_yarn() {
let (setup, install, run) = infer_scripts("typescript", "yarn", "src/index.ts");
assert!(setup.contains("nodejs") || setup.contains("nodesource"));
assert!(install.contains("yarn install"));
assert!(run.contains("yarn exec tsx src/index.ts"));
}

#[test]
fn infer_scripts_typescript_unknown_pm() {
let (setup, install, run) = infer_scripts("typescript", "deno", "src/index.ts");
assert!(setup.is_empty());
assert!(install.is_empty());
assert!(run.is_empty());
}

#[test]
fn load_manifest_missing_package_manager() {
let dir = tempfile::tempdir().unwrap();
let manifest_path = dir.path().join("iii.worker.yaml");
let yaml = r#"
name: broken-worker
runtime:
language: typescript
entry: src/index.ts
"#;
std::fs::write(&manifest_path, yaml).unwrap();
assert!(load_from_manifest(&manifest_path).is_none());
}

#[test]
fn validate_empty_run_cmd() {
let project = ProjectInfo {
name: "test".into(),
language: Some("typescript".into()),
setup_cmd: String::new(),
install_cmd: String::new(),
run_cmd: String::new(),
env: HashMap::new(),
};
assert!(project.validate().is_err());
}

#[test]
fn validate_unrecognized_language() {
let project = ProjectInfo {
name: "test".into(),
language: Some("typescirpt".into()),
setup_cmd: String::new(),
install_cmd: String::new(),
run_cmd: "node index.js".into(),
env: HashMap::new(),
};
let err = project.validate().unwrap_err();
assert!(err.contains("unrecognized language"));
assert!(err.contains("typescirpt"));
}
}
Loading