1use indexmap::IndexMap;
8use std::path::{Path, PathBuf};
9
10use super::lockfile::Lockfile;
11use super::manifest::{DepSpec, DetailedDep, Manifest, PackageMeta};
12use super::resolver::Resolver;
13use super::store::Store;
14use super::PkgResult;
15
16pub const MANIFEST_FILE: &str = "stryke.toml";
18pub const LOCKFILE_FILE: &str = "stryke.lock";
20
21pub fn find_project_root(start: &Path) -> Option<PathBuf> {
25 let mut cur = start.to_path_buf();
26 loop {
27 if cur.join(MANIFEST_FILE).is_file() {
28 return Some(cur);
29 }
30 if !cur.pop() {
31 return None;
32 }
33 }
34}
35
36fn is_help_flag(arg: &str) -> bool {
38 arg == "-h" || arg == "--help"
39}
40
41fn print_new_help() {
42 println!("usage: stryke new NAME");
43 println!();
44 println!("Scaffold a new stryke project at ./NAME/. Same layout as `stryke init`,");
45 println!("but creates the directory for you.");
46 println!();
47 println!("The new project gets:");
48 println!(" NAME/stryke.toml manifest with [package] and [bin]");
49 println!(" NAME/main.stk entry point");
50 println!(" NAME/lib/ library modules (used by `use Foo::Bar`)");
51 println!(" NAME/t/ test files (run with `s test`)");
52 println!(" NAME/benches/ benchmark files (run with `s bench`)");
53 println!(" NAME/bin/ additional executables");
54 println!(" NAME/examples/ example programs");
55 println!(" NAME/.gitignore ignores target/");
56}
57
58fn print_init_help() {
59 println!("usage: stryke init [NAME]");
60 println!();
61 println!("Scaffold the current directory as a stryke project. NAME defaults to the");
62 println!("cwd's basename. Writes stryke.toml + main.stk + lib/, t/, benches/, bin/,");
63 println!("examples/, .gitignore. Existing files are left alone.");
64}
65
66pub fn cmd_new(name: &str) -> i32 {
68 if is_help_flag(name) {
69 print_new_help();
70 return 0;
71 }
72 let project_dir = PathBuf::from(name);
73 if project_dir.exists() {
74 eprintln!("s new: {} already exists", name);
75 return 1;
76 }
77 if let Err(e) = std::fs::create_dir_all(&project_dir) {
78 eprintln!("s new: create {}: {}", project_dir.display(), e);
79 return 1;
80 }
81 scaffold_project(&project_dir, name)
82}
83
84pub fn cmd_init(name: Option<&str>) -> i32 {
87 if matches!(name, Some(n) if is_help_flag(n)) {
88 print_init_help();
89 return 0;
90 }
91 let cwd = match std::env::current_dir() {
92 Ok(c) => c,
93 Err(e) => {
94 eprintln!("s init: cwd: {}", e);
95 return 1;
96 }
97 };
98 let resolved_name = name
99 .map(|s| s.to_string())
100 .or_else(|| cwd.file_name().map(|n| n.to_string_lossy().into_owned()))
101 .unwrap_or_else(|| "stryke_project".to_string());
102 scaffold_project(&cwd, &resolved_name)
103}
104
105fn scaffold_project(project_dir: &Path, name: &str) -> i32 {
107 let mut created: Vec<String> = Vec::new();
108
109 let manifest_path = project_dir.join(MANIFEST_FILE);
110 if !manifest_path.exists() {
111 let m = default_manifest_for(name);
112 let s = match m.to_toml_string() {
113 Ok(s) => s,
114 Err(e) => {
115 eprintln!("s init: {}", e);
116 return 1;
117 }
118 };
119 if let Err(e) = std::fs::write(&manifest_path, s) {
120 eprintln!("s init: write {}: {}", manifest_path.display(), e);
121 return 1;
122 }
123 created.push(manifest_path.display().to_string());
124 }
125
126 let main_path = project_dir.join("main.stk");
127 if !main_path.exists() {
128 let body = format!("#!/usr/bin/env stryke\n\np \"hello from {}!\"\n", name);
129 if let Err(e) = std::fs::write(&main_path, body) {
130 eprintln!("s init: write {}: {}", main_path.display(), e);
131 return 1;
132 }
133 created.push(main_path.display().to_string());
134 }
135
136 for sub in ["lib", "t", "benches", "bin", "examples"] {
137 let d = project_dir.join(sub);
138 if !d.exists() {
139 if let Err(e) = std::fs::create_dir_all(&d) {
140 eprintln!("s init: mkdir {}: {}", d.display(), e);
141 return 1;
142 }
143 created.push(format!("{}/", d.display()));
144 }
145 }
146
147 let test_path = project_dir.join("t/test_main.stk");
148 if !test_path.exists() {
149 let body = "#!/usr/bin/env stryke\n\nuse Test\n\nok 1, \"it works\"\n\ndone_testing()\n";
150 if let Err(e) = std::fs::write(&test_path, body) {
151 eprintln!("s init: write {}: {}", test_path.display(), e);
152 return 1;
153 }
154 created.push(test_path.display().to_string());
155 }
156
157 let gi = project_dir.join(".gitignore");
158 if !gi.exists() {
159 let body = "# stryke build artifacts\n/target/\n";
160 if let Err(e) = std::fs::write(&gi, body) {
161 eprintln!("s init: write {}: {}", gi.display(), e);
162 return 1;
163 }
164 created.push(gi.display().to_string());
165 }
166
167 for c in &created {
168 eprintln!(" created {}", c);
169 }
170 eprintln!("\x1b[32m✓ Initialized stryke project `{}`\x1b[0m", name);
171 eprintln!();
172 eprintln!(" s install # populate stryke.lock from stryke.toml");
173 eprintln!(" s run # run main.stk");
174 eprintln!(" s test # run tests in t/");
175 0
176}
177
178fn default_manifest_for(name: &str) -> Manifest {
179 let mut bin = IndexMap::new();
180 bin.insert(name.to_string(), "main.stk".to_string());
181 Manifest {
182 package: Some(PackageMeta {
183 name: name.to_string(),
184 version: "0.1.0".to_string(),
185 description: String::new(),
186 authors: Vec::new(),
187 license: String::new(),
188 repository: String::new(),
189 edition: "2026".to_string(),
190 }),
191 bin,
192 ..Manifest::default()
193 }
194}
195
196pub fn cmd_add(args: &[String]) -> i32 {
200 if args.iter().any(|a| is_help_flag(a)) {
201 println!(
202 "usage: stryke add NAME[@VER] [--dev | --group=NAME] [--path=DIR] [--features=A,B]"
203 );
204 println!();
205 println!("Add a dependency to stryke.toml and run `s install` to refresh stryke.lock.");
206 println!();
207 println!("Flags:");
208 println!(" --dev add as a [dev-deps] entry instead of [deps]");
209 println!(" --group=NAME add to [groups.NAME] (bundler-style)");
210 println!(" --path=DIR depend on a local checkout (no registry needed)");
211 println!(" --features=A,B enable feature flags A and B for this dep");
212 println!();
213 println!("Examples:");
214 println!(" stryke add http@1.0");
215 println!(" stryke add test-utils --dev");
216 println!(" stryke add criterion --group=bench");
217 println!(" stryke add mylib --path=../mylib");
218 return 0;
219 }
220 let parsed = match parse_add_args(args) {
221 Ok(p) => p,
222 Err(msg) => {
223 eprintln!("s add: {}", msg);
224 return 1;
225 }
226 };
227
228 let cwd = match std::env::current_dir() {
229 Ok(c) => c,
230 Err(e) => {
231 eprintln!("s add: cwd: {}", e);
232 return 1;
233 }
234 };
235 let root = match find_project_root(&cwd) {
236 Some(r) => r,
237 None => {
238 eprintln!("s add: no stryke.toml found in this directory or any parent");
239 return 1;
240 }
241 };
242
243 let manifest_path = root.join(MANIFEST_FILE);
244 let mut manifest = match Manifest::from_path(&manifest_path) {
245 Ok(m) => m,
246 Err(e) => {
247 eprintln!("s add: {}", e);
248 return 1;
249 }
250 };
251
252 let target_map: &mut IndexMap<String, DepSpec> = match &parsed.kind {
253 AddKind::Runtime => &mut manifest.deps,
254 AddKind::Dev => &mut manifest.dev_deps,
255 AddKind::Group(g) => manifest.groups.entry(g.clone()).or_default(),
256 };
257 target_map.insert(parsed.name.clone(), parsed.spec.clone());
258
259 let body = match manifest.to_toml_string() {
260 Ok(s) => s,
261 Err(e) => {
262 eprintln!("s add: {}", e);
263 return 1;
264 }
265 };
266 if let Err(e) = std::fs::write(&manifest_path, body) {
267 eprintln!("s add: write {}: {}", manifest_path.display(), e);
268 return 1;
269 }
270 eprintln!(
271 " added {}{} = {}",
272 parsed.name,
273 match &parsed.kind {
274 AddKind::Runtime => "".to_string(),
275 AddKind::Dev => " (dev)".to_string(),
276 AddKind::Group(g) => format!(" (group:{})", g),
277 },
278 format_dep_for_log(&parsed.spec)
279 );
280
281 cmd_install(&[])
284}
285
286struct AddArgs {
287 name: String,
288 spec: DepSpec,
289 kind: AddKind,
290}
291
292enum AddKind {
293 Runtime,
294 Dev,
295 Group(String),
296}
297
298fn parse_add_args(args: &[String]) -> Result<AddArgs, String> {
299 if args.is_empty() {
300 return Err("usage: s add NAME[@VER] [--dev|--group=NAME] [--path=DIR]".into());
301 }
302 let mut positional: Vec<&String> = Vec::new();
303 let mut kind = AddKind::Runtime;
304 let mut path_override: Option<String> = None;
305 let mut features: Vec<String> = Vec::new();
306 for a in args {
307 match a.as_str() {
308 "--dev" => kind = AddKind::Dev,
309 s if s.starts_with("--group=") => {
310 kind = AddKind::Group(s["--group=".len()..].to_string())
311 }
312 s if s.starts_with("--path=") => path_override = Some(s["--path=".len()..].to_string()),
313 s if s.starts_with("--features=") => {
314 features = s["--features=".len()..]
315 .split(',')
316 .map(|s| s.trim().to_string())
317 .filter(|s| !s.is_empty())
318 .collect();
319 }
320 s if s.starts_with("--") => {
321 return Err(format!("unknown flag {}", s));
322 }
323 _ => positional.push(a),
324 }
325 }
326 if positional.len() != 1 {
327 return Err(format!(
328 "expected exactly one NAME[@VER] argument, got {}",
329 positional.len()
330 ));
331 }
332 let raw = positional[0].as_str();
333 let (name, version) = match raw.split_once('@') {
334 Some((n, v)) => (n.to_string(), Some(v.to_string())),
335 None => (raw.to_string(), None),
336 };
337 let spec = if let Some(p) = path_override {
338 DepSpec::Detailed(DetailedDep {
339 path: Some(p),
340 version,
341 features,
342 default_features: true,
343 ..DetailedDep::default()
344 })
345 } else if !features.is_empty() {
346 DepSpec::Detailed(DetailedDep {
347 version: Some(version.clone().unwrap_or_else(|| "*".to_string())),
348 features,
349 default_features: true,
350 ..DetailedDep::default()
351 })
352 } else {
353 DepSpec::Version(version.unwrap_or_else(|| "*".to_string()))
354 };
355 Ok(AddArgs { name, spec, kind })
356}
357
358fn format_dep_for_log(spec: &DepSpec) -> String {
359 match spec {
360 DepSpec::Version(v) => format!("\"{}\"", v),
361 DepSpec::Detailed(d) => {
362 let mut bits = Vec::new();
363 if let Some(v) = &d.version {
364 bits.push(format!("version = \"{}\"", v));
365 }
366 if let Some(p) = &d.path {
367 bits.push(format!("path = \"{}\"", p));
368 }
369 if let Some(g) = &d.git {
370 bits.push(format!("git = \"{}\"", g));
371 }
372 if !d.features.is_empty() {
373 bits.push(format!("features = {:?}", d.features));
374 }
375 format!("{{ {} }}", bits.join(", "))
376 }
377 DepSpec::Placeholder => "<placeholder>".into(),
378 }
379}
380
381pub fn cmd_remove(args: &[String]) -> i32 {
383 if args.iter().any(|a| is_help_flag(a)) {
384 println!("usage: stryke remove NAME");
385 println!();
386 println!("Drop NAME from stryke.toml ([deps], [dev-deps], or [groups.*]) and");
387 println!("rerun `s install` so stryke.lock matches.");
388 return 0;
389 }
390 if args.len() != 1 {
391 eprintln!("usage: s remove NAME");
392 return 1;
393 }
394 let name = &args[0];
395 let cwd = match std::env::current_dir() {
396 Ok(c) => c,
397 Err(e) => {
398 eprintln!("s remove: cwd: {}", e);
399 return 1;
400 }
401 };
402 let root = match find_project_root(&cwd) {
403 Some(r) => r,
404 None => {
405 eprintln!("s remove: no stryke.toml found in this directory or any parent");
406 return 1;
407 }
408 };
409 let manifest_path = root.join(MANIFEST_FILE);
410 let mut manifest = match Manifest::from_path(&manifest_path) {
411 Ok(m) => m,
412 Err(e) => {
413 eprintln!("s remove: {}", e);
414 return 1;
415 }
416 };
417 let mut removed = false;
418 if manifest.deps.shift_remove(name).is_some() {
419 removed = true;
420 }
421 if manifest.dev_deps.shift_remove(name).is_some() {
422 removed = true;
423 }
424 for (_g, group_map) in manifest.groups.iter_mut() {
425 if group_map.shift_remove(name).is_some() {
426 removed = true;
427 }
428 }
429 if !removed {
430 eprintln!("s remove: `{}` is not a direct dep", name);
431 return 1;
432 }
433 let body = match manifest.to_toml_string() {
434 Ok(s) => s,
435 Err(e) => {
436 eprintln!("s remove: {}", e);
437 return 1;
438 }
439 };
440 if let Err(e) = std::fs::write(&manifest_path, body) {
441 eprintln!("s remove: write {}: {}", manifest_path.display(), e);
442 return 1;
443 }
444 eprintln!(" removed {}", name);
445 cmd_install(&[])
446}
447
448pub fn cmd_install(args: &[String]) -> i32 {
452 if args.iter().any(|a| is_help_flag(a)) {
453 println!("usage: stryke install [--offline]");
454 println!();
455 println!("Resolve manifest deps, install path/workspace deps into ~/.stryke/store/,");
456 println!("and write stryke.lock with deterministic ordering + SHA-256 integrity hashes.");
457 println!();
458 println!("Flags:");
459 println!(" --offline only use cached packages; never fetch from the network");
460 return 0;
461 }
462 let _offline = args.iter().any(|a| a == "--offline");
463
464 let cwd = match std::env::current_dir() {
465 Ok(c) => c,
466 Err(e) => {
467 eprintln!("s install: cwd: {}", e);
468 return 1;
469 }
470 };
471 let root = match find_project_root(&cwd) {
472 Some(r) => r,
473 None => {
474 eprintln!("s install: no stryke.toml found in this directory or any parent");
475 return 1;
476 }
477 };
478
479 let manifest_path = root.join(MANIFEST_FILE);
480 let manifest = match Manifest::from_path(&manifest_path) {
481 Ok(m) => m,
482 Err(e) => {
483 eprintln!("s install: {}", e);
484 return 1;
485 }
486 };
487 if let Err(e) = manifest.validate() {
488 eprintln!("s install: {}", e);
489 return 1;
490 }
491
492 let store = match Store::user_default() {
493 Ok(s) => s,
494 Err(e) => {
495 eprintln!("s install: {}", e);
496 return 1;
497 }
498 };
499
500 let r = Resolver {
501 manifest: &manifest,
502 manifest_dir: &root,
503 store: &store,
504 };
505 let outcome = match r.resolve() {
506 Ok(o) => o,
507 Err(e) => {
508 eprintln!("s install: {}", e);
509 return 1;
510 }
511 };
512
513 if outcome.installed.is_empty() {
514 eprintln!(" no deps to install");
515 } else {
516 for (name, version, _path) in &outcome.installed {
517 eprintln!(" installed {}@{}", name, version);
518 }
519 }
520
521 let mut lf = outcome.lockfile;
522 let body = match lf.to_toml_string() {
523 Ok(s) => s,
524 Err(e) => {
525 eprintln!("s install: {}", e);
526 return 1;
527 }
528 };
529 let lock_path = root.join(LOCKFILE_FILE);
530 if let Err(e) = std::fs::write(&lock_path, body) {
531 eprintln!("s install: write {}: {}", lock_path.display(), e);
532 return 1;
533 }
534 eprintln!(
535 "\x1b[32m✓ wrote {} ({} package{})\x1b[0m",
536 lock_path.display(),
537 lf.packages.len(),
538 if lf.packages.len() == 1 { "" } else { "s" }
539 );
540 0
541}
542
543pub fn cmd_tree(args: &[String]) -> i32 {
547 if args.iter().any(|a| is_help_flag(a)) {
548 println!("usage: stryke tree");
549 println!();
550 println!("Print the resolved dependency graph from stryke.lock as a tree, with the");
551 println!("project at the root and direct + transitive deps underneath.");
552 println!();
553 println!("Run `s install` first to generate stryke.lock.");
554 return 0;
555 }
556 let cwd = match std::env::current_dir() {
557 Ok(c) => c,
558 Err(e) => {
559 eprintln!("s tree: cwd: {}", e);
560 return 1;
561 }
562 };
563 let root = match find_project_root(&cwd) {
564 Some(r) => r,
565 None => {
566 eprintln!("s tree: no stryke.toml found");
567 return 1;
568 }
569 };
570 let manifest = match Manifest::from_path(&root.join(MANIFEST_FILE)) {
571 Ok(m) => m,
572 Err(e) => {
573 eprintln!("s tree: {}", e);
574 return 1;
575 }
576 };
577 let lock_path = root.join(LOCKFILE_FILE);
578 if !lock_path.is_file() {
579 eprintln!("s tree: stryke.lock not found — run `s install` first");
580 return 1;
581 }
582 let lock = match Lockfile::from_path(&lock_path) {
583 Ok(l) => l,
584 Err(e) => {
585 eprintln!("s tree: {}", e);
586 return 1;
587 }
588 };
589
590 let pkg_label = manifest
591 .package
592 .as_ref()
593 .map(|p| format!("{} v{}", p.name, p.version))
594 .unwrap_or_else(|| "(workspace)".to_string());
595 println!("{}", pkg_label);
596
597 let direct_names: Vec<String> = manifest
598 .deps
599 .keys()
600 .chain(manifest.dev_deps.keys())
601 .chain(manifest.groups.values().flat_map(|g| g.keys()))
602 .cloned()
603 .collect();
604
605 for (i, dep_name) in direct_names.iter().enumerate() {
606 let last = i + 1 == direct_names.len();
607 print_tree_entry(&lock, dep_name, "", last);
608 }
609 0
610}
611
612fn print_tree_entry(lock: &Lockfile, name: &str, prefix: &str, last: bool) {
613 let connector = if last { "└── " } else { "├── " };
614 let next_prefix = if last { " " } else { "│ " };
615 match lock.find(name) {
616 Some(entry) => {
617 println!("{}{}{} v{}", prefix, connector, entry.name, entry.version);
618 for (i, dep_pin) in entry.deps.iter().enumerate() {
619 let dep_name = dep_pin.split_once('@').map(|(n, _)| n).unwrap_or(dep_pin);
620 let last_child = i + 1 == entry.deps.len();
621 print_tree_entry(
622 lock,
623 dep_name,
624 &format!("{}{}", prefix, next_prefix),
625 last_child,
626 );
627 }
628 }
629 None => {
630 println!("{}{}{} (not in lockfile)", prefix, connector, name);
631 }
632 }
633}
634
635pub fn cmd_info(args: &[String]) -> i32 {
639 if args.iter().any(|a| is_help_flag(a)) {
640 println!("usage: stryke info NAME");
641 println!();
642 println!("Print the lockfile entry and store path for an installed dep. Shows name,");
643 println!("version, source URL, integrity hash, enabled features, and transitive deps.");
644 println!();
645 println!("Run `s install` first to generate stryke.lock.");
646 return 0;
647 }
648 if args.len() != 1 {
649 eprintln!("usage: s info NAME");
650 return 1;
651 }
652 let name = &args[0];
653 let cwd = match std::env::current_dir() {
654 Ok(c) => c,
655 Err(e) => {
656 eprintln!("s info: cwd: {}", e);
657 return 1;
658 }
659 };
660 let root = match find_project_root(&cwd) {
661 Some(r) => r,
662 None => {
663 eprintln!("s info: no stryke.toml found");
664 return 1;
665 }
666 };
667 let lock_path = root.join(LOCKFILE_FILE);
668 if !lock_path.is_file() {
669 eprintln!("s info: stryke.lock not found — run `s install` first");
670 return 1;
671 }
672 let lock = match Lockfile::from_path(&lock_path) {
673 Ok(l) => l,
674 Err(e) => {
675 eprintln!("s info: {}", e);
676 return 1;
677 }
678 };
679 let entry = match lock.find(name) {
680 Some(e) => e,
681 None => {
682 eprintln!("s info: `{}` is not in stryke.lock", name);
683 return 1;
684 }
685 };
686 let store = match Store::user_default() {
687 Ok(s) => s,
688 Err(e) => {
689 eprintln!("s info: {}", e);
690 return 1;
691 }
692 };
693 let pkg_dir = store.package_dir(&entry.name, &entry.version);
694 println!("name: {}", entry.name);
695 println!("version: {}", entry.version);
696 println!("source: {}", entry.source);
697 println!("integrity: {}", entry.integrity);
698 if !entry.features.is_empty() {
699 println!("features: {}", entry.features.join(", "));
700 }
701 if !entry.deps.is_empty() {
702 println!("deps: {}", entry.deps.join(", "));
703 }
704 println!("store path: {}", pkg_dir.display());
705 let nested_manifest = pkg_dir.join(MANIFEST_FILE);
706 if nested_manifest.is_file() {
707 if let Ok(m) = Manifest::from_path(&nested_manifest) {
708 if let Some(meta) = &m.package {
709 if !meta.description.is_empty() {
710 println!("description: {}", meta.description);
711 }
712 if !meta.license.is_empty() {
713 println!("license: {}", meta.license);
714 }
715 if !meta.repository.is_empty() {
716 println!("repo: {}", meta.repository);
717 }
718 }
719 }
720 }
721 0
722}
723
724pub fn load_project(root: &Path) -> PkgResult<(Manifest, Option<Lockfile>)> {
727 let manifest = Manifest::from_path(&root.join(MANIFEST_FILE))?;
728 let lock_path = root.join(LOCKFILE_FILE);
729 let lockfile = if lock_path.is_file() {
730 Some(Lockfile::from_path(&lock_path)?)
731 } else {
732 None
733 };
734 Ok((manifest, lockfile))
735}
736
737pub fn resolve_module(root: &Path, logical_name: &str) -> PkgResult<Option<PathBuf>> {
745 let segments: Vec<&str> = logical_name.split("::").collect();
746 if segments.is_empty() {
747 return Ok(None);
748 }
749
750 let local = root.join("lib").join(segments_to_path(&segments));
752 if local.is_file() {
753 return Ok(Some(local));
754 }
755
756 let lock_path = root.join(LOCKFILE_FILE);
759 if lock_path.is_file() {
760 let lock = Lockfile::from_path(&lock_path)?;
761 let pkg_name = segments[0].to_lowercase();
762 if let Some(entry) = lock.find(&pkg_name) {
763 let store = Store::user_default()?;
764 let store_pkg = store.package_dir(&entry.name, &entry.version);
765 let nested_path = if segments.len() == 1 {
766 store_pkg.join("lib").join(format!("{}.stk", segments[0]))
767 } else {
768 store_pkg.join("lib").join(segments_to_path(&segments[1..]))
769 };
770 if nested_path.is_file() {
771 return Ok(Some(nested_path));
772 }
773 }
774 }
775 Ok(None)
776}
777
778fn segments_to_path(segments: &[&str]) -> PathBuf {
779 let mut p = PathBuf::new();
780 for (i, seg) in segments.iter().enumerate() {
781 if i + 1 == segments.len() {
782 p.push(format!("{}.stk", seg));
783 } else {
784 p.push(seg);
785 }
786 }
787 p
788}
789
790pub fn cmd_clean(args: &[String]) -> i32 {
796 if args.iter().any(|a| is_help_flag(a)) {
797 println!("usage: stryke clean [--all]");
798 println!();
799 println!("Remove the local target/ directory and per-project bytecode cache.");
800 println!();
801 println!("Flags:");
802 println!(" --all additionally clear ~/.stryke/cache/ and ~/.stryke/store/");
803 return 0;
804 }
805 let want_global = args.iter().any(|a| a == "--all");
806
807 let cwd = match std::env::current_dir() {
808 Ok(c) => c,
809 Err(e) => {
810 eprintln!("s clean: cwd: {}", e);
811 return 1;
812 }
813 };
814 let root = find_project_root(&cwd).unwrap_or(cwd);
815 let mut wiped: Vec<String> = Vec::new();
816 for sub in ["target", ".stryke-cache"] {
817 let d = root.join(sub);
818 if d.exists() {
819 if let Err(e) = std::fs::remove_dir_all(&d) {
820 eprintln!("s clean: remove {}: {}", d.display(), e);
821 return 1;
822 }
823 wiped.push(d.display().to_string());
824 }
825 }
826
827 if want_global {
828 if let Ok(store) = Store::user_default() {
829 for d in [store.cache_dir(), store.store_dir(), store.git_dir()] {
830 if d.exists() {
831 if let Err(e) = std::fs::remove_dir_all(&d) {
832 eprintln!("s clean: remove {}: {}", d.display(), e);
833 return 1;
834 }
835 wiped.push(d.display().to_string());
836 }
837 }
838 }
839 }
840
841 if wiped.is_empty() {
842 eprintln!(" nothing to clean");
843 } else {
844 for w in &wiped {
845 eprintln!(" removed {}", w);
846 }
847 }
848 0
849}
850
851pub fn cmd_update(args: &[String]) -> i32 {
856 if args.iter().any(|a| is_help_flag(a)) {
857 println!("usage: stryke update [NAME]");
858 println!();
859 println!("Re-resolve the dependency graph and rewrite stryke.lock. With registry");
860 println!("deps unwired, this currently re-pins path/workspace dep integrity hashes.");
861 println!();
862 println!("NAME: when given, only that dep is re-resolved (others stay pinned).");
863 return 0;
864 }
865 let cwd = match std::env::current_dir() {
866 Ok(c) => c,
867 Err(e) => {
868 eprintln!("s update: cwd: {}", e);
869 return 1;
870 }
871 };
872 let root = match find_project_root(&cwd) {
873 Some(r) => r,
874 None => {
875 eprintln!("s update: no stryke.toml found");
876 return 1;
877 }
878 };
879 let lock_path = root.join(LOCKFILE_FILE);
880 if lock_path.exists() {
881 if let Err(e) = std::fs::remove_file(&lock_path) {
882 eprintln!("s update: remove {}: {}", lock_path.display(), e);
883 return 1;
884 }
885 }
886 eprintln!(" re-resolving dependency graph");
887 cmd_install(&[])
888}
889
890pub fn cmd_outdated(args: &[String]) -> i32 {
895 if args.iter().any(|a| is_help_flag(a)) {
896 println!("usage: stryke outdated");
897 println!();
898 println!("Show deps whose stryke.lock pin no longer matches the upstream state.");
899 println!("Path deps: integrity hash recomputed against the source directory.");
900 println!("Registry deps: not wired in this stryke version.");
901 return 0;
902 }
903 let cwd = match std::env::current_dir() {
904 Ok(c) => c,
905 Err(e) => {
906 eprintln!("s outdated: cwd: {}", e);
907 return 1;
908 }
909 };
910 let root = match find_project_root(&cwd) {
911 Some(r) => r,
912 None => {
913 eprintln!("s outdated: no stryke.toml found");
914 return 1;
915 }
916 };
917 let manifest = match Manifest::from_path(&root.join(MANIFEST_FILE)) {
918 Ok(m) => m,
919 Err(e) => {
920 eprintln!("s outdated: {}", e);
921 return 1;
922 }
923 };
924 let lock_path = root.join(LOCKFILE_FILE);
925 if !lock_path.is_file() {
926 eprintln!("s outdated: stryke.lock not found — run `s install` first");
927 return 1;
928 }
929 let lock = match Lockfile::from_path(&lock_path) {
930 Ok(l) => l,
931 Err(e) => {
932 eprintln!("s outdated: {}", e);
933 return 1;
934 }
935 };
936
937 let mut drifted: Vec<String> = Vec::new();
938 let mut registry_skipped: Vec<String> = Vec::new();
939 for (name, spec) in manifest.deps.iter() {
940 if let Some(p) = spec.path() {
941 let abs = if std::path::Path::new(p).is_absolute() {
942 std::path::PathBuf::from(p)
943 } else {
944 root.join(p)
945 };
946 if let Ok(now) = super::lockfile::integrity_for_directory(&abs) {
947 if let Some(entry) = lock.find(name) {
948 if entry.integrity != now {
949 drifted.push(format!(
950 " {}@{} pinned {} current {}",
951 name, entry.version, entry.integrity, now
952 ));
953 }
954 } else {
955 drifted.push(format!(
956 " {} (path) not in lockfile — run `s install`",
957 name
958 ));
959 }
960 }
961 } else {
962 registry_skipped.push(name.clone());
963 }
964 }
965
966 if drifted.is_empty() && registry_skipped.is_empty() {
967 eprintln!("\x1b[32m✓ all path deps are up to date\x1b[0m");
968 return 0;
969 }
970 if !drifted.is_empty() {
971 eprintln!("path deps with drift (run `s install` to re-pin):");
972 for d in &drifted {
973 eprintln!("{}", d);
974 }
975 }
976 if !registry_skipped.is_empty() {
977 eprintln!(
978 "registry deps skipped — wire protocol not deployed yet ({}): {}",
979 registry_skipped.len(),
980 registry_skipped.join(", ")
981 );
982 }
983 0
984}
985
986pub fn cmd_audit(args: &[String]) -> i32 {
991 if args.iter().any(|a| is_help_flag(a)) {
992 println!("usage: stryke audit [--fail-on=high|critical]");
993 println!();
994 println!("Check stryke.lock against a vulnerability advisory feed. The feed itself");
995 println!("is not deployed yet — this command currently reports the dep count and");
996 println!("emits an honest 'no advisories' message rather than faking it.");
997 return 0;
998 }
999 let cwd = match std::env::current_dir() {
1000 Ok(c) => c,
1001 Err(e) => {
1002 eprintln!("s audit: cwd: {}", e);
1003 return 1;
1004 }
1005 };
1006 let root = match find_project_root(&cwd) {
1007 Some(r) => r,
1008 None => {
1009 eprintln!("s audit: no stryke.toml found");
1010 return 1;
1011 }
1012 };
1013 let lock_path = root.join(LOCKFILE_FILE);
1014 if !lock_path.is_file() {
1015 eprintln!("s audit: stryke.lock not found — run `s install` first");
1016 return 1;
1017 }
1018 let lock = match Lockfile::from_path(&lock_path) {
1019 Ok(l) => l,
1020 Err(e) => {
1021 eprintln!("s audit: {}", e);
1022 return 1;
1023 }
1024 };
1025 eprintln!(
1026 " audited {} package{}",
1027 lock.packages.len(),
1028 if lock.packages.len() == 1 { "" } else { "s" }
1029 );
1030 eprintln!("\x1b[33m advisory feed not yet deployed — no vulnerabilities reported\x1b[0m");
1031 0
1032}
1033
1034pub fn cmd_run_script(args: &[String]) -> i32 {
1042 if args.iter().any(|a| is_help_flag(a)) {
1043 println!("usage: stryke run SCRIPT [ARGS...]");
1044 println!();
1045 println!("Look up SCRIPT in the [scripts] table of stryke.toml and execute it via");
1046 println!("the system shell. Any ARGS are appended to the script command line.");
1047 println!();
1048 println!("Without [scripts], `stryke run` falls back to running ./main.stk directly.");
1049 return 0;
1050 }
1051 if args.is_empty() {
1052 eprintln!("usage: s run SCRIPT [ARGS...]");
1053 return 1;
1054 }
1055 let script = &args[0];
1056 let cwd = match std::env::current_dir() {
1057 Ok(c) => c,
1058 Err(e) => {
1059 eprintln!("s run: cwd: {}", e);
1060 return 1;
1061 }
1062 };
1063 let root = match find_project_root(&cwd) {
1064 Some(r) => r,
1065 None => {
1066 eprintln!("s run: no stryke.toml found in this directory or any parent");
1067 return 1;
1068 }
1069 };
1070 let manifest = match Manifest::from_path(&root.join(MANIFEST_FILE)) {
1071 Ok(m) => m,
1072 Err(e) => {
1073 eprintln!("s run: {}", e);
1074 return 1;
1075 }
1076 };
1077 let cmd = match manifest.scripts.get(script) {
1078 Some(c) => c.clone(),
1079 None => {
1080 eprintln!("s run: no script `{}` in [scripts]", script);
1081 if !manifest.scripts.is_empty() {
1082 eprintln!(
1083 "available: {}",
1084 manifest
1085 .scripts
1086 .keys()
1087 .cloned()
1088 .collect::<Vec<_>>()
1089 .join(", ")
1090 );
1091 }
1092 return 1;
1093 }
1094 };
1095 let extra = &args[1..];
1096 let full = if extra.is_empty() {
1097 cmd.clone()
1098 } else {
1099 format!(
1100 "{} {}",
1101 cmd,
1102 extra
1103 .iter()
1104 .map(|a| shell_escape_simple(a))
1105 .collect::<Vec<_>>()
1106 .join(" ")
1107 )
1108 };
1109 eprintln!(" $ {}", full);
1110 let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
1111 let status = std::process::Command::new(&shell)
1112 .arg("-c")
1113 .arg(&full)
1114 .current_dir(&root)
1115 .status();
1116 match status {
1117 Ok(s) => s.code().unwrap_or(1),
1118 Err(e) => {
1119 eprintln!("s run: spawn {}: {}", shell, e);
1120 1
1121 }
1122 }
1123}
1124
1125fn shell_escape_simple(s: &str) -> String {
1127 if !s.contains(' ')
1128 && !s.contains('\'')
1129 && !s.contains('"')
1130 && !s.contains('$')
1131 && !s.contains('`')
1132 {
1133 return s.to_string();
1134 }
1135 let escaped = s.replace('\'', "'\\''");
1136 format!("'{}'", escaped)
1137}
1138
1139pub fn cmd_vendor(args: &[String]) -> i32 {
1144 if args.iter().any(|a| is_help_flag(a)) {
1145 println!("usage: stryke vendor");
1146 println!();
1147 println!("Copy every dep in stryke.lock from ~/.stryke/store/ into ./vendor/ so");
1148 println!("the project is offline-buildable. Useful for shipping a tarball that");
1149 println!("builds without registry access.");
1150 return 0;
1151 }
1152 let cwd = match std::env::current_dir() {
1153 Ok(c) => c,
1154 Err(e) => {
1155 eprintln!("s vendor: cwd: {}", e);
1156 return 1;
1157 }
1158 };
1159 let root = match find_project_root(&cwd) {
1160 Some(r) => r,
1161 None => {
1162 eprintln!("s vendor: no stryke.toml found");
1163 return 1;
1164 }
1165 };
1166 let lock_path = root.join(LOCKFILE_FILE);
1167 if !lock_path.is_file() {
1168 eprintln!("s vendor: stryke.lock not found — run `s install` first");
1169 return 1;
1170 }
1171 let lock = match Lockfile::from_path(&lock_path) {
1172 Ok(l) => l,
1173 Err(e) => {
1174 eprintln!("s vendor: {}", e);
1175 return 1;
1176 }
1177 };
1178 let store = match Store::user_default() {
1179 Ok(s) => s,
1180 Err(e) => {
1181 eprintln!("s vendor: {}", e);
1182 return 1;
1183 }
1184 };
1185
1186 let vendor_dir = root.join("vendor");
1187 if vendor_dir.exists() {
1188 if let Err(e) = std::fs::remove_dir_all(&vendor_dir) {
1189 eprintln!("s vendor: clear {}: {}", vendor_dir.display(), e);
1190 return 1;
1191 }
1192 }
1193 if let Err(e) = std::fs::create_dir_all(&vendor_dir) {
1194 eprintln!("s vendor: mkdir {}: {}", vendor_dir.display(), e);
1195 return 1;
1196 }
1197
1198 let mut copied = 0_usize;
1199 for pkg in &lock.packages {
1200 let src = store.package_dir(&pkg.name, &pkg.version);
1201 if !src.is_dir() {
1202 eprintln!(
1203 "s vendor: {}@{} not in store — run `s install` first",
1204 pkg.name, pkg.version
1205 );
1206 return 1;
1207 }
1208 let dst = vendor_dir.join(format!("{}@{}", pkg.name, pkg.version));
1209 if let Err(e) = copy_tree(&src, &dst) {
1210 eprintln!("s vendor: copy {}: {}", src.display(), e);
1211 return 1;
1212 }
1213 copied += 1;
1214 }
1215 eprintln!(
1216 "\x1b[32m✓ vendored {} package{} into {}\x1b[0m",
1217 copied,
1218 if copied == 1 { "" } else { "s" },
1219 vendor_dir.display()
1220 );
1221 0
1222}
1223
1224fn copy_tree(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
1228 std::fs::create_dir_all(dst)?;
1229 for entry in std::fs::read_dir(src)? {
1230 let entry = entry?;
1231 let from = entry.path();
1232 let to = dst.join(entry.file_name());
1233 let meta = entry.metadata()?;
1234 if meta.is_dir() {
1235 copy_tree(&from, &to)?;
1236 } else if meta.file_type().is_symlink() {
1237 #[cfg(unix)]
1238 {
1239 let target = std::fs::read_link(&from)?;
1240 std::os::unix::fs::symlink(target, &to)?;
1241 }
1242 #[cfg(not(unix))]
1243 std::fs::copy(&from, &to)?;
1244 } else {
1245 std::fs::copy(&from, &to)?;
1246 }
1247 }
1248 Ok(())
1249}
1250
1251pub fn cmd_install_global(args: &[String]) -> i32 {
1255 if args.iter().any(|a| is_help_flag(a)) || args.is_empty() {
1256 println!("usage: stryke install -g PATH");
1257 println!();
1258 println!("Install a local package's [bin] entries into ~/.stryke/bin/ as launcher");
1259 println!("scripts that invoke `stryke <main_stk>`. PATH is the path to a directory");
1260 println!("containing a stryke.toml with a [bin] table.");
1261 return if args.is_empty() { 1 } else { 0 };
1262 }
1263 let pkg_path = std::path::PathBuf::from(&args[0]);
1264 if !pkg_path.is_dir() {
1265 eprintln!("s install -g: {} is not a directory", pkg_path.display());
1266 return 1;
1267 }
1268 let manifest = match Manifest::from_path(&pkg_path.join(MANIFEST_FILE)) {
1269 Ok(m) => m,
1270 Err(e) => {
1271 eprintln!("s install -g: {}", e);
1272 return 1;
1273 }
1274 };
1275 if manifest.bin.is_empty() {
1276 eprintln!("s install -g: package has no [bin] entries");
1277 return 1;
1278 }
1279 let store = match Store::user_default() {
1280 Ok(s) => s,
1281 Err(e) => {
1282 eprintln!("s install -g: {}", e);
1283 return 1;
1284 }
1285 };
1286 if let Err(e) = store.ensure_layout() {
1287 eprintln!("s install -g: {}", e);
1288 return 1;
1289 }
1290
1291 let abs_pkg = match pkg_path.canonicalize() {
1292 Ok(p) => p,
1293 Err(e) => {
1294 eprintln!("s install -g: canonicalize {}: {}", pkg_path.display(), e);
1295 return 1;
1296 }
1297 };
1298
1299 for (bin_name, entry) in &manifest.bin {
1300 let target = abs_pkg.join(entry);
1301 if !target.is_file() {
1302 eprintln!(
1303 "s install -g: bin `{}` -> {} does not exist",
1304 bin_name,
1305 target.display()
1306 );
1307 return 1;
1308 }
1309 let launcher = store.bin_dir().join(bin_name);
1310 if let Err(e) = write_launcher(&launcher, &target) {
1311 eprintln!("s install -g: write {}: {}", launcher.display(), e);
1312 return 1;
1313 }
1314 eprintln!(" installed {} -> {}", launcher.display(), target.display());
1315 }
1316 eprintln!(
1317 "\x1b[32m✓ installed {} bin{} (add {} to PATH)\x1b[0m",
1318 manifest.bin.len(),
1319 if manifest.bin.len() == 1 { "" } else { "s" },
1320 store.bin_dir().display()
1321 );
1322 0
1323}
1324
1325fn write_launcher(
1330 launcher_path: &std::path::Path,
1331 target: &std::path::Path,
1332) -> std::io::Result<()> {
1333 if launcher_path.exists() {
1334 std::fs::remove_file(launcher_path)?;
1335 }
1336 let body = format!(
1337 "#!/bin/sh\nexec stryke {:?} \"$@\"\n",
1338 target.display().to_string()
1339 );
1340 std::fs::write(launcher_path, body)?;
1341 #[cfg(unix)]
1342 {
1343 use std::os::unix::fs::PermissionsExt;
1344 let mut perms = std::fs::metadata(launcher_path)?.permissions();
1345 perms.set_mode(0o755);
1346 std::fs::set_permissions(launcher_path, perms)?;
1347 }
1348 Ok(())
1349}
1350
1351pub fn cmd_uninstall_global(args: &[String]) -> i32 {
1353 if args.iter().any(|a| is_help_flag(a)) || args.is_empty() {
1354 println!("usage: stryke uninstall -g NAME");
1355 println!();
1356 println!("Remove the launcher script ~/.stryke/bin/NAME installed by `stryke install -g`.");
1357 return if args.is_empty() { 1 } else { 0 };
1358 }
1359 let store = match Store::user_default() {
1360 Ok(s) => s,
1361 Err(e) => {
1362 eprintln!("s uninstall -g: {}", e);
1363 return 1;
1364 }
1365 };
1366 let target = store.bin_dir().join(&args[0]);
1367 if !target.exists() {
1368 eprintln!("s uninstall -g: {} not installed", args[0]);
1369 return 1;
1370 }
1371 if let Err(e) = std::fs::remove_file(&target) {
1372 eprintln!("s uninstall -g: remove {}: {}", target.display(), e);
1373 return 1;
1374 }
1375 eprintln!(" removed {}", target.display());
1376 0
1377}
1378
1379pub fn cmd_list_global(args: &[String]) -> i32 {
1381 if args.iter().any(|a| is_help_flag(a)) {
1382 println!("usage: stryke list -g");
1383 println!();
1384 println!("List launchers installed via `stryke install -g` in ~/.stryke/bin/.");
1385 return 0;
1386 }
1387 let store = match Store::user_default() {
1388 Ok(s) => s,
1389 Err(e) => {
1390 eprintln!("s list -g: {}", e);
1391 return 1;
1392 }
1393 };
1394 let bin_dir = store.bin_dir();
1395 if !bin_dir.is_dir() {
1396 eprintln!(" no global tools installed");
1397 return 0;
1398 }
1399 let mut names: Vec<String> = Vec::new();
1400 let entries = match std::fs::read_dir(&bin_dir) {
1401 Ok(e) => e,
1402 Err(e) => {
1403 eprintln!("s list -g: read {}: {}", bin_dir.display(), e);
1404 return 1;
1405 }
1406 };
1407 for entry in entries.flatten() {
1408 if let Some(n) = entry.file_name().to_str() {
1409 names.push(n.to_string());
1410 }
1411 }
1412 names.sort();
1413 if names.is_empty() {
1414 eprintln!(" no global tools installed");
1415 } else {
1416 for n in &names {
1417 println!("{}", n);
1418 }
1419 }
1420 0
1421}
1422
1423pub fn cmd_search(args: &[String]) -> i32 {
1427 if args.iter().any(|a| is_help_flag(a)) {
1428 println!("usage: stryke search NAME");
1429 println!();
1430 println!("Query the stryke registry for packages matching NAME. The registry");
1431 println!("endpoint is not deployed yet — this command emits a clear diagnostic");
1432 println!("rather than silent failure.");
1433 return 0;
1434 }
1435 if args.is_empty() {
1436 eprintln!("usage: s search NAME");
1437 return 1;
1438 }
1439 eprintln!(
1440 "s search: registry endpoint not deployed yet (RFC §\"Registry Protocol\"). \
1441 Query was `{}`.",
1442 args[0]
1443 );
1444 1
1445}
1446
1447pub fn cmd_publish(args: &[String]) -> i32 {
1451 if args.iter().any(|a| is_help_flag(a)) {
1452 println!("usage: stryke publish [--registry=URL] [--dry-run]");
1453 println!();
1454 println!("Package the project as a tarball and push to the stryke registry. The");
1455 println!("registry endpoint is not deployed yet — this command currently performs");
1456 println!("the local pack step (under --dry-run) and stops.");
1457 return 0;
1458 }
1459 let dry_run = args.iter().any(|a| a == "--dry-run");
1460 if !dry_run {
1461 eprintln!(
1462 "s publish: registry endpoint not deployed yet (RFC §\"Registry Protocol\"). \
1463 Pass --dry-run to exercise the local pack step."
1464 );
1465 return 1;
1466 }
1467 let cwd = match std::env::current_dir() {
1469 Ok(c) => c,
1470 Err(e) => {
1471 eprintln!("s publish: cwd: {}", e);
1472 return 1;
1473 }
1474 };
1475 let root = match find_project_root(&cwd) {
1476 Some(r) => r,
1477 None => {
1478 eprintln!("s publish: no stryke.toml found");
1479 return 1;
1480 }
1481 };
1482 let manifest = match Manifest::from_path(&root.join(MANIFEST_FILE)) {
1483 Ok(m) => m,
1484 Err(e) => {
1485 eprintln!("s publish: {}", e);
1486 return 1;
1487 }
1488 };
1489 if let Err(e) = manifest.validate() {
1490 eprintln!("s publish: {}", e);
1491 return 1;
1492 }
1493 let pkg = match manifest.package.as_ref() {
1494 Some(p) => p,
1495 None => {
1496 eprintln!("s publish: workspace roots can't be published — pick a member");
1497 return 1;
1498 }
1499 };
1500 let integrity = match super::lockfile::integrity_for_directory(&root) {
1501 Ok(s) => s,
1502 Err(e) => {
1503 eprintln!("s publish: hash {}: {}", root.display(), e);
1504 return 1;
1505 }
1506 };
1507 eprintln!(" would publish {} v{}", pkg.name, pkg.version);
1508 eprintln!(" source dir: {}", root.display());
1509 eprintln!(" integrity: {}", integrity);
1510 eprintln!(" (dry run — no upload performed)");
1511 0
1512}
1513
1514pub fn cmd_yank(args: &[String]) -> i32 {
1517 if args.iter().any(|a| is_help_flag(a)) {
1518 println!("usage: stryke yank VERSION");
1519 println!();
1520 println!("Mark a published version as do-not-resolve. Registry endpoint not");
1521 println!("deployed yet — this command emits a clear diagnostic rather than");
1522 println!("silent failure. Yanked versions are never deleted (immutable registry).");
1523 return 0;
1524 }
1525 if args.is_empty() {
1526 eprintln!("usage: s yank VERSION");
1527 return 1;
1528 }
1529 eprintln!(
1530 "s yank: registry endpoint not deployed yet (RFC §\"Registry Protocol\"). \
1531 Version was `{}`.",
1532 args[0]
1533 );
1534 1
1535}
1536
1537pub fn dispatch(args: &[String]) -> i32 {
1541 let want_help = args.first().map(|a| is_help_flag(a)).unwrap_or(false);
1542 if args.is_empty() || want_help {
1543 println!("usage: stryke pkg <subcommand> [args]");
1544 println!();
1545 println!("Package-manager subcommand dispatcher. The same handlers are also");
1546 println!("reachable as top-level commands (e.g. `stryke install` ≡ `stryke pkg install`).");
1547 println!();
1548 println!("Subcommands:");
1549 println!(" init [NAME] scaffold project in cwd");
1550 println!(" new NAME scaffold project at ./NAME/");
1551 println!(" install [--offline] resolve deps + write stryke.lock");
1552 println!(" install -g PATH install a local package's [bin] entries globally");
1553 println!(" uninstall -g NAME remove a global launcher");
1554 println!(" list -g list global launchers");
1555 println!(" add NAME[@VER] [...] add a dep to stryke.toml");
1556 println!(" remove NAME drop a dep from stryke.toml");
1557 println!(" update [NAME] re-resolve and rewrite stryke.lock");
1558 println!(" outdated report deps drifted from their lock pin");
1559 println!(" audit check lockfile against advisory feed");
1560 println!(" tree print resolved dep graph");
1561 println!(" info NAME show lockfile entry for a dep");
1562 println!(" vendor snapshot store deps to ./vendor/");
1563 println!(" clean [--all] wipe target/ (and optionally global caches)");
1564 println!(" search NAME registry query (registry not deployed)");
1565 println!(" publish [--dry-run] publish to registry (registry not deployed)");
1566 println!(" yank VERSION yank a version (registry not deployed)");
1567 println!(" run SCRIPT [ARGS...] run a [scripts] entry");
1568 println!();
1569 println!("Run `stryke <subcommand> -h` for per-subcommand flags.");
1570 return if args.is_empty() { 1 } else { 0 };
1571 }
1572 match args[0].as_str() {
1573 "init" => cmd_init(args.get(1).map(|s| s.as_str())),
1574 "new" => match args.get(1) {
1575 Some(name) => cmd_new(name),
1576 None => {
1577 eprintln!("usage: s pkg new NAME");
1578 1
1579 }
1580 },
1581 "add" => cmd_add(&args[1..]),
1582 "remove" => cmd_remove(&args[1..]),
1583 "install" => {
1584 if args.iter().skip(1).any(|a| a == "-g" || a == "--global") {
1586 let filtered: Vec<String> = args[1..]
1587 .iter()
1588 .filter(|a| !matches!(a.as_str(), "-g" | "--global"))
1589 .cloned()
1590 .collect();
1591 cmd_install_global(&filtered)
1592 } else {
1593 cmd_install(&args[1..])
1594 }
1595 }
1596 "uninstall" => {
1597 if args.iter().skip(1).any(|a| a == "-g" || a == "--global") {
1598 let filtered: Vec<String> = args[1..]
1599 .iter()
1600 .filter(|a| !matches!(a.as_str(), "-g" | "--global"))
1601 .cloned()
1602 .collect();
1603 cmd_uninstall_global(&filtered)
1604 } else {
1605 eprintln!("s uninstall: pass -g for global tools (no per-project uninstall yet)");
1606 1
1607 }
1608 }
1609 "list" => {
1610 if args.iter().skip(1).any(|a| a == "-g" || a == "--global") {
1611 let filtered: Vec<String> = args[1..]
1612 .iter()
1613 .filter(|a| !matches!(a.as_str(), "-g" | "--global"))
1614 .cloned()
1615 .collect();
1616 cmd_list_global(&filtered)
1617 } else {
1618 eprintln!("s list: pass -g to list global tools");
1619 1
1620 }
1621 }
1622 "tree" => cmd_tree(&args[1..]),
1623 "info" => cmd_info(&args[1..]),
1624 "update" => cmd_update(&args[1..]),
1625 "outdated" => cmd_outdated(&args[1..]),
1626 "audit" => cmd_audit(&args[1..]),
1627 "vendor" => cmd_vendor(&args[1..]),
1628 "clean" => cmd_clean(&args[1..]),
1629 "search" => cmd_search(&args[1..]),
1630 "publish" => cmd_publish(&args[1..]),
1631 "yank" => cmd_yank(&args[1..]),
1632 "run" => cmd_run_script(&args[1..]),
1633 other => {
1634 eprintln!("s pkg: unknown subcommand `{}`", other);
1635 1
1636 }
1637 }
1638}
1639
1640#[cfg(test)]
1641mod tests {
1642 use super::*;
1643
1644 fn tempdir(tag: &str) -> PathBuf {
1645 let pid = std::process::id();
1646 let nanos = std::time::SystemTime::now()
1647 .duration_since(std::time::UNIX_EPOCH)
1648 .unwrap()
1649 .subsec_nanos();
1650 let p = std::env::temp_dir().join(format!("stryke-cmd-{}-{}-{}", tag, pid, nanos));
1651 let _ = std::fs::remove_dir_all(&p);
1652 std::fs::create_dir_all(&p).unwrap();
1653 p
1654 }
1655
1656 #[test]
1657 fn find_project_root_walks_up() {
1658 let root = tempdir("root");
1659 std::fs::write(
1660 root.join(MANIFEST_FILE),
1661 "[package]\nname=\"x\"\nversion=\"0.1.0\"\n",
1662 )
1663 .unwrap();
1664 let nested = root.join("a/b/c");
1665 std::fs::create_dir_all(&nested).unwrap();
1666 let canonical_root = root.canonicalize().unwrap();
1667 let canonical_nested = nested.canonicalize().unwrap();
1668 let found = find_project_root(&canonical_nested).unwrap();
1669 let canonical_found = found.canonicalize().unwrap();
1670 assert_eq!(canonical_found, canonical_root);
1671 }
1672
1673 #[test]
1674 fn resolve_module_local_lib_hit() {
1675 let root = tempdir("proj");
1676 std::fs::write(
1677 root.join(MANIFEST_FILE),
1678 "[package]\nname=\"x\"\nversion=\"0.1.0\"\n",
1679 )
1680 .unwrap();
1681 std::fs::create_dir_all(root.join("lib/Foo")).unwrap();
1682 std::fs::write(root.join("lib/Foo/Bar.stk"), "# bar").unwrap();
1683 let r = resolve_module(&root, "Foo::Bar").unwrap().unwrap();
1684 assert!(r.ends_with("lib/Foo/Bar.stk"), "got {:?}", r);
1685 }
1686
1687 #[test]
1688 fn resolve_module_falls_back_when_nothing_resolves() {
1689 let root = tempdir("proj");
1690 std::fs::write(
1691 root.join(MANIFEST_FILE),
1692 "[package]\nname=\"x\"\nversion=\"0.1.0\"\n",
1693 )
1694 .unwrap();
1695 let r = resolve_module(&root, "Foo::Bar").unwrap();
1696 assert!(r.is_none());
1697 }
1698}