use anyhow::{anyhow, Result};
use std::fs;
use std::path::{Path, PathBuf};
pub trait GreenCiStarter: Sync {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>>;
}
fn write_if_needed(path: &Path, body: &str, force: bool) -> Result<Option<PathBuf>> {
if path.is_file() && !force { return Ok(None); }
if path.is_file() && force && !file_is_substrate_stub(path) {
return Ok(None);
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| anyhow!("create_dir_all {}: {e}", parent.display()))?;
}
fs::write(path, body)
.map_err(|e| anyhow!("write {}: {e}", path.display()))?;
Ok(Some(path.to_path_buf()))
}
pub fn file_is_substrate_stub(path: &Path) -> bool {
let Ok(text) = std::fs::read_to_string(path) else { return true; };
text.contains("Replace this stub")
}
pub struct RustStarter;
impl GreenCiStarter for RustStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> {
let body = "//! pleme-io typed wrapper — auto-generated by caixa-forge.\n\
//!\n\
//! Replace this stub with the actual library implementation.\n\
//! The smoke test below ensures `cargo test` is green by default.\n\
\n\
#[cfg(test)]\n\
mod tests {\n\
\x20 #[test]\n\
\x20 fn smoke() {\n\
\x20 assert_eq!(2 + 2, 4);\n\
\x20 }\n\
}\n";
let mut out = vec![];
if let Some(p) = write_if_needed(&out_root.join("src/lib.rs"), body, force)? {
out.push(p);
}
Ok(out)
}
}
pub struct GoStarter;
impl GreenCiStarter for GoStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> {
let main = "// pleme-io typed wrapper — auto-generated by caixa-forge.\n\
//\n\
// Replace this stub with the actual program implementation.\n\
package main\n\
\n\
import \"fmt\"\n\
\n\
func main() {\n\
\tfmt.Println(\"pleme-io wrapper stub\")\n\
}\n";
let test = "package main\n\
\n\
import \"testing\"\n\
\n\
func TestSmoke(t *testing.T) {\n\
\tif 2+2 != 4 {\n\
\t\tt.Fatal(\"arithmetic broken\")\n\
\t}\n\
}\n";
let mut out = vec![];
if let Some(p) = write_if_needed(&out_root.join("main.go"), main, force)? {
out.push(p);
}
if let Some(p) = write_if_needed(&out_root.join("main_test.go"), test, force)? {
out.push(p);
}
Ok(out)
}
}
pub struct NpmStarter;
impl GreenCiStarter for NpmStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> {
let index = "// pleme-io typed wrapper — auto-generated by caixa-forge.\n\
// Replace this stub with the actual package implementation.\n\
\n\
module.exports = {\n\
\x20 smoke: () => 4,\n\
};\n";
let test = "const { test } = require('node:test');\n\
const assert = require('node:assert');\n\
const pkg = require('../src/index.js');\n\
\n\
test('smoke', () => {\n\
\x20 assert.strictEqual(pkg.smoke(), 4);\n\
});\n";
let mut out = vec![];
if let Some(p) = write_if_needed(&out_root.join("src/index.js"), index, force)? {
out.push(p);
}
if let Some(p) = write_if_needed(&out_root.join("test/test.js"), test, force)? {
out.push(p);
}
Ok(out)
}
}
pub struct PythonStarter;
impl GreenCiStarter for PythonStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = name.replace('-', "_");
let init_body = "\"\"\"pleme-io typed wrapper — auto-generated by caixa-forge.\n\
\n\
Replace this stub with the actual package implementation.\n\
\"\"\"\n\
\n\
def smoke() -> int:\n\
\x20 return 4\n";
let init_path = out_root.join("src").join(&mod_name).join("__init__.py");
let test_path = out_root.join("tests/test_smoke.py");
let mut out = vec![];
if let Some(p) = write_if_needed(&init_path, init_body, force)? {
out.push(p);
}
if !test_path.is_file() || force {
use crate::ast::Render;
use crate::python_ast::{Class, Expr as PExpr, File as PFile, Stmt as PStmt};
let mut f = PFile::new();
f.push(PStmt::Import { module: "unittest".into(), names: vec![] });
f.push(PStmt::Import { module: mod_name.clone(), names: vec!["smoke".into()] });
let test_method = PStmt::FunctionDef {
name: "test_smoke".into(),
params: vec!["self".into()],
body: vec![PStmt::Expr(PExpr::call("self.assertEqual", vec![
PExpr::pos(PExpr::call("smoke", vec![])),
PExpr::pos(PExpr::i(4)),
]))],
};
let mut cls = Class::new("SmokeTest", vec!["unittest.TestCase".into()]);
cls.push(test_method);
f.class(cls);
f.push_after_class(PStmt::If {
cond: r#"__name__ == "__main__""#.into(),
body: vec![PStmt::Expr(PExpr::call("unittest.main", vec![]))],
});
if let Some(parent) = test_path.parent() {
fs::create_dir_all(parent)?;
}
crate::ast::emit(&test_path, &f)?;
out.push(test_path);
}
Ok(out)
}
}
pub struct JavaGradleKtsStarter;
impl GreenCiStarter for JavaGradleKtsStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> {
let main = "// pleme-io typed wrapper.\npackage io.pleme.wrapper;\npublic class Smoke { public static int value() { return 4; } }\n";
let test = "package io.pleme.wrapper;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nclass SmokeTest { @Test void smoke() { assertEquals(4, Smoke.value()); } }\n";
let mut out = vec![];
if let Some(p) = write_if_needed(&out_root.join("src/main/java/io/pleme/wrapper/Smoke.java"), main, force)? { out.push(p); }
if let Some(p) = write_if_needed(&out_root.join("src/test/java/io/pleme/wrapper/SmokeTest.java"), test, force)? { out.push(p); }
Ok(out)
}
}
pub struct CppCmakeStarter;
impl GreenCiStarter for CppCmakeStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> { CppVcpkgStarter.write(out_root, _name, force) }
}
pub struct CppMesonStarter;
impl GreenCiStarter for CppMesonStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> { CppVcpkgStarter.write(out_root, _name, force) }
}
pub struct CppConanStarter;
impl GreenCiStarter for CppConanStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> { CppVcpkgStarter.write(out_root, _name, force) }
}
pub struct HaskellCabalStarter;
impl GreenCiStarter for HaskellCabalStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = pascal(&name.replace('-', "_"));
let lib = format!("-- pleme-io typed wrapper.\nmodule {mod_name} (smoke) where\nsmoke :: Int\nsmoke = 4\n");
let test = format!("import {mod_name}\nimport System.Exit\nmain :: IO ()\nmain = if smoke == 4 then exitSuccess else exitFailure\n");
let mut out = vec![];
if let Some(p) = write_if_needed(&out_root.join(format!("src/{mod_name}.hs")), &lib, force)? { out.push(p); }
if let Some(p) = write_if_needed(&out_root.join("test/Spec.hs"), &test, force)? { out.push(p); }
Ok(out)
}
}
pub struct NimNimbleStarter;
impl GreenCiStarter for NimNimbleStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = name.replace('-', "_");
let lib = format!("# pleme-io typed wrapper.\nproc smoke*(): int = 4\n");
let test = format!("import unittest\nimport {mod_name}\n\nsuite \"smoke\":\n test \"smoke is four\":\n check smoke() == 4\n");
let mut out = vec![];
if let Some(p) = write_if_needed(&out_root.join(format!("src/{mod_name}.nim")), &lib, force)? { out.push(p); }
if let Some(p) = write_if_needed(&out_root.join(format!("tests/test_{mod_name}.nim")), &test, force)? { out.push(p); }
Ok(out)
}
}
pub struct LuaRockspecStarter;
impl GreenCiStarter for LuaRockspecStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = name.replace('-', "_");
let lib = format!("-- pleme-io typed wrapper.\nlocal M = {{}}\nfunction M.smoke() return 4 end\nreturn M\n");
let test = format!("local m = require('{mod_name}')\nassert(m.smoke() == 4)\nprint('smoke ok')\n");
let mut out = vec![];
if let Some(p) = write_if_needed(&out_root.join(format!("src/{mod_name}.lua")), &lib, force)? { out.push(p); }
if let Some(p) = write_if_needed(&out_root.join("spec/smoke_spec.lua"), &test, force)? { out.push(p); }
Ok(out)
}
}
pub struct RDescriptionStarter;
impl GreenCiStarter for RDescriptionStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> {
let r = "# pleme-io typed wrapper.\nsmoke <- function() 4\n";
let test = "library(testthat)\nsource(\"../R/smoke.R\")\ntest_that(\"smoke\", { expect_equal(smoke(), 4) })\n";
let mut out = vec![];
if let Some(p) = write_if_needed(&out_root.join("R/smoke.R"), r, force)? { out.push(p); }
if let Some(p) = write_if_needed(&out_root.join("tests/testthat/test-smoke.R"), test, force)? { out.push(p); }
Ok(out)
}
}
pub struct PythonPipenvStarter;
impl GreenCiStarter for PythonPipenvStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> { PythonStarter.write(out_root, name, force) }
}
pub struct GithubActionStarter;
impl GreenCiStarter for GithubActionStarter {
fn write(&self, _out_root: &Path, _name: &str, _force: bool) -> Result<Vec<PathBuf>> {
Ok(vec![])
}
}
pub struct JavaMavenStarter;
impl GreenCiStarter for JavaMavenStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> {
let main = "// pleme-io typed wrapper — auto-generated by caixa-forge.\n\
package io.pleme.wrapper;\n\n\
public final class Smoke {\n\
\x20 public static int value() { return 4; }\n\
\x20 public static void main(String[] args) { System.out.println(\"pleme-io wrapper stub\"); }\n\
}\n";
let test = "package io.pleme.wrapper;\n\n\
import org.junit.jupiter.api.Test;\n\
import static org.junit.jupiter.api.Assertions.assertEquals;\n\n\
class SmokeTest {\n\
\x20 @Test void smoke() { assertEquals(4, Smoke.value()); }\n\
}\n";
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join("src/main/java/io/pleme/wrapper/Smoke.java"), main, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("src/test/java/io/pleme/wrapper/SmokeTest.java"), test, force)? { out.push(p); }
Ok(out)
}
}
pub struct JsDenoStarter;
impl GreenCiStarter for JsDenoStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_ts = "// pleme-io typed wrapper — auto-generated by caixa-forge.\nexport function smoke(): number { return 4; }\n";
let test_ts = "import { assertEquals } from \"jsr:@std/assert\";\nimport { smoke } from \"./mod.ts\";\n\nDeno.test(\"smoke\", () => assertEquals(smoke(), 4));\n";
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join("mod.ts"), mod_ts, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("mod_test.ts"), test_ts, force)? { out.push(p); }
Ok(out)
}
}
pub struct CppVcpkgStarter;
impl GreenCiStarter for CppVcpkgStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> {
let main = "// pleme-io typed wrapper — auto-generated by caixa-forge.\n\
#include <iostream>\n\
\n\
int smoke() { return 4; }\n\
int main() { std::cout << \"pleme-io wrapper stub\\n\"; return 0; }\n";
let test = "#include <cassert>\nint smoke();\nint main() { assert(smoke() == 4); return 0; }\n";
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join("src/main.cpp"), main, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("test/test_smoke.cpp"), test, force)? { out.push(p); }
Ok(out)
}
}
pub struct PythonCondaStarter;
impl GreenCiStarter for PythonCondaStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
PythonStarter.write(out_root, name, force)
}
}
pub struct AdaAlireStarter;
impl GreenCiStarter for AdaAlireStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = name.replace('-', "_");
let lib_ads = format!(
"-- pleme-io typed wrapper — auto-generated by caixa-forge.\n\
package {} is\n\
\x20 function Smoke return Integer;\n\
end {};\n", pascal(&mod_name), pascal(&mod_name));
let lib_adb = format!(
"package body {} is\n\
\x20 function Smoke return Integer is begin return 4; end Smoke;\n\
end {};\n", pascal(&mod_name), pascal(&mod_name));
let main_adb = format!(
"with {}; with Ada.Text_IO;\n\
procedure Main is\n\
begin\n\
\x20 Ada.Text_IO.Put_Line (Integer'Image ({}.Smoke));\n\
end Main;\n", pascal(&mod_name), pascal(&mod_name));
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join(format!("src/{mod_name}.ads")), &lib_ads, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join(format!("src/{mod_name}.adb")), &lib_adb, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("src/main.adb"), &main_adb, force)? { out.push(p); }
Ok(out)
}
}
pub struct ZigStarter;
impl GreenCiStarter for ZigStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> {
let main = "//! pleme-io typed wrapper — auto-generated by caixa-forge.\n\
const std = @import(\"std\");\n\
\n\
pub fn smoke() i32 { return 4; }\n\
\n\
pub fn main() void {\n\
\x20 std.debug.print(\"pleme-io wrapper stub\\n\", .{});\n\
}\n\
\n\
test \"smoke\" {\n\
\x20 try std.testing.expectEqual(@as(i32, 4), smoke());\n\
}\n";
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join("src/main.zig"), main, force)? { out.push(p); }
Ok(out)
}
}
pub struct FortranFpmStarter;
impl GreenCiStarter for FortranFpmStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = name.replace('-', "_");
let lib = format!(
"! pleme-io typed wrapper — auto-generated by caixa-forge.\n\
module {mod_name}\n\
\x20 implicit none\n\
\x20 private\n\
\x20 public :: smoke\n\
contains\n\
\x20 integer function smoke()\n\
\x20\x20\x20 smoke = 4\n\
\x20 end function smoke\n\
end module {mod_name}\n");
let test = format!(
"program test_smoke\n use {mod_name}, only: smoke\n implicit none\n\
\x20 if (smoke() /= 4) error stop \"smoke broken\"\n\
\x20 print *, \"smoke ok\"\nend program test_smoke\n");
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join(format!("src/{mod_name}.f90")), &lib, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("test/test_smoke.f90"), &test, force)? { out.push(p); }
Ok(out)
}
}
pub struct GleamStarter;
impl GreenCiStarter for GleamStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = name.replace('-', "_");
let lib = format!(
"// pleme-io typed wrapper — auto-generated by caixa-forge.\n\
pub fn smoke() -> Int {{\n 4\n}}\n");
let test = format!(
"import gleeunit\nimport gleeunit/should\nimport {mod_name}\n\n\
pub fn main() {{\n gleeunit.main()\n}}\n\n\
pub fn smoke_test() {{\n {mod_name}.smoke() |> should.equal(4)\n}}\n");
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join(format!("src/{mod_name}.gleam")), &lib, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join(format!("test/{mod_name}_test.gleam")), &test, force)? { out.push(p); }
Ok(out)
}
}
pub struct RacketInfoStarter;
impl GreenCiStarter for RacketInfoStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = name.replace('-', "_");
let lib = ";; pleme-io typed wrapper — auto-generated by caixa-forge.\n\
#lang racket\n\
(provide smoke)\n\
(define (smoke) 4)\n";
let test = format!(
";; smoke test for {mod_name}\n#lang racket\n\
(require rackunit \"{mod_name}.rkt\")\n\n\
(check-equal? (smoke) 4)\n");
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join(format!("{mod_name}.rkt")), lib, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("tests/smoke.rkt"), &test, force)? { out.push(p); }
Ok(out)
}
}
pub struct CrystalStarter;
impl GreenCiStarter for CrystalStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = name.replace('-', "_");
let src = format!(
"# pleme-io typed wrapper — auto-generated by caixa-forge.\n\
module {} \n\
\x20 def self.smoke\n 4\n end\nend\n",
pascal(&mod_name));
let spec = format!(
"require \"spec\"\nrequire \"../src/{mod_name}\"\n\n\
describe {} do\n it \"smoke\" do\n {}.smoke.should eq(4)\n end\nend\n",
pascal(&mod_name), pascal(&mod_name));
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join(format!("src/{mod_name}.cr")), &src, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("spec/smoke_spec.cr"), &spec, force)? { out.push(p); }
Ok(out)
}
}
pub struct DartStarter;
impl GreenCiStarter for DartStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = name.replace('-', "_");
let lib = format!(
"// pleme-io typed wrapper — auto-generated by caixa-forge.\n\
library {mod_name};\n\n\
int smoke() => 4;\n");
let test = format!(
"import 'package:test/test.dart';\nimport 'package:{mod_name}/{mod_name}.dart';\n\n\
void main() {{\n test('smoke', () {{\n expect(smoke(), equals(4));\n }});\n}}\n");
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join(format!("lib/{mod_name}.dart")), &lib, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("test/smoke_test.dart"), &test, force)? { out.push(p); }
Ok(out)
}
}
pub struct ComposerStarter;
impl GreenCiStarter for ComposerStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> {
let src = "<?php\n// pleme-io typed wrapper — auto-generated by caixa-forge.\n\
namespace Pleme\\Wrapper;\n\nclass Smoke {\n public static function value(): int { return 4; }\n}\n";
let test = "<?php\nuse PHPUnit\\Framework\\TestCase;\nuse Pleme\\Wrapper\\Smoke;\n\n\
class SmokeTest extends TestCase {\n public function testSmoke() { $this->assertEquals(4, Smoke::value()); }\n}\n";
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join("src/Smoke.php"), src, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("tests/SmokeTest.php"), test, force)? { out.push(p); }
Ok(out)
}
}
pub struct JuliaStarter;
impl GreenCiStarter for JuliaStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = pascal(&name.replace('-', "_"));
let src = format!(
"# pleme-io typed wrapper — auto-generated by caixa-forge.\n\
module {mod_name}\n\nexport smoke\n\nsmoke() = 4\n\nend # module\n");
let test = format!(
"using Test\nusing {mod_name}\n\n\
@testset \"smoke\" begin\n @test {mod_name}.smoke() == 4\nend\n");
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join(format!("src/{mod_name}.jl")), &src, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("test/runtests.jl"), &test, force)? { out.push(p); }
Ok(out)
}
}
fn pascal(s: &str) -> String {
s.split('_').map(|p| {
let mut c = p.chars();
c.next().map(|f| f.to_uppercase().chain(c).collect::<String>())
.unwrap_or_default()
}).collect()
}
pub struct ScalaSbtStarter;
impl GreenCiStarter for ScalaSbtStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> {
let main = "// pleme-io typed wrapper — auto-generated by caixa-forge.\n\
object Main {\n\
\x20 def smoke: Int = 4\n\
\x20 def main(args: Array[String]): Unit = println(\"pleme-io wrapper stub\")\n\
}\n";
let test = "import org.scalatest.funsuite.AnyFunSuite\n\
\n\
class SmokeTest extends AnyFunSuite {\n\
\x20 test(\"smoke\") { assert(Main.smoke == 4) }\n\
}\n";
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join("src/main/scala/Main.scala"), main, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("src/test/scala/SmokeTest.scala"), test, force)? { out.push(p); }
Ok(out)
}
}
pub struct ClojureDepsStarter;
impl GreenCiStarter for ClojureDepsStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let ns = name.replace('-', "_");
let core = format!(
";; pleme-io typed wrapper — auto-generated by caixa-forge.\n\
(ns {ns}.core)\n\
\n\
(defn smoke [] 4)\n"
);
let test = format!(
"(ns {ns}.core-test\n (:require [clojure.test :refer :all]\n [{ns}.core :as core]))\n\
\n\
(deftest smoke-test\n (is (= 4 (core/smoke))))\n"
);
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join("src").join(&ns).join("core.clj"), &core, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("test").join(&ns).join("core_test.clj"), &test, force)? { out.push(p); }
Ok(out)
}
}
pub struct SwiftSpmStarter;
impl GreenCiStarter for SwiftSpmStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let lib = "// pleme-io typed wrapper — auto-generated by caixa-forge.\n\
public enum Smoke {\n\
\x20 public static func value() -> Int { 4 }\n\
}\n";
let mut test = String::from("import XCTest\n@testable import ");
test.push_str(name);
test.push_str("\n\nfinal class SmokeTests: XCTestCase {\n");
test.push_str(" func testSmoke() { XCTAssertEqual(Smoke.value(), 4) }\n");
test.push_str("}\n");
let lib_path = out_root.join("Sources").join(name).join(format!("{name}.swift"));
let test_path = out_root.join("Tests").join(format!("{name}Tests")).join(format!("{name}Tests.swift"));
let mut out = vec![];
if let Some(p) = write_if_needed(&lib_path, lib, force)? { out.push(p); }
if let Some(p) = write_if_needed(&test_path, &test, force)? { out.push(p); }
Ok(out)
}
}
pub struct ElixirMixStarter;
impl GreenCiStarter for ElixirMixStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = name.replace('-', "_");
let pascal: String = mod_name.split('_').map(|p| {
let mut c = p.chars();
c.next().map(|f| f.to_uppercase().chain(c).collect::<String>())
.unwrap_or_default()
}).collect();
let mut lib = String::from("# pleme-io typed wrapper — auto-generated by caixa-forge.\ndefmodule ");
lib.push_str(&pascal);
lib.push_str(" do\n def smoke, do: 4\nend\n");
let mut test = String::from("defmodule ");
test.push_str(&pascal);
test.push_str("Test do\n use ExUnit.Case\n\n test \"smoke\" do\n assert ");
test.push_str(&pascal);
test.push_str(".smoke() == 4\n end\nend\n");
let test_helper = "ExUnit.start()\n";
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join(format!("lib/{mod_name}.ex")), &lib, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join(format!("test/{mod_name}_test.exs")), &test, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("test/test_helper.exs"), test_helper, force)? { out.push(p); }
Ok(out)
}
}
pub struct RubyGemStarter;
impl GreenCiStarter for RubyGemStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = name.replace('-', "_");
let mut lib = String::from("# pleme-io typed wrapper — auto-generated by caixa-forge.\n");
lib.push_str("module ");
let pascal: String = mod_name.split('_').map(|p| {
let mut c = p.chars();
c.next().map(|f| f.to_uppercase().chain(c).collect::<String>())
.unwrap_or_default()
}).collect();
lib.push_str(&pascal);
lib.push_str("\n def self.smoke\n 4\n end\nend\n");
let mut spec = String::from("require \"minitest/autorun\"\nrequire_relative \"../lib/");
spec.push_str(&mod_name);
spec.push_str("\"\n\nclass SmokeTest < Minitest::Test\n def test_smoke\n assert_equal 4, ");
spec.push_str(&pascal);
spec.push_str(".smoke\n end\nend\n");
let mut out = vec![];
let mut lib_path = out_root.join("lib");
lib_path.push(format!("{mod_name}.rb"));
if let Some(p) = write_if_needed(&lib_path, &lib, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("test/test_smoke.rb"), &spec, force)? { out.push(p); }
Ok(out)
}
}
pub struct OcamlDuneStarter;
impl GreenCiStarter for OcamlDuneStarter {
fn write(&self, out_root: &Path, name: &str, force: bool) -> Result<Vec<PathBuf>> {
let mod_name = name.replace('-', "_");
let lib_dune = format!("(library\n (name {mod_name})\n (public_name {name}))\n");
let lib_ml = "(* pleme-io typed wrapper — auto-generated by caixa-forge. *)\nlet smoke () = 4\n";
let test_dune = format!("(test\n (name test_smoke)\n (libraries {mod_name}))\n");
let mut test_ml = String::from("let () =\n assert (");
test_ml.push_str(&mod_name);
test_ml.push_str(".smoke () = 4)\n");
let mut out = vec![];
if let Some(p) = write_if_needed(&out_root.join("lib/dune"), &lib_dune, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join(format!("lib/{mod_name}.ml")), lib_ml, force)? { out.push(p); }
if let Some(p) = write_if_needed(&out_root.join("test/dune"), &test_dune, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("test/test_smoke.ml"), &test_ml, force)? { out.push(p); }
Ok(out)
}
}
pub struct DotnetCsprojStarter;
impl GreenCiStarter for DotnetCsprojStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> {
let program = "// pleme-io typed wrapper — auto-generated by caixa-forge.\n\
using System;\n\
\n\
namespace Pleme.Wrapper;\n\
\n\
public static class Program\n\
{\n\
\x20 public static int Smoke() => 4;\n\
\n\
\x20 public static void Main() => Console.WriteLine(\"pleme-io wrapper stub\");\n\
}\n";
let mut out = vec![];
if let Some(p) = write_if_needed(&out_root.join("Program.cs"), program, force)? { out.push(p); }
Ok(out)
}
}
pub struct HelmStarter;
impl GreenCiStarter for HelmStarter {
fn write(&self, out_root: &Path, _name: &str, force: bool) -> Result<Vec<PathBuf>> {
let helpers = "{{/*\n\
Expand the name of the chart.\n\
*/}}\n\
{{- define \"chart.name\" -}}\n\
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" -}}\n\
{{- end -}}\n\
\n\
{{/*\n\
Create a default fully qualified app name.\n\
*/}}\n\
{{- define \"chart.fullname\" -}}\n\
{{- $name := default .Chart.Name .Values.nameOverride -}}\n\
{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" -}}\n\
{{- end -}}\n";
let deployment = "{{- if .Values.enabled -}}\n\
apiVersion: apps/v1\n\
kind: Deployment\n\
metadata:\n\
\x20 name: {{ include \"chart.fullname\" . }}\n\
\x20 labels:\n\
\x20\x20\x20 app.kubernetes.io/name: {{ include \"chart.name\" . }}\n\
spec:\n\
\x20 replicas: {{ .Values.replicaCount }}\n\
\x20 selector:\n\
\x20\x20\x20 matchLabels:\n\
\x20\x20\x20\x20\x20 app.kubernetes.io/name: {{ include \"chart.name\" . }}\n\
\x20 template:\n\
\x20\x20\x20 metadata:\n\
\x20\x20\x20\x20\x20 labels:\n\
\x20\x20\x20\x20\x20\x20\x20 app.kubernetes.io/name: {{ include \"chart.name\" . }}\n\
\x20\x20\x20 spec:\n\
\x20\x20\x20\x20\x20 containers:\n\
\x20\x20\x20\x20\x20\x20\x20 - name: app\n\
\x20\x20\x20\x20\x20\x20\x20\x20\x20 image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n\
{{- end }}\n";
let mut out = vec![];
if let Some(p) = write_if_needed(
&out_root.join("templates/_helpers.tpl"), helpers, force)? { out.push(p); }
if let Some(p) = write_if_needed(
&out_root.join("templates/deployment.yaml"), deployment, force)? { out.push(p); }
Ok(out)
}
}
pub struct NoStarter;
impl GreenCiStarter for NoStarter {
fn write(&self, _out_root: &Path, _name: &str, _force: bool) -> Result<Vec<PathBuf>> {
Ok(vec![])
}
}
pub fn starter_for(ecosystem: &str) -> &'static dyn GreenCiStarter {
match ecosystem {
"rust-single-crate" | "rust-workspace" => &RustStarter,
"go" => &GoStarter,
"npm" | "js-pnpm" => &NpmStarter,
"python" | "python-pdm" => &PythonStarter,
"helm" => &HelmStarter,
"ruby-gem" => &RubyGemStarter,
"ocaml-dune" => &OcamlDuneStarter,
"dotnet-csproj" => &DotnetCsprojStarter,
"swift-spm" => &SwiftSpmStarter,
"elixir-mix" => &ElixirMixStarter,
"scala-sbt" => &ScalaSbtStarter,
"clojure-deps" => &ClojureDepsStarter,
"crystal" => &CrystalStarter,
"dart" => &DartStarter,
"composer" => &ComposerStarter,
"julia" => &JuliaStarter,
"zig" => &ZigStarter,
"fortran-fpm" => &FortranFpmStarter,
"gleam" => &GleamStarter,
"racket-info" => &RacketInfoStarter,
"java-maven" => &JavaMavenStarter,
"js-deno" => &JsDenoStarter,
"cpp-vcpkg" => &CppVcpkgStarter,
"python-conda" => &PythonCondaStarter,
"ada-alire" => &AdaAlireStarter,
"java-gradle-kts" => &JavaGradleKtsStarter,
"cpp-cmake" => &CppCmakeStarter,
"cpp-meson" => &CppMesonStarter,
"cpp-conan" => &CppConanStarter,
"haskell-cabal" => &HaskellCabalStarter,
"nim-nimble" => &NimNimbleStarter,
"lua-rockspec" => &LuaRockspecStarter,
"r-description" => &RDescriptionStarter,
"python-pipenv" => &PythonPipenvStarter,
"github-action" => &GithubActionStarter,
_ => &NoStarter,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp() -> tempdir::TempDir {
tempdir::TempDir::new("green-ci").expect("tempdir")
}
#[test]
fn rust_starter_emits_lib_rs_with_smoke() {
let dir = tmp();
let files = RustStarter.write(dir.path(), "x", true).unwrap();
assert_eq!(files.len(), 1);
let body = fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(body.contains("fn smoke"));
assert!(body.contains("assert_eq!(2 + 2, 4)"));
}
#[test]
fn go_starter_emits_main_and_test() {
let dir = tmp();
let files = GoStarter.write(dir.path(), "x", true).unwrap();
assert_eq!(files.len(), 2);
assert!(dir.path().join("main.go").is_file());
let test = fs::read_to_string(dir.path().join("main_test.go")).unwrap();
assert!(test.contains("func TestSmoke"));
}
#[test]
fn npm_starter_emits_index_and_test() {
let dir = tmp();
let files = NpmStarter.write(dir.path(), "x", true).unwrap();
assert_eq!(files.len(), 2);
assert!(dir.path().join("src/index.js").is_file());
let test = fs::read_to_string(dir.path().join("test/test.js")).unwrap();
assert!(test.contains("test('smoke',"));
}
#[test]
fn python_starter_substitutes_name_safely_via_typed_ast() {
let dir = tmp();
let files = PythonStarter.write(dir.path(), "my-cool-pkg", true).unwrap();
assert_eq!(files.len(), 2);
assert!(dir.path().join("src/my_cool_pkg/__init__.py").is_file());
let test = fs::read_to_string(dir.path().join("tests/test_smoke.py")).unwrap();
assert!(test.contains("from my_cool_pkg import smoke"));
assert!(test.contains("class SmokeTest(unittest.TestCase):"));
let class_pos = test.find("class SmokeTest").unwrap();
let main_pos = test.find("__main__").unwrap();
assert!(main_pos > class_pos,
"__main__ guard should follow class def; got: {test}");
}
#[test]
fn write_is_idempotent_when_force_false_and_file_exists() {
let dir = tmp();
let first = RustStarter.write(dir.path(), "x", false).unwrap();
assert_eq!(first.len(), 1);
fs::write(dir.path().join("src/lib.rs"), "// tampered\n").unwrap();
let second = RustStarter.write(dir.path(), "x", false).unwrap();
assert_eq!(second.len(), 0, "expected no write when force=false + file exists");
let body = fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert_eq!(body, "// tampered\n", "tampered content should be preserved");
}
#[test]
fn starter_for_returns_no_starter_for_unknown_ecosystem() {
let s = starter_for("some-future-ecosystem");
let dir = tmp();
let files = s.write(dir.path(), "x", true).unwrap();
assert!(files.is_empty(), "unknown ecosystem should get NoStarter");
}
#[test]
fn helm_starter_emits_template_files() {
let dir = tmp();
let files = HelmStarter.write(dir.path(), "x", true).unwrap();
assert_eq!(files.len(), 2);
assert!(dir.path().join("templates/_helpers.tpl").is_file());
let dep = fs::read_to_string(dir.path().join("templates/deployment.yaml")).unwrap();
assert!(dep.contains("apiVersion: apps/v1"));
assert!(dep.contains("kind: Deployment"));
assert!(dep.contains("include \"chart.fullname\""));
}
#[test]
fn starter_for_dispatches_rust_correctly() {
let s = starter_for("rust-single-crate");
let dir = tmp();
let files = s.write(dir.path(), "x", true).unwrap();
assert_eq!(files.len(), 1);
assert!(dir.path().join("src/lib.rs").is_file());
}
}