noxid-cli 0.2.0

The Noxid compiler command line: check, build, test, adapt, and the agent surface
#[derive(Clone, Copy, Debug)]
pub struct StorageContractDriver {
    pub name: &'static str,
    pub manifest_value: &'static str,
    pub persists_across_processes: bool,
    pub needs_database: bool,
    pub needs_redis: bool,
}

pub const BUILTIN_STORAGE_DRIVERS: &[StorageContractDriver] = &[
    StorageContractDriver {
        name: "memory",
        manifest_value: "memory",
        persists_across_processes: false,
        needs_database: false,
        needs_redis: false,
    },
    StorageContractDriver {
        name: "fs",
        manifest_value: "fs",
        persists_across_processes: true,
        needs_database: false,
        needs_redis: false,
    },
    StorageContractDriver {
        name: "postgres",
        manifest_value: "postgres",
        persists_across_processes: true,
        needs_database: true,
        needs_redis: false,
    },
    StorageContractDriver {
        name: "redis",
        manifest_value: "redis",
        persists_across_processes: true,
        needs_database: false,
        needs_redis: true,
    },
];

pub const STORAGE_API_CONTRACT: &str = r#"
import { storage } from "./server/noxid-server.js";
const direct = storage("wo27-contract");
const isolated = storage("wo27-contract-isolated");

await direct.set("alpha", { value: 1 });
await direct.set("alphabet", 2);
await direct.set("beta", null);
await isolated.set("alpha", { isolated: true });
if ((await direct.get("alpha"))?.value !== 1) throw new Error("get/set did not round trip JSON");
if ((await isolated.get("alpha"))?.isolated !== true) throw new Error("namespaces aliased");
if (JSON.stringify(await direct.list("alpha")) !== JSON.stringify(["alpha", "alphabet"])) throw new Error("prefix list changed");
if (JSON.stringify(await direct.list()) !== JSON.stringify(["alpha", "alphabet", "beta"])) throw new Error("sorted list changed");
for (const [key, value] of [["high-\ud800", 1], ["low-\udfff", 2], ["\u001fnoxid:utf16:literal", 3]]) {
  await direct.set(key, value);
  if (await direct.get(key) !== value) throw new Error(`full JavaScript string key did not round trip: ${JSON.stringify(key)}`);
}
const unusual = await direct.list();
if (!["high-\ud800", "low-\udfff", "\u001fnoxid:utf16:literal"].every((key) => unusual.includes(key))) throw new Error(`full JavaScript string keys did not survive list: ${JSON.stringify(unusual)}`);
await direct.set("expired", true, { ttl: 0.01 });
await new Promise((resolve) => setTimeout(resolve, 30));
if (await direct.get("expired") !== null || (await direct.list()).includes("expired")) throw new Error("TTL did not expire on read/list");
if (!(await direct.delete("alphabet")) || await direct.delete("alphabet")) throw new Error("delete result changed");
await direct.set("persistent", { value: 7 });
process.exit(0);
"#;

pub fn persistence_check(expects_value: bool) -> String {
    format!(
        r#"import {{ storage }} from "./server/noxid-server.js";
const value = await storage("wo27-contract").get("persistent");
if ({expects_value} ? value?.value !== 7 : value !== null) throw new Error(`persistence changed: ${{JSON.stringify(value)}}`);
process.exit(0);
"#
    )
}