diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml
index c8f1dad6..55bf3a65 100644
--- a/.github/workflows/examples.yml
+++ b/.github/workflows/examples.yml
@@ -54,10 +54,6 @@ jobs:
with:
shared-key: examples-hm
- - name: Install esbuild (for harmont-ts bundle)
- working-directory: crates/hm-dsl-engine/harmont-ts
- run: npm ci
-
- name: Build hm
run: cargo build -p harmont-cli
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index aaac3470..e3c42d07 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,10 +25,6 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- - name: Install esbuild (for harmont-ts bundle)
- working-directory: crates/hm-dsl-engine/harmont-ts
- run: npm ci
-
- name: Set version from tag
run: |
VERSION="${GITHUB_REF_NAME#v}"
@@ -181,52 +177,6 @@ jobs:
cargo publish -p harmont-cli --token ${{ secrets.CRATES_IO_TOKEN }} --allow-dirty --no-verify
fi
- npm:
- name: Publish to npm
- runs-on: ubuntu-latest
- timeout-minutes: 10
- permissions:
- contents: read
- id-token: write
- steps:
- - uses: actions/checkout@v4
-
- - uses: actions/setup-node@v4
- with:
- node-version: "20"
- registry-url: "https://registry.npmjs.org"
- cache: npm
- cache-dependency-path: crates/hm-dsl-engine/harmont-ts/package-lock.json
-
- # node 20 ships npm 10, which can sign provenance but cannot AUTHENTICATE
- # via OIDC trusted publishing — that needs npm >= 11.5.1. Without it the
- # publish PUT goes out unauthenticated and npm masks it as a 404. Upgrade
- # so the configured trusted publisher actually authenticates the publish.
- - name: Upgrade npm for OIDC trusted publishing
- run: npm install -g npm@latest
-
- - name: Install dependencies
- working-directory: crates/hm-dsl-engine/harmont-ts
- run: npm ci
-
- - name: Build
- working-directory: crates/hm-dsl-engine/harmont-ts
- run: npm run build
-
- - name: Set version from tag
- working-directory: crates/hm-dsl-engine/harmont-ts
- run: npm version "${GITHUB_REF_NAME#v}" --no-git-tag-version
-
- - name: Publish
- working-directory: crates/hm-dsl-engine/harmont-ts
- run: |
- VERSION="${GITHUB_REF_NAME#v}"
- if npm view @harmont/hm@"$VERSION" version 2>/dev/null; then
- echo "@harmont/hm@$VERSION already published, skipping"
- else
- npm publish --access public --provenance
- fi
-
pypi:
name: Publish to PyPI
runs-on: ubuntu-latest
@@ -303,16 +253,6 @@ jobs:
if: matrix.musl
run: sudo apt-get update && sudo apt-get install -y musl-tools
- - uses: actions/setup-node@v4
- with:
- node-version: "20"
- cache: npm
- cache-dependency-path: crates/hm-dsl-engine/harmont-ts/package-lock.json
-
- - name: Install esbuild
- working-directory: crates/hm-dsl-engine/harmont-ts
- run: npm ci
-
- name: Set version from tag
run: |
VERSION="${GITHUB_REF_NAME#v}"
diff --git a/.hm/ci.py b/.hm/ci.py
index 56629d69..8d3b8d3d 100644
--- a/.hm/ci.py
+++ b/.hm/ci.py
@@ -25,11 +25,7 @@ def rust_project(shared_base: hm.Target[hm.Step]) -> tuple[hm.Step, ...]:
# the build emits an 18-byte stub and the bundled_sources tests fail (CLI-37).
# Installing Node here also lets the JS-runtime-gated render tests actually
# run instead of self-skipping.
- ts_deps = hm.js.project(
- path="crates/hm-dsl-engine/harmont-ts",
- base=shared_base,
- ).install()
- project = hm.rust.project(path=".", base=ts_deps)
+ project = hm.rust.project(path=".")
return hm.group([
project.test(), # cargo test --workspace --locked — every package
project.clippy(),
@@ -51,18 +47,6 @@ def py_project(shared_base: hm.Target[hm.Step]) -> tuple[hm.Step, ...]:
])
-@hm.target()
-def ts_project(shared_base: hm.Target[hm.Step]) -> tuple[hm.Step, ...]:
- project = hm.js.project(
- path="crates/hm-dsl-engine/harmont-ts",
- base=shared_base,
- )
- return hm.group([
- project.run("typecheck", label=":typescript: tsc"),
- project.run("test", label=":test_tube: vitest"),
- ])
-
-
@hm.pipeline(
"ci",
env={"CI": "true"},
@@ -74,6 +58,5 @@ def ts_project(shared_base: hm.Target[hm.Step]) -> tuple[hm.Step, ...]:
def ci(
rust_project: hm.Target[tuple[hm.Step, ...]],
py_project: hm.Target[tuple[hm.Step, ...]],
- ts_project: hm.Target[tuple[hm.Step, ...]],
) -> list:
- return [rust_project, py_project, ts_project]
+ return [rust_project, py_project]
diff --git a/CLAUDE.md b/CLAUDE.md
index d4eb60e6..4d8d5c1b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -7,22 +7,18 @@ The `cli/` directory is a Cargo workspace.
- `crates/hm-util/` — shared OS and filesystem utilities.
- `crates/hm-plugin-protocol/` — wire types (serde structs only).
- `crates/hm-plugin-sdk/` — authoring SDK for plugin writers.
-Run `cargo build` from the workspace root. Build requires esbuild
-(`npm ci` in `crates/hm-dsl-engine/harmont-ts/`).
+Run `cargo build` from the workspace root.
For cross-cutting doctrine see [PRINCIPLES.md](../PRINCIPLES.md).
-## DSLs
+## DSL
-Both DSLs live inside `crates/hm-dsl-engine/` so they ship with the crate:
-
-- `crates/hm-dsl-engine/harmont-py/` — the `harmont` Python package (pipeline DSL).
-- `crates/hm-dsl-engine/harmont-ts/` — the `harmont` TypeScript package (pipeline DSL).
+The `harmont` Python package (pipeline DSL) lives inside `crates/hm-dsl-engine/harmont-py/` so it ships with the crate.
## Keep the SDK, `hm init` templates, and docs in sync
The toolchain helpers in `crates/hm-dsl-engine/` (e.g.
-`harmont-py/harmont/_rust.py`, `harmont-ts/src/toolchains/rust.ts`) are the
+`harmont-py/harmont/_rust.py`) are the
**public authoring SDK**. They have two downstream surfaces that drift silently
unless you update them in the same change. **A toolchain change is not done until
all three agree:**
diff --git a/README.md b/README.md
index ebc4862e..a797c840 100644
--- a/README.md
+++ b/README.md
@@ -13,12 +13,12 @@
- CI/CD as real code. Write your pipelines in Python or TypeScript, then run the exact same pipeline locally in Docker or on managed runners in Harmont Cloud — with layer caching and DAG parallelism built in.
+ CI/CD as real code. Write your pipelines in Python, then run the exact same pipeline locally in Docker or on managed runners in Harmont Cloud — with layer caching and DAG parallelism built in.
## What is Harmont?
-Harmont lets you define CI/CD pipelines in **TypeScript or Python** and run them
+Harmont lets you define CI/CD pipelines in **Python** and run them
two ways from a single definition: instantly on your own machine in Docker, or on
managed runners in [Harmont Cloud](https://app.harmont.dev). It's the same
pipeline either way — the run you debug locally is byte-for-byte the run that
@@ -76,7 +76,7 @@ cargo install harmont-cli
hm init
```
-`hm init` scaffolds a working `.hm/pipeline.{py,ts}` from a template and offers
+`hm init` scaffolds a working `.hm/pipeline.{py}` from a template and offers
to install Claude Code skills that write and maintain your pipeline. Run it and
pick your stack from the menu, or name a template up front with `-t`:
@@ -99,10 +99,7 @@ same pipeline, on managed runners. See [Cloud](#cloud) below.
### Or write it by hand
-A pipeline is just code. Save this as `.hm/pipeline.py` (or `.hm/pipeline.ts`):
-
-
-Python
+A pipeline is just code. Save this as `.hm/pipeline.py`:
```python
import harmont as hm
@@ -125,37 +122,6 @@ def ci(project: hm.Target[PythonToolchain]) -> tuple[hm.Step, ...]:
)
```
-
-
-
-TypeScript
-
-```typescript
-import { pipeline, push, type PipelineDefinition } from "@harmont/hm";
-import { python } from "@harmont/hm/toolchains";
-
-const project = python({ path: "." });
-
-const pipelines: PipelineDefinition[] = [
- {
- slug: "ci",
- triggers: [push({ branch: "main" })],
- pipeline: pipeline(
- [
- project.test(),
- project.lint(),
- project.fmt(),
- project.typecheck(),
- ],
- ),
- },
-];
-
-export default pipelines;
-```
-
-
-
```sh
hm run ci
```
@@ -372,7 +338,7 @@ input reference, sub-actions, and caching details.
## Examples
The [`examples/`](./examples) directory has a complete, runnable pipeline for
-each stack — every one shipped in **both** Python and TypeScript:
+each stack:
| | | |
|---|---|---|
diff --git a/crates/hm-dsl-engine/Cargo.toml b/crates/hm-dsl-engine/Cargo.toml
index 4c489a2a..4d0eb80c 100644
--- a/crates/hm-dsl-engine/Cargo.toml
+++ b/crates/hm-dsl-engine/Cargo.toml
@@ -4,7 +4,7 @@ version = "0.0.0-dev"
edition.workspace = true
license.workspace = true
repository.workspace = true
-description = "DSL engine: evaluate Python/TypeScript pipeline definitions via system runtimes."
+description = "DSL engine: evaluate Python pipeline definitions via system runtimes."
keywords = ["ci", "harmont", "dsl"]
categories = ["command-line-utilities"]
diff --git a/crates/hm-dsl-engine/build.rs b/crates/hm-dsl-engine/build.rs
deleted file mode 100644
index c66e0f31..00000000
--- a/crates/hm-dsl-engine/build.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-// Build scripts legitimately panic on errors — no runtime to propagate to.
-#![allow(
- clippy::expect_used,
- clippy::unwrap_used,
- clippy::panic,
- clippy::print_stderr
-)]
-
-use std::env;
-use std::path::{Path, PathBuf};
-use std::process::Command;
-
-fn main() {
- let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
- let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
- let ts_src = manifest_dir.join("harmont-ts/src");
-
- println!("cargo:rerun-if-changed=build.rs");
- println!("cargo:rerun-if-changed={}", ts_src.display());
-
- let esbuild = find_esbuild(&manifest_dir);
-
- let Some(esbuild) = esbuild else {
- panic!(
- "esbuild not found.\
- Install it or run `npm ci` in crates/hm-dsl-engine/harmont-ts/ for generating js bundles before building the crate."
- );
- };
-
- bundle(
- &esbuild,
- &ts_src.join("index.ts"),
- &out_dir.join("harmont-index.mjs"),
- );
- bundle(
- &esbuild,
- &ts_src.join("toolchains/index.ts"),
- &out_dir.join("harmont-toolchains.mjs"),
- );
-}
-
-fn bundle(esbuild: &Path, entry: &Path, outfile: &Path) {
- let status = Command::new(esbuild)
- .arg(entry)
- .arg("--bundle")
- .arg("--format=esm")
- .arg("--platform=node")
- .arg(format!("--outfile={}", outfile.display()))
- .status()
- .expect("failed to run esbuild");
-
- assert!(status.success(), "esbuild failed for {}", entry.display());
-}
-
-fn find_esbuild(manifest_dir: &Path) -> Option {
- let local = manifest_dir.join("harmont-ts/node_modules/.bin/esbuild");
- if local.exists() {
- return Some(local);
- }
- let which = Command::new("which").arg("esbuild").output().ok()?;
- if which.status.success() {
- let path = String::from_utf8_lossy(&which.stdout).trim().to_string();
- if !path.is_empty() {
- return Some(PathBuf::from(path));
- }
- }
- None
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/CLAUDE.md b/crates/hm-dsl-engine/harmont-ts/CLAUDE.md
deleted file mode 100644
index 51730277..00000000
--- a/crates/hm-dsl-engine/harmont-ts/CLAUDE.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# harmont (TypeScript DSL)
-
-TypeScript pipeline DSL — equivalent of `harmont-py/` (sibling directory).
-
-## Commands
-
-- `npm test` — run Vitest test suite
-- `npm run build` — compile TypeScript to `dist/`
-
-## Architecture
-
-- `src/step.ts` — Step class (immutable chain primitive)
-- `src/cache.ts` — Cache policy discriminated unions
-- `src/triggers.ts` — Trigger factory functions
-- `src/keys.ts` — Step key resolution (slug/hash)
-- `src/pipeline.ts` — Lowering pass (step chains → petgraph IR)
-- `src/target.ts` — Memoized reusable targets
-- `src/envelope.ts` — Envelope rendering (schema_version:1)
-- `src/toolchains/` — Language toolchain abstractions
-- `src/index.ts` — Public API barrel export
-
-## IR Compatibility
-
-Output must match the v0 IR that `crates/hm-pipeline-ir/` deserializes.
-The Rust `CommandStep` accepts: key, cmd, label?, image?, env?, timeout_seconds?, cache?, runner?, runner_args?.
-The Rust `Cache` accepts: policy, key?.
-Edge kinds: `builds_in`, `depends_on`.
-Envelope: `{ schema_version: "1", pipelines: [...] }`.
diff --git a/crates/hm-dsl-engine/harmont-ts/package-lock.json b/crates/hm-dsl-engine/harmont-ts/package-lock.json
deleted file mode 100644
index 64eb979b..00000000
--- a/crates/hm-dsl-engine/harmont-ts/package-lock.json
+++ /dev/null
@@ -1,1628 +0,0 @@
-{
- "name": "@harmont/hm",
- "version": "0.0.0-dev",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "@harmont/hm",
- "version": "0.0.0-dev",
- "devDependencies": {
- "@types/node": "^25.9.1",
- "typescript": "^5.8.0",
- "vitest": "^3.2.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
- "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
- "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
- "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
- "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
- "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
- "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
- "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
- "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
- "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
- "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
- "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
- "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
- "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
- "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
- "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
- "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
- "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
- "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
- "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
- "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
- "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
- "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
- "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
- "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
- "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
- "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
- "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
- "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
- "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
- "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
- "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
- "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
- "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
- "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
- "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
- "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
- "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
- "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
- "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
- "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
- "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
- "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
- "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
- "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
- "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
- "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ]
- },
- "node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
- "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
- "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
- "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
- "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
- "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@types/chai": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
- "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/deep-eql": "*",
- "assertion-error": "^2.0.1"
- }
- },
- "node_modules/@types/deep-eql": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/estree": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
- "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "25.9.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
- "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": ">=7.24.0 <7.24.7"
- }
- },
- "node_modules/@vitest/expect": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
- "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/chai": "^5.2.2",
- "@vitest/spy": "3.2.4",
- "@vitest/utils": "3.2.4",
- "chai": "^5.2.0",
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/mocker": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
- "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "3.2.4",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.17"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "msw": "^2.4.9",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
- },
- "peerDependenciesMeta": {
- "msw": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
- }
- },
- "node_modules/@vitest/pretty-format": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
- "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/runner": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
- "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/utils": "3.2.4",
- "pathe": "^2.0.3",
- "strip-literal": "^3.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/snapshot": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
- "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "3.2.4",
- "magic-string": "^0.30.17",
- "pathe": "^2.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/spy": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
- "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyspy": "^4.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/utils": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
- "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "3.2.4",
- "loupe": "^3.1.4",
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/cac": {
- "version": "6.7.14",
- "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
- "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/chai": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
- "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "assertion-error": "^2.0.1",
- "check-error": "^2.1.1",
- "deep-eql": "^5.0.1",
- "loupe": "^3.1.0",
- "pathval": "^2.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/check-error": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
- "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 16"
- }
- },
- "node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/deep-eql": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
- "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/es-module-lexer": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/esbuild": {
- "version": "0.27.7",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
- "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.27.7",
- "@esbuild/android-arm": "0.27.7",
- "@esbuild/android-arm64": "0.27.7",
- "@esbuild/android-x64": "0.27.7",
- "@esbuild/darwin-arm64": "0.27.7",
- "@esbuild/darwin-x64": "0.27.7",
- "@esbuild/freebsd-arm64": "0.27.7",
- "@esbuild/freebsd-x64": "0.27.7",
- "@esbuild/linux-arm": "0.27.7",
- "@esbuild/linux-arm64": "0.27.7",
- "@esbuild/linux-ia32": "0.27.7",
- "@esbuild/linux-loong64": "0.27.7",
- "@esbuild/linux-mips64el": "0.27.7",
- "@esbuild/linux-ppc64": "0.27.7",
- "@esbuild/linux-riscv64": "0.27.7",
- "@esbuild/linux-s390x": "0.27.7",
- "@esbuild/linux-x64": "0.27.7",
- "@esbuild/netbsd-arm64": "0.27.7",
- "@esbuild/netbsd-x64": "0.27.7",
- "@esbuild/openbsd-arm64": "0.27.7",
- "@esbuild/openbsd-x64": "0.27.7",
- "@esbuild/openharmony-arm64": "0.27.7",
- "@esbuild/sunos-x64": "0.27.7",
- "@esbuild/win32-arm64": "0.27.7",
- "@esbuild/win32-ia32": "0.27.7",
- "@esbuild/win32-x64": "0.27.7"
- }
- },
- "node_modules/estree-walker": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "^1.0.0"
- }
- },
- "node_modules/expect-type": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
- "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=12.0.0"
- }
- },
- "node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/js-tokens": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
- "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/loupe": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
- "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/nanoid": {
- "version": "3.3.12",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
- "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/pathe": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/pathval": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
- "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14.16"
- }
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/postcss": {
- "version": "8.5.15",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
- "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.12",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/rollup": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
- "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "1.0.8"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
- },
- "optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.60.4",
- "@rollup/rollup-android-arm64": "4.60.4",
- "@rollup/rollup-darwin-arm64": "4.60.4",
- "@rollup/rollup-darwin-x64": "4.60.4",
- "@rollup/rollup-freebsd-arm64": "4.60.4",
- "@rollup/rollup-freebsd-x64": "4.60.4",
- "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
- "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
- "@rollup/rollup-linux-arm64-gnu": "4.60.4",
- "@rollup/rollup-linux-arm64-musl": "4.60.4",
- "@rollup/rollup-linux-loong64-gnu": "4.60.4",
- "@rollup/rollup-linux-loong64-musl": "4.60.4",
- "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
- "@rollup/rollup-linux-ppc64-musl": "4.60.4",
- "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
- "@rollup/rollup-linux-riscv64-musl": "4.60.4",
- "@rollup/rollup-linux-s390x-gnu": "4.60.4",
- "@rollup/rollup-linux-x64-gnu": "4.60.4",
- "@rollup/rollup-linux-x64-musl": "4.60.4",
- "@rollup/rollup-openbsd-x64": "4.60.4",
- "@rollup/rollup-openharmony-arm64": "4.60.4",
- "@rollup/rollup-win32-arm64-msvc": "4.60.4",
- "@rollup/rollup-win32-ia32-msvc": "4.60.4",
- "@rollup/rollup-win32-x64-gnu": "4.60.4",
- "@rollup/rollup-win32-x64-msvc": "4.60.4",
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/rollup/node_modules/@types/estree": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
- "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/siginfo": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
- "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/stackback": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
- "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/std-env": {
- "version": "3.10.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
- "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/strip-literal": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
- "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^9.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/tinybench": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
- "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinyexec": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
- "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinyglobby": {
- "version": "0.2.16",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
- "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/tinypool": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
- "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- }
- },
- "node_modules/tinyrainbow": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
- "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/tinyspy": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
- "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/undici-types": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
- "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/vite": {
- "version": "7.3.3",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz",
- "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "^0.27.0",
- "fdir": "^6.5.0",
- "picomatch": "^4.0.3",
- "postcss": "^8.5.6",
- "rollup": "^4.43.0",
- "tinyglobby": "^0.2.15"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^20.19.0 || >=22.12.0",
- "jiti": ">=1.21.0",
- "less": "^4.0.0",
- "lightningcss": "^1.21.0",
- "sass": "^1.70.0",
- "sass-embedded": "^1.70.0",
- "stylus": ">=0.54.8",
- "sugarss": "^5.0.0",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- },
- "node_modules/vite-node": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
- "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cac": "^6.7.14",
- "debug": "^4.4.1",
- "es-module-lexer": "^1.7.0",
- "pathe": "^2.0.3",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
- },
- "bin": {
- "vite-node": "vite-node.mjs"
- },
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/vitest": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
- "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/chai": "^5.2.2",
- "@vitest/expect": "3.2.4",
- "@vitest/mocker": "3.2.4",
- "@vitest/pretty-format": "^3.2.4",
- "@vitest/runner": "3.2.4",
- "@vitest/snapshot": "3.2.4",
- "@vitest/spy": "3.2.4",
- "@vitest/utils": "3.2.4",
- "chai": "^5.2.0",
- "debug": "^4.4.1",
- "expect-type": "^1.2.1",
- "magic-string": "^0.30.17",
- "pathe": "^2.0.3",
- "picomatch": "^4.0.2",
- "std-env": "^3.9.0",
- "tinybench": "^2.9.0",
- "tinyexec": "^0.3.2",
- "tinyglobby": "^0.2.14",
- "tinypool": "^1.1.1",
- "tinyrainbow": "^2.0.0",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
- "vite-node": "3.2.4",
- "why-is-node-running": "^2.3.0"
- },
- "bin": {
- "vitest": "vitest.mjs"
- },
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@edge-runtime/vm": "*",
- "@types/debug": "^4.1.12",
- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
- "@vitest/browser": "3.2.4",
- "@vitest/ui": "3.2.4",
- "happy-dom": "*",
- "jsdom": "*"
- },
- "peerDependenciesMeta": {
- "@edge-runtime/vm": {
- "optional": true
- },
- "@types/debug": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@vitest/browser": {
- "optional": true
- },
- "@vitest/ui": {
- "optional": true
- },
- "happy-dom": {
- "optional": true
- },
- "jsdom": {
- "optional": true
- }
- }
- },
- "node_modules/why-is-node-running": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
- "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "siginfo": "^2.0.0",
- "stackback": "0.0.2"
- },
- "bin": {
- "why-is-node-running": "cli.js"
- },
- "engines": {
- "node": ">=8"
- }
- }
- }
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/package.json b/crates/hm-dsl-engine/harmont-ts/package.json
deleted file mode 100644
index 77500f1a..00000000
--- a/crates/hm-dsl-engine/harmont-ts/package.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "name": "@harmont/hm",
- "version": "0.0.0-dev",
- "type": "module",
- "repository": {
- "type": "git",
- "url": "git+https://github.com/harmont-dev/harmont-cli.git",
- "directory": "crates/hm-dsl-engine/harmont-ts"
- },
- "exports": {
- ".": {
- "import": "./dist/index.js",
- "types": "./dist/index.d.ts"
- },
- "./toolchains": {
- "import": "./dist/toolchains/index.js",
- "types": "./dist/toolchains/index.d.ts"
- }
- },
- "files": [
- "dist"
- ],
- "scripts": {
- "build": "tsc",
- "typecheck": "tsc --noEmit",
- "test": "vitest run",
- "test:watch": "vitest"
- },
- "devDependencies": {
- "@types/node": "^25.9.1",
- "typescript": "^5.8.0",
- "vitest": "^3.2.0"
- },
- "engines": {
- "node": ">=18"
- }
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/cache.ts b/crates/hm-dsl-engine/harmont-ts/src/cache.ts
deleted file mode 100644
index fc423c6d..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/cache.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-export interface CacheForever {
- readonly kind: "forever";
- readonly envKeys: readonly string[];
-}
-
-export interface CacheTTL {
- readonly kind: "ttl";
- readonly durationSeconds: number;
- readonly envKeys: readonly string[];
-}
-
-export interface CacheOnChange {
- readonly kind: "on_change";
- readonly paths: readonly string[];
-}
-
-export interface CacheCompose {
- readonly kind: "compose";
- readonly policies: readonly CachePolicy[];
-}
-
-export type CachePolicy = CacheForever | CacheTTL | CacheOnChange | CacheCompose;
-
-export function forever(opts?: { envKeys?: string[] }): CacheForever {
- return { kind: "forever", envKeys: opts?.envKeys ?? [] };
-}
-
-export function ttl(
- durationSeconds: number,
- opts?: { envKeys?: string[] },
-): CacheTTL {
- return { kind: "ttl", durationSeconds, envKeys: opts?.envKeys ?? [] };
-}
-
-export function onChange(...paths: string[]): CacheOnChange {
- return { kind: "on_change", paths };
-}
-
-export function compose(...policies: CachePolicy[]): CacheCompose {
- return { kind: "compose", policies };
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/duration.ts b/crates/hm-dsl-engine/harmont-ts/src/duration.ts
deleted file mode 100644
index 37eb142e..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/duration.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-// src/duration.ts
-/**
- * Parse a human duration to a positive integer number of seconds.
- *
- * Accepts a Go-style string ("30s", "5m", "1h30m"; units h, m, s) or a
- * number of seconds. Used by `timeout()` and `pipeline({ timeout })`.
- */
-const DURATION_RE = /^(?:\d+[hms])+$/;
-const SEGMENT_RE = /(\d+)([hms])/g;
-const UNIT_SECONDS: Record = { h: 3600, m: 60, s: 1 };
-
-export function parseDuration(value: string | number): number {
- let seconds: number;
- if (typeof value === "number") {
- if (!Number.isInteger(value)) {
- throw new Error(
- `hm: timeout duration must be a whole number of seconds — got ${value}`,
- );
- }
- seconds = value;
- } else if (typeof value === "string") {
- seconds = parseStr(value);
- } else {
- throw new Error(
- `hm: timeout duration must be a string or number — got ${typeof value}`,
- );
- }
-
- if (seconds <= 0) {
- throw new Error(
- `hm: timeout duration must be positive — got ${JSON.stringify(value)}\n` +
- ` → use a value like "30s" or "5m"`,
- );
- }
- return seconds;
-}
-
-function parseStr(text: string): number {
- const stripped = text.trim();
- if (!DURATION_RE.test(stripped)) {
- throw new Error(
- `hm: invalid timeout duration ${JSON.stringify(text)}\n` +
- ` → use a Go-style duration like "30s", "5m", or "1h30m" (units: h, m, s)`,
- );
- }
- let total = 0;
- for (const m of stripped.matchAll(SEGMENT_RE)) {
- total += Number(m[1]) * UNIT_SECONDS[m[2]];
- }
- return total;
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/envelope.ts b/crates/hm-dsl-engine/harmont-ts/src/envelope.ts
deleted file mode 100644
index 7fc9f18b..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/envelope.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { resolvePipelineCacheKeys } from "./keygen.js";
-import type { PipelineIR } from "./pipeline.js";
-import type { Trigger } from "./triggers.js";
-
-export interface PipelineDefinition {
- readonly slug: string;
- readonly name?: string;
- readonly allowManual?: boolean;
- readonly triggers?: readonly Trigger[];
- readonly pipeline: PipelineIR;
-}
-
-export interface RenderOptions {
- readonly basePath?: string;
- readonly pipelineOrg?: string;
- readonly now?: number;
- readonly env?: Readonly>;
-}
-
-interface EnvelopeJSON {
- schema_version: string;
- pipelines: EnvelopePipelineJSON[];
-}
-
-interface EnvelopePipelineJSON {
- slug: string;
- name: string;
- allow_manual: boolean;
- triggers: Record[];
- definition: PipelineIR;
-}
-
-export function renderEnvelope(
- definitions: readonly PipelineDefinition[],
- opts?: RenderOptions,
-): string {
- const pipelineOrg =
- opts?.pipelineOrg ??
- (typeof process !== "undefined"
- ? process.env.HM_PIPELINE_ORG
- : undefined) ??
- "default";
- const now = opts?.now ?? Math.floor(Date.now() / 1000);
- const basePath = opts?.basePath;
- const env: Readonly> =
- opts?.env ??
- (typeof process !== "undefined"
- ? (process.env as Record)
- : {});
-
- const envelope: EnvelopeJSON = {
- schema_version: "1",
- pipelines: definitions.map((def) => {
- const entry: EnvelopePipelineJSON = {
- slug: def.slug,
- name: def.name ?? def.slug,
- allow_manual: def.allowManual ?? true,
- triggers: (def.triggers ?? []).map((t) => t.toJSON()),
- definition: def.pipeline,
- };
-
- const hasCachedSteps = entry.definition.graph.nodes.some(
- (node) => {
- const policy = node.step.cache as
- | { policy: string }
- | undefined;
- return policy != null && policy.policy !== "none";
- },
- );
-
- if (hasCachedSteps && basePath == null) {
- throw new Error(
- `Pipeline "${def.slug}" contains cached steps but no basePath was provided. ` +
- `Pass { basePath: process.cwd() } as the second argument to renderEnvelope().`,
- );
- }
-
- if (basePath != null) {
- resolvePipelineCacheKeys(entry.definition.graph, {
- pipelineOrg,
- pipelineSlug: def.slug,
- now,
- basePath,
- env,
- });
- }
-
- return entry;
- }),
- };
- return JSON.stringify(envelope);
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/index.ts b/crates/hm-dsl-engine/harmont-ts/src/index.ts
deleted file mode 100644
index c950c850..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/index.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-export { Step, scratch, sh, timeout, wait, type StepOptions } from "./step.js";
-export {
- type CachePolicy,
- type CacheForever,
- type CacheTTL,
- type CacheOnChange,
- type CacheCompose,
- forever,
- ttl,
- onChange,
- compose,
-} from "./cache.js";
-export {
- type Trigger,
- PushTrigger,
- PullRequestTrigger,
- push,
- pullRequest,
-} from "./triggers.js";
-export { pipeline, type PipelineIR, type PipelineOptions } from "./pipeline.js";
-export { target, clearTargetCache } from "./target.js";
-export { aptBase } from "./toolchains/shared.js";
-export {
- renderEnvelope,
- type PipelineDefinition,
- type RenderOptions,
-} from "./envelope.js";
-export {
- resolvePipelineCacheKeys,
- type CacheKeyOptions,
-} from "./keygen.js";
diff --git a/crates/hm-dsl-engine/harmont-ts/src/keygen.ts b/crates/hm-dsl-engine/harmont-ts/src/keygen.ts
deleted file mode 100644
index 2867b3c5..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/keygen.ts
+++ /dev/null
@@ -1,221 +0,0 @@
-import { createHash } from "node:crypto";
-import {
- readFileSync,
- readdirSync,
- statSync,
- existsSync,
-} from "node:fs";
-import { join, resolve as resolvePath, relative } from "node:path";
-import type { PipelineIR } from "./pipeline.js";
-
-const NUL = "\0";
-
-export interface CacheKeyOptions {
- readonly pipelineOrg: string;
- readonly pipelineSlug: string;
- readonly now: number;
- readonly basePath: string;
- readonly env: Readonly>;
-}
-
-export function resolvePipelineCacheKeys(
- graph: PipelineIR["graph"],
- opts: CacheKeyOptions,
-): void {
- const nodes = graph.nodes;
- const edges = graph.edges;
-
- const keyByIdx = new Map();
- for (let i = 0; i < nodes.length; i++) {
- keyByIdx.set(i, nodes[i].step.key as string);
- }
-
- const parentKeyMap = new Map();
- for (const [src, dst, kind] of edges) {
- if (kind === "builds_in") {
- parentKeyMap.set(keyByIdx.get(dst)!, keyByIdx.get(src)!);
- }
- }
-
- const resolved = new Map();
-
- for (const node of nodes) {
- const step = node.step;
- const cache = step.cache as Record | undefined;
- if (!cache || cache.policy === "none") continue;
-
- const cmd = (step.cmd as string) ?? "";
- const stepKey = step.key as string;
- const parentStepKey = parentKeyMap.get(stepKey);
- const parentResolved = lookupParent(parentStepKey, resolved);
- const policyRes = resolvePolicy(cache, cmd, opts);
-
- const key = sha256hex(
- opts.pipelineOrg +
- NUL +
- opts.pipelineSlug +
- NUL +
- stepKey +
- NUL +
- parentResolved +
- NUL +
- policyRes,
- );
-
- cache.key = key;
- resolved.set(stepKey, key);
- }
-}
-
-function lookupParent(
- parentStepKey: string | undefined,
- resolved: Map,
-): string {
- if (parentStepKey == null) return "scratch";
- const key = resolved.get(parentStepKey);
- if (key == null) {
- throw new Error(
- `step references builds_in "${parentStepKey}" which has no cached key (parent must be defined upstream and cached)`,
- );
- }
- return key;
-}
-
-function resolvePolicy(
- cache: Record,
- cmd: string,
- opts: CacheKeyOptions,
-): string {
- const policy = cache.policy as string;
-
- if (policy === "none") return "none";
-
- if (policy === "forever") {
- const envKeys = (cache.env_keys as string[]) ?? [];
- return "forever-" + sha256hex(cmd + NUL + envSubset(envKeys, opts.env));
- }
-
- if (policy === "ttl") {
- const duration = cache.duration_seconds as number;
- const bucket = Math.floor(opts.now / duration);
- const envKeys = (cache.env_keys as string[]) ?? [];
- return (
- "ttl-" +
- bucket +
- "-" +
- sha256hex(cmd + NUL + envSubset(envKeys, opts.env))
- );
- }
-
- if (policy === "on_change") {
- const paths = (cache.paths as string[]) ?? [];
- const resolvedPaths: string[] = [];
- for (const p of [...paths].sort()) {
- if (/[*?[]/.test(p)) {
- resolvedPaths.push(...globPaths(opts.basePath, p));
- } else {
- const full = resolvePath(opts.basePath, p);
- if (existsSync(full)) resolvedPaths.push(full);
- }
- }
- const pre = resolvedPaths.map((r) => pathHash(r) + NUL).join("");
- return "sha-" + sha256hex(pre);
- }
-
- if (policy === "compose") {
- const subs = cache.sub_policies as Record[];
- const parts = subs.map((sub) =>
- sub.policy !== "none" ? resolvePolicy(sub, cmd, opts) : "none",
- );
- return "compose-" + sha256hex(parts.join(""));
- }
-
- throw new Error(`resolve-policy-key: unknown policy "${policy}"`);
-}
-
-function envSubset(
- envKeys: readonly string[],
- env: Readonly>,
-): string {
- const sorted = [...envKeys].sort();
- return sorted.map((k) => k + "=" + (env[k] ?? "") + NUL).join("");
-}
-
-function pathHash(fullPath: string): string {
- const stat = statSync(fullPath);
- if (stat.isFile()) {
- return sha256hex(readFileSync(fullPath));
- }
- if (stat.isDirectory()) {
- const h = createHash("sha256");
- const files = walkDir(fullPath).sort();
- for (const child of files) {
- const rel = relative(fullPath, child).split("\\").join("/");
- h.update(rel, "utf8");
- h.update(NUL);
- h.update(readFileSync(child));
- h.update(NUL);
- }
- return h.digest("hex");
- }
- throw new Error(`on_change path does not exist: ${fullPath}`);
-}
-
-function walkDir(dir: string): string[] {
- const results: string[] = [];
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
- const full = join(dir, entry.name);
- if (entry.isDirectory()) {
- results.push(...walkDir(full));
- } else if (entry.isFile()) {
- results.push(full);
- }
- }
- return results;
-}
-
-function globPaths(basePath: string, pattern: string): string[] {
- const parts = pattern.split("/");
- let candidates = [basePath];
-
- for (const part of parts) {
- const next: string[] = [];
- for (const dir of candidates) {
- if (!existsSync(dir) || !statSync(dir).isDirectory()) continue;
- if (part === "**") {
- next.push(dir);
- next.push(...walkDir(dir).map((f) => join(f, "..")));
- } else if (/[*?[]/.test(part)) {
- const re = globPartToRegex(part);
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
- if (re.test(entry.name)) {
- next.push(join(dir, entry.name));
- }
- }
- } else {
- const full = join(dir, part);
- if (existsSync(full)) next.push(full);
- }
- }
- candidates = next;
- }
-
- return candidates
- .filter((p) => existsSync(p) && statSync(p).isFile())
- .sort();
-}
-
-function globPartToRegex(part: string): RegExp {
- let re = "^";
- for (const ch of part) {
- if (ch === "*") re += ".*";
- else if (ch === "?") re += ".";
- else re += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- }
- re += "$";
- return new RegExp(re);
-}
-
-function sha256hex(data: string | Buffer): string {
- return createHash("sha256").update(data).digest("hex");
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/keys.ts b/crates/hm-dsl-engine/harmont-ts/src/keys.ts
deleted file mode 100644
index 75408b3c..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/keys.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { createHash } from "node:crypto";
-import type { Step } from "./step.js";
-
-const EMOJI_SHORTCODE_RE = /:[a-z0-9_+-]+:/g;
-const NON_ALNUM_RE = /[^a-z0-9]+/g;
-
-export function slugifyLabel(label: string): string {
- let s = label.toLowerCase();
- s = s.replace(EMOJI_SHORTCODE_RE, " ");
- s = s.replace(NON_ALNUM_RE, "-");
- s = s.replace(/^-+|-+$/g, "");
- return s;
-}
-
-export function hashKey(parentKey: string, cmd: string, position: number): string {
- const h = createHash("sha256");
- h.update(parentKey, "utf8");
- h.update("\0");
- h.update(cmd, "utf8");
- h.update("\0");
- h.update(String(position), "utf8");
- return h.digest("hex").slice(0, 12);
-}
-
-export function resolveKeys(steps: readonly Step[]): Map {
- const overrides = new Map();
- const naturalSlugs = new Map();
-
- for (const s of steps) {
- if (s._keyOverride != null) {
- overrides.set(s._id, s._keyOverride);
- }
- if (s._label != null) {
- const slug = slugifyLabel(s._label);
- if (slug) {
- naturalSlugs.set(s._id, slug);
- }
- }
- }
-
- const reserved = new Set(overrides.values());
-
- const slugCounts = new Map();
- for (const slug of naturalSlugs.values()) {
- slugCounts.set(slug, (slugCounts.get(slug) ?? 0) + 1);
- }
-
- const labelSlugs = new Map();
- for (const [id, slug] of naturalSlugs) {
- if (!overrides.has(id)) {
- labelSlugs.set(id, slug);
- }
- }
-
- const keys = new Map();
- for (let position = 0; position < steps.length; position++) {
- const s = steps[position];
- const sid = s._id;
-
- if (overrides.has(sid)) {
- keys.set(sid, overrides.get(sid)!);
- continue;
- }
-
- const candidateSlug = labelSlugs.get(sid);
- if (
- candidateSlug != null &&
- !reserved.has(candidateSlug) &&
- slugCounts.get(candidateSlug) === 1
- ) {
- keys.set(sid, candidateSlug);
- reserved.add(candidateSlug);
- continue;
- }
-
- let parentKey = "";
- if (s._parent != null && keys.has(s._parent._id)) {
- parentKey = keys.get(s._parent._id)!;
- }
- keys.set(sid, hashKey(parentKey, s._cmd ?? "", position));
- }
-
- return keys;
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/pipeline.ts b/crates/hm-dsl-engine/harmont-ts/src/pipeline.ts
deleted file mode 100644
index bf745598..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/pipeline.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-import type { CachePolicy } from "./cache.js";
-import { parseDuration } from "./duration.js";
-import { resolveKeys } from "./keys.js";
-import type { Step } from "./step.js";
-
-// Across-the-board default image for imageless root steps. The SDK's
-// toolchains assume an apt-capable base (apt-get), so ubuntu:24.04 is the
-// universal default; child steps boot from their parent's snapshot.
-const DEFAULT_IMAGE = "ubuntu:24.04";
-
-export interface PipelineOptions {
- readonly env?: Readonly>;
- readonly timeout?: string | number;
-}
-
-export interface PipelineIR {
- version: string;
- timeout_seconds?: number;
- graph: {
- nodes: GraphNode[];
- node_holes: never[];
- edge_property: "directed";
- edges: [number, number, string][];
- };
-}
-
-interface GraphNode {
- step: Record;
- env: Record;
-}
-
-export function pipeline(
- leaves: Step[],
- opts?: PipelineOptions,
-): PipelineIR {
- if (!Array.isArray(leaves)) {
- throw new Error("pipeline() expects an array of steps as its first argument");
- }
-
- if (leaves.length === 0) {
- throw new Error(
- "pipeline must have at least one leaf — pass the terminal step(s) of each branch as the first argument",
- );
- }
-
- const ir: PipelineIR = { version: "0", graph: lowerToGraph(leaves, opts) };
- if (opts?.timeout != null) {
- ir.timeout_seconds = parseDuration(opts.timeout);
- }
- return ir;
-}
-
-function lowerToGraph(
- leaves: Step[],
- opts?: PipelineOptions,
-): PipelineIR["graph"] {
- const ordered = topoCollect(leaves);
- const commandSteps = ordered.filter((s) => s._cmd !== null && !s._isWait);
- const keys = resolveKeys(commandSteps);
-
- const idxById = new Map();
- for (let i = 0; i < commandSteps.length; i++) {
- idxById.set(commandSteps[i]._id, i);
- }
-
- const hasBuildsInParent = new Set();
- const nodes: GraphNode[] = [];
- const edges: [number, number, string][] = [];
-
- let preWaitIndices: number[] = [];
- let pendingDependsOn: number[] = [];
-
- for (const s of ordered) {
- if (s._isWait) {
- pendingDependsOn = [...preWaitIndices];
- preWaitIndices = [];
- continue;
- }
-
- if (s._cmd === null) continue;
-
- const nodeIdx = idxById.get(s._id)!;
- const stepKey = keys.get(s._id)!;
-
- const stepDict: Record = {
- key: stepKey,
- cmd: s._cmd,
- };
- if (s._label != null) stepDict.label = s._label;
- if (s._cache != null) stepDict.cache = cachePolicyToDict(s._cache);
- if (s._timeoutSeconds != null) stepDict.timeout_seconds = s._timeoutSeconds;
- if (s._image != null) stepDict.image = s._image;
- if (s._runner != null) stepDict.runner = s._runner;
- if (s._runnerArgs != null) stepDict.runner_args = s._runnerArgs;
-
- const mergedEnv: Record = {
- DEBIAN_FRONTEND: "noninteractive",
- TERM: "dumb",
- };
- if (opts?.env) Object.assign(mergedEnv, opts.env);
- if (s._env) Object.assign(mergedEnv, s._env);
-
- nodes.push({ step: stepDict, env: mergedEnv });
-
- const parentKey = resolvedParentKey(s, keys);
- if (parentKey !== null) {
- const parentIdx = findIdxByKey(parentKey, commandSteps, keys, idxById);
- edges.push([parentIdx, nodeIdx, "builds_in"]);
- hasBuildsInParent.add(nodeIdx);
- }
-
- for (const depIdx of pendingDependsOn) {
- edges.push([depIdx, nodeIdx, "depends_on"]);
- }
-
- preWaitIndices.push(nodeIdx);
- }
-
- for (let i = 0; i < nodes.length; i++) {
- if (!hasBuildsInParent.has(i) && !("image" in nodes[i].step)) {
- nodes[i].step.image = DEFAULT_IMAGE;
- }
- }
-
- return {
- nodes,
- node_holes: [],
- edge_property: "directed",
- edges,
- };
-}
-
-function topoCollect(leaves: Step[]): Step[] {
- const seen = new Set();
- const ordered: Step[] = [];
-
- for (const leaf of leaves) {
- if (leaf._isWait) {
- ordered.push(leaf);
- continue;
- }
- const chain: Step[] = [];
- let node: Step | null = leaf;
- while (node !== null) {
- if (seen.has(node._id)) break;
- chain.push(node);
- node = node._parent;
- }
- for (let i = chain.length - 1; i >= 0; i--) {
- const s = chain[i];
- if (seen.has(s._id)) continue;
- seen.add(s._id);
- ordered.push(s);
- }
- }
-
- return ordered;
-}
-
-function resolvedParentKey(
- s: Step,
- keys: Map,
-): string | null {
- let node = s._parent;
- while (node !== null) {
- if (node._cmd !== null && !node._isWait) {
- return keys.get(node._id) ?? null;
- }
- node = node._parent;
- }
- return null;
-}
-
-function findIdxByKey(
- key: string,
- commandSteps: Step[],
- keys: Map,
- idxById: Map,
-): number {
- for (const s of commandSteps) {
- if (keys.get(s._id) === key) {
- return idxById.get(s._id)!;
- }
- }
- throw new Error(`BUG: no step with key "${key}"`);
-}
-
-function cachePolicyToDict(policy: CachePolicy): Record {
- switch (policy.kind) {
- case "forever":
- return { policy: "forever", env_keys: [...policy.envKeys] };
- case "ttl":
- return {
- policy: "ttl",
- duration_seconds: policy.durationSeconds,
- env_keys: [...policy.envKeys],
- };
- case "on_change":
- return { policy: "on_change", paths: [...policy.paths] };
- case "compose":
- return {
- policy: "compose",
- sub_policies: policy.policies.map(cachePolicyToDict),
- };
- }
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/step.ts b/crates/hm-dsl-engine/harmont-ts/src/step.ts
deleted file mode 100644
index 5803c0cf..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/step.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import type { CachePolicy } from "./cache.js";
-import { parseDuration } from "./duration.js";
-
-export interface StepOptions {
- readonly label?: string;
- readonly cache?: CachePolicy;
- readonly env?: Readonly>;
- readonly image?: string;
- readonly runner?: string;
- readonly runnerArgs?: Readonly>;
- readonly key?: string;
- readonly cwd?: string;
-}
-
-let nextId = 0;
-
-export class Step {
- readonly _id: number;
- readonly _cmd: string | null;
- readonly _parent: Step | null;
- readonly _isWait: boolean;
- readonly _continueOnFailure: boolean;
- readonly _label: string | undefined;
- readonly _cache: CachePolicy | undefined;
- readonly _env: Readonly> | undefined;
- readonly _timeoutSeconds: number | undefined;
- readonly _image: string | undefined;
- readonly _runner: string | undefined;
- readonly _runnerArgs: Readonly> | undefined;
- readonly _keyOverride: string | undefined;
-
- /** @internal */
- constructor(init: {
- cmd: string | null;
- parent: Step | null;
- isWait?: boolean;
- continueOnFailure?: boolean;
- label?: string;
- cache?: CachePolicy;
- env?: Record;
- timeoutSeconds?: number;
- image?: string;
- runner?: string;
- runnerArgs?: Record;
- keyOverride?: string;
- }) {
- this._id = nextId++;
- this._cmd = init.cmd;
- this._parent = init.parent;
- this._isWait = init.isWait ?? false;
- this._continueOnFailure = init.continueOnFailure ?? false;
- this._label = init.label;
- this._cache = init.cache;
- this._env = init.env;
- this._timeoutSeconds = init.timeoutSeconds;
- this._image = init.image;
- this._runner = init.runner;
- this._runnerArgs = init.runnerArgs;
- this._keyOverride = init.keyOverride;
- }
-
- sh(cmd: string, opts?: StepOptions): Step {
- if (opts?.cwd === "") {
- throw new Error(
- 'hm: cwd must be a non-empty path\n → omit cwd to run in the workspace root, or pass cwd="some/dir"',
- );
- }
- const effectiveCmd = opts?.cwd != null ? `cd ${opts.cwd} && ${cmd}` : cmd;
- const effectiveImage =
- opts?.image != null
- ? opts.image
- : this._cmd === null
- ? this._image
- : undefined;
- return new Step({
- cmd: effectiveCmd,
- parent: this,
- label: opts?.label,
- cache: opts?.cache,
- env: opts?.env,
- image: effectiveImage,
- runner: opts?.runner,
- runnerArgs: opts?.runnerArgs,
- keyOverride: opts?.key,
- });
- }
-
- fork(opts?: { label?: string }): Step {
- return new Step({
- cmd: null,
- parent: this,
- label: opts?.label,
- });
- }
-
- /** @internal — returns a copy with the timeout set; preserves the chain. */
- withTimeoutSeconds(seconds: number): Step {
- return new Step({
- cmd: this._cmd,
- parent: this._parent,
- isWait: this._isWait,
- continueOnFailure: this._continueOnFailure,
- label: this._label,
- cache: this._cache,
- env: this._env as Record | undefined,
- timeoutSeconds: seconds,
- image: this._image,
- runner: this._runner,
- runnerArgs: this._runnerArgs as Record | undefined,
- keyOverride: this._keyOverride,
- });
- }
-}
-
-export function scratch(opts?: { image?: string }): Step {
- return new Step({ cmd: null, parent: null, image: opts?.image });
-}
-
-export function sh(cmd: string, opts?: StepOptions): Step {
- return scratch().sh(cmd, opts);
-}
-
-export function wait(opts?: { continueOnFailure?: boolean }): Step {
- return new Step({
- cmd: null,
- parent: null,
- isWait: true,
- continueOnFailure: opts?.continueOnFailure ?? false,
- });
-}
-
-/**
- * Apply a wall-clock timeout to a single step. The executor (and `hm run`
- * locally) kills the step once `duration` elapses; the step then fails as
- * timed out. Wrapping a step that already has a timeout replaces it.
- *
- * @param duration "30s" / "5m" / "1h30m" (units h, m, s) or a number of seconds.
- * @param step A command step (not a `wait` barrier).
- */
-export function timeout(duration: string | number, step: Step): Step {
- if (step._isWait) {
- throw new Error(
- 'hm: timeout() cannot wrap a wait() barrier\n' +
- ' → apply timeout() to a command step, e.g. timeout("30s", sh("make test"))',
- );
- }
- return step.withTimeoutSeconds(parseDuration(duration));
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/target.ts b/crates/hm-dsl-engine/harmont-ts/src/target.ts
deleted file mode 100644
index 0aa289d4..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/target.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-const cache = new Map();
-
-export function target(_name: string, fn: () => T): () => T {
- const key = Symbol(_name);
- return () => {
- if (!cache.has(key)) {
- cache.set(key, fn());
- }
- return cache.get(key) as T;
- };
-}
-
-export function clearTargetCache(): void {
- cache.clear();
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/toolchains/cargo.ts b/crates/hm-dsl-engine/harmont-ts/src/toolchains/cargo.ts
deleted file mode 100644
index 7fedf4e2..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/toolchains/cargo.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-// Shared cargo-argument assembly for the Rust toolchain helper.
-//
-// Mirrors harmont-py/harmont/_cargo.py exactly so both DSLs emit identical
-// cargo command strings. User-supplied *values* are shell-quoted; raw `flags`
-// pass through verbatim. `exclude` pairs with `--workspace` (cargo requires it).
-
-export interface CargoOpts {
- readonly workspace?: boolean;
- readonly packages?: readonly string[];
- readonly exclude?: readonly string[];
- readonly allFeatures?: boolean;
- readonly noDefaultFeatures?: boolean;
- readonly features?: readonly string[];
- readonly target?: string;
- readonly allTargets?: boolean;
- readonly release?: boolean;
- readonly profile?: string;
- readonly locked?: boolean;
- readonly flags?: readonly string[];
-}
-
-// POSIX single-quote escaping, byte-for-byte identical to Python's shlex.quote:
-// safe characters are left bare; everything else is wrapped in single quotes
-// with embedded single quotes rendered as '"'"'. The empty string becomes ''.
-const SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/;
-export function shQuote(s: string): string {
- if (s.length === 0) return "''";
- if (SAFE.test(s)) return s;
- return "'" + s.replace(/'/g, "'\"'\"'") + "'";
-}
-
-function validate(o: CargoOpts): void {
- if (o.allFeatures && ((o.features?.length ?? 0) > 0 || o.noDefaultFeatures)) {
- throw new Error(
- "rust: --all-features conflicts with features/noDefaultFeatures\n" +
- ` observed: allFeatures=true, features=${JSON.stringify(o.features ?? [])}, ` +
- `noDefaultFeatures=${o.noDefaultFeatures ?? false}\n` +
- " → pass allFeatures alone, or list explicit features without allFeatures",
- );
- }
- if (o.release && o.profile !== undefined) {
- throw new Error(
- "rust: release conflicts with profile\n" +
- ` observed: release=true, profile=${JSON.stringify(o.profile)}\n` +
- ' → use profile: "release" (identical effect) or drop one',
- );
- }
- if (o.exclude?.length) {
- if (o.packages?.length) {
- throw new Error(
- "rust: exclude cannot combine with packages\n" +
- ` observed: packages=${JSON.stringify(o.packages)}, exclude=${JSON.stringify(o.exclude)}\n` +
- " → --exclude pairs with --workspace; packages already selects explicitly, so drop one",
- );
- }
- if (!o.workspace) {
- throw new Error(
- "rust: exclude requires workspace\n" +
- ` observed: exclude=${JSON.stringify(o.exclude)} without workspace=true\n` +
- " → cargo --exclude only applies to --workspace; pass workspace: true",
- );
- }
- }
-}
-
-export function cargoFlags(o: CargoOpts): string {
- validate(o);
- const toks: string[] = [];
-
- if (o.packages?.length) {
- for (const p of o.packages) toks.push(`-p ${shQuote(p)}`);
- } else if (o.workspace) {
- toks.push("--workspace");
- for (const e of o.exclude ?? []) toks.push(`--exclude ${shQuote(e)}`);
- }
-
- if (o.allTargets) toks.push("--all-targets");
-
- if (o.allFeatures) {
- toks.push("--all-features");
- } else {
- if (o.noDefaultFeatures) toks.push("--no-default-features");
- if (o.features?.length) toks.push(`--features ${shQuote(o.features.join(","))}`);
- }
-
- if (o.target !== undefined) toks.push(`--target ${shQuote(o.target)}`);
-
- if (o.profile !== undefined) toks.push(`--profile ${shQuote(o.profile)}`);
- else if (o.release) toks.push("--release");
-
- if (o.locked ?? true) toks.push("--locked");
-
- if (o.flags?.length) toks.push(...o.flags);
-
- return toks.length ? " " + toks.join(" ") : "";
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/toolchains/cmake.ts b/crates/hm-dsl-engine/harmont-ts/src/toolchains/cmake.ts
deleted file mode 100644
index ca30e36f..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/toolchains/cmake.ts
+++ /dev/null
@@ -1,350 +0,0 @@
-import type { Step, StepOptions } from "../step.js";
-import { forever, onChange, type CachePolicy } from "../cache.js";
-import { makeInstallChain } from "./shared.js";
-
-const COMPILER_RE = /^(gcc|clang)(-\d+)?$/;
-
-type ActionOptions = Omit;
-
-export interface CMakeToolchainOptions {
- readonly compiler?: string;
- readonly generator?: "ninja" | "make";
- readonly ccache?: boolean;
- readonly image?: string;
- readonly base?: Step;
-}
-
-export interface CMakeProjectOptions {
- readonly path?: string;
- readonly preset?: string;
- readonly defines?: Record;
- readonly deps?: "vcpkg" | null;
- readonly target?: string;
- readonly cache?: CachePolicy;
-}
-
-export type CMakeOptions = CMakeToolchainOptions & CMakeProjectOptions;
-
-// ---------------------------------------------------------------------------
-// Helpers
-// ---------------------------------------------------------------------------
-
-function aptPackages(
- compiler: string | undefined,
- ccache: boolean,
- generator: string,
-): string[] {
- const pkgs: string[] = ["cmake", "build-essential", "pkg-config"];
- if (generator === "ninja") pkgs.push("ninja-build");
- if (ccache) pkgs.push("ccache");
- pkgs.push("clang-format");
- pkgs.push("clang-tidy");
- if (compiler != null) {
- const m = COMPILER_RE.exec(compiler);
- if (m == null) {
- throw new Error(
- `hm.cmake: invalid compiler "${compiler}"\n → use "gcc", "gcc-14", "clang", or "clang-18"`,
- );
- }
- const family = m[1];
- const suffix = m[2] ?? "";
- if (family === "gcc") {
- pkgs.push(`gcc${suffix}`, `g++${suffix}`);
- } else {
- pkgs.push(`clang${suffix}`, `lld${suffix}`);
- }
- }
- return pkgs;
-}
-
-function verifyCmd(
- compiler: string | undefined,
- ccache: boolean,
- generator: string,
-): string {
- const parts: string[] = ["cmake --version"];
- if (generator === "ninja") parts.push("ninja --version");
- if (ccache) parts.push("ccache --version");
- if (compiler != null) {
- const m = COMPILER_RE.exec(compiler);
- if (m) {
- const family = m[1];
- const suffix = m[2] ?? "";
- if (family === "gcc") {
- parts.push(`gcc${suffix} --version`);
- } else {
- parts.push(`clang${suffix} --version`);
- }
- }
- }
- return parts.join(" && ");
-}
-
-function configureCmd(opts: {
- path: string;
- preset: string | undefined;
- defines: Record | undefined;
- compiler: string | undefined;
- ccache: boolean;
- generator: string;
- buildDir?: string;
-}): string {
- if (opts.preset != null) {
- return `cd ${opts.path} && cmake --preset ${opts.preset}`;
- }
-
- const buildDir = opts.buildDir ?? "build";
- const genFlag = opts.generator === "ninja" ? "Ninja" : "Unix Makefiles";
- const parts: string[] = [
- `cd ${opts.path} && cmake -S . -B ${buildDir}`,
- `-G ${genFlag}`,
- "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
- ];
-
- if (opts.ccache) {
- parts.push("-DCMAKE_C_COMPILER_LAUNCHER=ccache");
- parts.push("-DCMAKE_CXX_COMPILER_LAUNCHER=ccache");
- }
-
- if (opts.compiler != null) {
- const m = COMPILER_RE.exec(opts.compiler);
- if (m) {
- const family = m[1];
- const suffix = m[2] ?? "";
- if (family === "gcc") {
- parts.push(`-DCMAKE_C_COMPILER=gcc${suffix}`);
- parts.push(`-DCMAKE_CXX_COMPILER=g++${suffix}`);
- } else {
- parts.push(`-DCMAKE_C_COMPILER=clang${suffix}`);
- parts.push(`-DCMAKE_CXX_COMPILER=clang++${suffix}`);
- }
- }
- }
-
- if (opts.defines) {
- for (const [k, v] of Object.entries(opts.defines)) {
- parts.push(`-D${k}=${v}`);
- }
- }
-
- return parts.join(" ");
-}
-
-function buildCmd(
- path: string,
- target: string | undefined,
- buildDir: string = "build",
- relative: boolean = false,
-): string {
- const prefix = relative ? buildDir : `${path}/${buildDir}`;
- let cmd = `cmake --build ${prefix} --parallel $(nproc)`;
- if (target != null) {
- cmd += ` --target ${target}`;
- }
- return cmd;
-}
-
-// ---------------------------------------------------------------------------
-// CMakeToolchain
-// ---------------------------------------------------------------------------
-
-export class CMakeToolchain {
- private readonly _installed: Step;
- readonly compiler: string | undefined;
- readonly ccache: boolean;
- readonly generator: string;
-
- constructor(
- installed: Step,
- compiler: string | undefined,
- ccache: boolean,
- generator: string,
- ) {
- this._installed = installed;
- this.compiler = compiler;
- this.ccache = ccache;
- this.generator = generator;
- }
-
- install(): Step {
- return this._installed;
- }
-
- /** Append a post-install command and return an advanced toolchain; chainable.
- * For prep steps the toolchain's actions must depend on but the SDK does not
- * model natively (codegen, fixtures, extra tooling). Project actions forked
- * off this toolchain see its results. (On built-based projects, splice prep
- * here, pre-configure: hm.cmake().setup("…").project({ path: "." }).)
- * @example hm.cmake().setup("conan install .").project({ path: "." }) */
- setup(cmd: string, opts?: StepOptions): CMakeToolchain {
- return new CMakeToolchain(this._installed.sh(cmd, opts), this.compiler, this.ccache, this.generator);
- }
-
- project(opts?: CMakeProjectOptions): CMakeProject {
- const path = opts?.path ?? ".";
- const preset = opts?.preset;
- const defines = opts?.defines;
- const deps = opts?.deps;
- const target = opts?.target;
- const cache = opts?.cache;
-
- const configure = configureCmd({
- path,
- preset,
- defines,
- compiler: this.compiler,
- ccache: this.ccache,
- generator: this.generator,
- });
- const warmupCmd = `${configure} && ${buildCmd(path, target, "build", true)}`;
-
- // Determine warmup cache policy
- let warmupCache: CachePolicy;
- if (cache != null) {
- warmupCache = cache;
- } else if (deps === "vcpkg") {
- warmupCache = onChange("vcpkg.json");
- } else {
- const cmakelists =
- path !== "." ? `${path}/CMakeLists.txt` : "CMakeLists.txt";
- warmupCache = onChange(cmakelists);
- }
-
- // Determine the parent for the warmup step
- let warmupParent: Step;
- if (deps === "vcpkg") {
- const vcpkgCmd = [
- "git clone https://github.com/microsoft/vcpkg.git /opt/vcpkg",
- "/opt/vcpkg/bootstrap-vcpkg.sh",
- `cd ${path} && /opt/vcpkg/vcpkg install`,
- ].join(" && ");
- warmupParent = this._installed.sh(vcpkgCmd, {
- label: ":cmake: vcpkg",
- cache: onChange("vcpkg.json"),
- });
- } else {
- warmupParent = this._installed;
- }
-
- const built = warmupParent.sh(warmupCmd, {
- label: ":cmake: build",
- cache: warmupCache,
- });
-
- return new CMakeProject(this, built, path);
- }
-}
-
-// ---------------------------------------------------------------------------
-// CMakeProject
-// ---------------------------------------------------------------------------
-
-export class CMakeProject {
- readonly toolchain: CMakeToolchain;
- private readonly _built: Step;
- readonly path: string;
-
- constructor(toolchain: CMakeToolchain, built: Step, path: string) {
- this.toolchain = toolchain;
- this._built = built;
- this.path = path;
- }
-
- build(): Step {
- return this._built;
- }
-
- test(opts?: ActionOptions & { parallel?: boolean }): Step {
- const parallel = opts?.parallel ?? true;
- const { parallel: _, ...rest } = opts ?? {};
- const parallelFlag = parallel ? " --parallel $(nproc)" : "";
- const cmd = [
- buildCmd(this.path, undefined),
- `ctest --test-dir ${this.path}/build --output-on-failure${parallelFlag}`,
- ].join(" && ");
- return this._built.sh(cmd, { label: ":cmake: test", ...rest });
- }
-
- install(opts?: ActionOptions & { prefix?: string }): Step {
- const prefixFlag = opts?.prefix ? ` --prefix ${opts.prefix}` : "";
- const { prefix: _, ...rest } = opts ?? {};
- const cmd = `cmake --install ${this.path}/build${prefixFlag}`;
- return this._built.sh(cmd, { label: ":cmake: install", ...rest });
- }
-
- fmt(opts?: ActionOptions & { fix?: boolean }): Step {
- const mode = opts?.fix ? "-i" : "--dry-run --Werror";
- const { fix: _, ...rest } = opts ?? {};
- const cmd = [
- `cd ${this.path} && find . -not -path './build/*'`,
- `\\( -name '*.c' -o -name '*.h'`,
- `-o -name '*.cpp' -o -name '*.hpp' -o -name '*.cc' -o -name '*.cxx' \\) |`,
- `xargs clang-format ${mode}`,
- ].join(" ");
- return this.toolchain.install().sh(cmd, { label: ":cmake: fmt", ...rest });
- }
-
- lint(opts?: ActionOptions): Step {
- const cmd = `cd ${this.path} && run-clang-tidy -p build`;
- return this._built.sh(cmd, { label: ":cmake: lint", ...opts });
- }
-
- package(opts?: ActionOptions & { generator?: string }): Step {
- const { generator, ...rest } = opts ?? {};
- const genFlag = generator ? ` -G ${generator}` : "";
- const cmd = `cd ${this.path}/build && cpack${genFlag}`;
- return this._built.sh(cmd, { label: ":cmake: package", ...rest });
- }
-}
-
-// ---------------------------------------------------------------------------
-// Factory function (overloaded)
-// ---------------------------------------------------------------------------
-
-export function cmake(opts: CMakeOptions & { path: string }): CMakeProject;
-export function cmake(opts?: CMakeToolchainOptions): CMakeToolchain;
-export function cmake(opts?: CMakeOptions): CMakeToolchain | CMakeProject {
- const compiler = opts?.compiler;
- const generator = opts?.generator ?? "ninja";
- const ccache = opts?.ccache ?? true;
-
- if (generator !== "ninja" && generator !== "make") {
- throw new Error(
- `hm.cmake: invalid generator "${generator}"\n → use "ninja" or "make"`,
- );
- }
-
- if (compiler != null && !COMPILER_RE.test(compiler)) {
- throw new Error(
- `hm.cmake: invalid compiler "${compiler}"\n → use "gcc", "gcc-14", "clang", or "clang-18"`,
- );
- }
-
- const pkgs = aptPackages(compiler, ccache, generator);
- const verify = verifyCmd(compiler, ccache, generator);
-
- const installed = makeInstallChain({
- aptPackages: pkgs,
- installCmd: verify,
- installCache: forever(),
- langTag: "cmake",
- installTag: "verify",
- image: opts?.image,
- base: opts?.base,
- });
-
- const toolchain = new CMakeToolchain(installed, compiler, ccache, generator);
-
- if (opts?.path != null) {
- return toolchain.project({
- path: opts.path,
- preset: (opts as CMakeProjectOptions).preset,
- defines: (opts as CMakeProjectOptions).defines,
- deps: (opts as CMakeProjectOptions).deps,
- target: (opts as CMakeProjectOptions).target,
- cache: (opts as CMakeProjectOptions).cache,
- });
- }
-
- return toolchain;
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/toolchains/detect.ts b/crates/hm-dsl-engine/harmont-ts/src/toolchains/detect.ts
deleted file mode 100644
index d037b926..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/toolchains/detect.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { readFileSync, readdirSync } from "node:fs";
-import { join } from "node:path";
-
-type Runtime = "node" | "bun" | "deno";
-type Pm = "npm" | "pnpm" | "yarn-classic" | "yarn-berry" | "bun";
-
-export interface DetectedToolchain {
- runtime?: Runtime;
- pm?: Pm;
- pmVersion?: string;
-}
-
-export function detectFromPackageJson(
- packageJson: Record,
-): DetectedToolchain {
- const result: DetectedToolchain = {};
-
- const engines = packageJson.engines;
- if (engines != null && typeof engines === "object") {
- const eng = engines as Record;
- if ("bun" in eng) {
- result.runtime = "bun";
- result.pm = "bun";
- } else if ("deno" in eng) {
- result.runtime = "deno";
- } else if ("node" in eng) {
- result.runtime = "node";
- }
- }
-
- if (result.pm == null) {
- const pmField = packageJson.packageManager;
- if (typeof pmField === "string") {
- const name = pmField.split("@")[0];
- const version = pmField.split("@")[1];
- if (name === "pnpm") result.pm = "pnpm";
- else if (name === "bun") result.pm = "bun";
- else if (name === "npm") result.pm = "npm";
- else if (name === "yarn") {
- result.pm = version && parseInt(version, 10) >= 2 ? "yarn-berry" : "yarn-classic";
- }
- if (version) result.pmVersion = version;
- }
- }
-
- return result;
-}
-
-export function detectFromLockfiles(
- files: readonly string[],
-): DetectedToolchain {
- const set = new Set(files);
-
- if (set.has("bun.lock") || set.has("bun.lockb")) {
- return { pm: "bun", runtime: "bun" };
- }
- if (set.has("pnpm-lock.yaml")) {
- return { pm: "pnpm" };
- }
- if (set.has("deno.lock")) {
- return { runtime: "deno" };
- }
- if (set.has("package-lock.json")) {
- return { pm: "npm" };
- }
- if (set.has("yarn.lock")) {
- return { pm: "yarn-classic" };
- }
-
- return {};
-}
-
-export function detect(path: string): DetectedToolchain {
- let fromPkg: DetectedToolchain = {};
- try {
- const raw = readFileSync(join(path, "package.json"), "utf8");
- fromPkg = detectFromPackageJson(JSON.parse(raw));
- } catch {
- // no package.json or invalid JSON
- }
-
- let fromLock: DetectedToolchain = {};
- try {
- fromLock = detectFromLockfiles(readdirSync(path));
- } catch {
- // directory unreadable
- }
-
- const result: DetectedToolchain = {};
- const runtime = fromPkg.runtime ?? fromLock.runtime;
- const pm = fromPkg.pm ?? fromLock.pm;
- if (runtime != null) result.runtime = runtime;
- if (pm != null) result.pm = pm;
- const pmVersion = fromPkg.pmVersion;
- if (pmVersion != null) result.pmVersion = pmVersion;
- return result;
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/toolchains/elixir.ts b/crates/hm-dsl-engine/harmont-ts/src/toolchains/elixir.ts
deleted file mode 100644
index 38f46527..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/toolchains/elixir.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-import type { Step, StepOptions } from "../step.js";
-import { forever, onChange } from "../cache.js";
-import { makeInstallChain } from "./shared.js";
-
-const APT_PACKAGES = [
- "curl",
- "ca-certificates",
- "git",
- "unzip",
- "build-essential",
- "autoconf",
- "libncurses-dev",
- "libssl-dev",
-] as const;
-
-const ELIXIR_ENV = { ELIXIR_ERL_OPTIONS: "+fnu" } as const;
-
-const ELIXIR_VERSION_RE = /^[0-9]+\.[0-9]+\.[0-9]+$/;
-const OTP_VERSION_RE = /^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$/;
-
-export interface ElixirOptions {
- readonly path?: string;
- readonly elixirVersion?: string;
- readonly otpVersion?: string;
- readonly image?: string;
- readonly base?: Step;
-}
-
-type ActionOptions = Omit;
-
-export class ElixirProject {
- readonly path: string;
- private readonly _installed: Step;
- private _plt: Step | null = null;
-
- constructor(path: string, installed: Step) {
- this.path = path;
- this._installed = installed;
- }
-
- install(): Step {
- return this._installed;
- }
-
- /** Append a post-install command and return an advanced project; chainable.
- * For prep steps the toolchain's actions must depend on but the SDK does not
- * model natively (codegen, fixtures, extra tooling). Action methods on the
- * returned object fork from this step.
- * @example hm.elixir({ path: "elixir" }).setup("mix proto.gen").compile() */
- setup(cmd: string, opts?: StepOptions): ElixirProject {
- return new ElixirProject(this.path, this._installed.sh(cmd, opts));
- }
-
- private _sh(parent: Step, cmd: string, opts?: ActionOptions): Step {
- const { env: userEnv, ...rest } = opts ?? {};
- return parent.sh(cmd, { env: { ...ELIXIR_ENV, ...userEnv }, ...rest });
- }
-
- compile(opts?: ActionOptions): Step {
- return this._sh(this._installed,
- `cd ${this.path} && mix compile --warnings-as-errors`,
- { label: ":ex: compile", ...opts },
- );
- }
-
- test(opts?: ActionOptions & { cover?: boolean; partitions?: number }): Step {
- const flags: string[] = [];
- if (opts?.cover) flags.push("--cover");
- if (opts?.partitions != null) flags.push(`--partitions ${opts.partitions}`);
- const { cover: _, partitions: __, ...rest } = opts ?? {};
- const flagStr = flags.length > 0 ? ` ${flags.join(" ")}` : "";
- return this._sh(this._installed, `cd ${this.path} && mix test${flagStr}`, {
- label: ":ex: test",
- ...rest,
- });
- }
-
- format(opts?: ActionOptions): Step {
- return this._sh(this._installed,
- `cd ${this.path} && mix format --check-formatted`,
- { label: ":ex: format", ...opts },
- );
- }
-
- credo(opts?: ActionOptions & { strict?: boolean }): Step {
- const strict = opts?.strict !== false ? " --strict" : "";
- const { strict: _, ...rest } = opts ?? {};
- return this._sh(this._installed, `cd ${this.path} && mix credo${strict}`, {
- label: ":ex: credo",
- ...rest,
- });
- }
-
- plt(): Step {
- if (this._plt == null) {
- this._plt = this._sh(this._installed, `cd ${this.path} && mix dialyzer --plt`, {
- label: ":ex: plt",
- cache: onChange(`${this.path}/mix.lock`),
- });
- }
- return this._plt;
- }
-
- dialyzer(opts?: ActionOptions): Step {
- return this._sh(this.plt(), `cd ${this.path} && mix dialyzer`, {
- label: ":ex: dialyzer",
- ...opts,
- });
- }
-
- sobelow(opts?: ActionOptions): Step {
- return this._sh(this._installed, `cd ${this.path} && mix sobelow --exit`, {
- label: ":ex: sobelow",
- ...opts,
- });
- }
-
- depsAudit(opts?: ActionOptions): Step {
- return this._sh(this._installed, `cd ${this.path} && mix deps.audit`, {
- label: ":ex: deps-audit",
- ...opts,
- });
- }
-
- hexAudit(opts?: ActionOptions): Step {
- return this._sh(this._installed, `cd ${this.path} && mix hex.audit`, {
- label: ":ex: hex-audit",
- ...opts,
- });
- }
-
- mix(task: string, opts?: ActionOptions): Step {
- return this._sh(this._installed, `cd ${this.path} && mix ${task}`, {
- label: `:ex: ${task}`,
- ...opts,
- });
- }
-
- release(opts?: ActionOptions & { mixEnv?: string }): Step {
- const env = opts?.mixEnv ?? "prod";
- const { mixEnv: _, ...rest } = opts ?? {};
- return this._sh(this._installed,
- `cd ${this.path} && MIX_ENV=${env} mix release`,
- { label: ":ex: release", ...rest },
- );
- }
-
-}
-
-export function elixir(opts?: ElixirOptions): ElixirProject {
- const path = opts?.path ?? ".";
- const elixirVersion = opts?.elixirVersion ?? "1.18.3";
- const otpVersion = opts?.otpVersion ?? "27.3.3";
-
- if (!ELIXIR_VERSION_RE.test(elixirVersion)) {
- throw new Error(
- `hm.elixir: invalid elixir version "${elixirVersion}"\n → use a semver like "1.18.3"`,
- );
- }
-
- if (!OTP_VERSION_RE.test(otpVersion)) {
- throw new Error(
- `hm.elixir: invalid otp version "${otpVersion}"\n → use a semver like "27" or "27.3.3"`,
- );
- }
-
- const otpMajor = otpVersion.split(".")[0];
-
- const erlangInstallCmd = [
- `curl -fsSL https://binaries2.erlang-solutions.com/debian/pool/contrib/e/esl-erlang/esl-erlang_${otpVersion}-1~debian~bookworm_amd64.deb -o /tmp/erlang.deb`,
- "(dpkg -i /tmp/erlang.deb || apt-get install -fy)",
- "erl -eval 'erlang:display(erlang:system_info(otp_release)), halt().' -noshell",
- ].join(" && ");
-
- const erlangInstalled = makeInstallChain({
- aptPackages: [...APT_PACKAGES],
- installCmd: erlangInstallCmd,
- installCache: forever(),
- langTag: "ex",
- installTag: "erlang-install",
- image: opts?.image,
- base: opts?.base,
- });
-
- const elixirInstalled = erlangInstalled.sh(
- [
- `curl -fsSL https://github.com/elixir-lang/elixir/releases/download/v${elixirVersion}/elixir-otp-${otpMajor}.zip -o /tmp/elixir.zip`,
- "unzip -q /tmp/elixir.zip -d /usr/local/elixir",
- "ln -sf /usr/local/elixir/bin/elixir /usr/local/bin/elixir",
- "ln -sf /usr/local/elixir/bin/mix /usr/local/bin/mix",
- "ln -sf /usr/local/elixir/bin/iex /usr/local/bin/iex",
- "mix local.hex --force",
- "mix local.rebar --force",
- "elixir --version",
- ].join(" && "),
- { label: ":ex: elixir-install", cache: forever(), env: ELIXIR_ENV },
- );
-
- const depsInstalled = elixirInstalled.sh(
- `cd ${path} && mix deps.get && mix deps.compile`,
- {
- label: ":ex: mix-deps",
- cache: onChange(`${path}/mix.lock`),
- env: ELIXIR_ENV,
- },
- );
-
- return new ElixirProject(path, depsInstalled);
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/toolchains/go.ts b/crates/hm-dsl-engine/harmont-ts/src/toolchains/go.ts
deleted file mode 100644
index d925ddad..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/toolchains/go.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import type { Step, StepOptions } from "../step.js";
-import { forever } from "../cache.js";
-import { makeInstallChain } from "./shared.js";
-
-const APT_PACKAGES = ["curl", "ca-certificates", "git"] as const;
-const VERSION_RE = /^[0-9]+\.[0-9]+(\.[0-9]+)?$/;
-
-export interface GoOptions {
- readonly path?: string;
- readonly version?: string;
- readonly image?: string;
- readonly base?: Step;
-}
-
-type ActionOptions = Omit;
-
-export class GoToolchain {
- readonly path: string;
- private readonly _installed: Step;
-
- constructor(path: string, installed: Step) {
- this.path = path;
- this._installed = installed;
- }
-
- install(): Step {
- return this._installed;
- }
-
- /** Append a post-install command and return an advanced toolchain; chainable.
- * For prep steps the toolchain's actions must depend on but the SDK does not
- * model natively (codegen, fixtures, extra tooling). Action methods on the
- * returned object fork from this step.
- * @example hm.go({ path: "." }).setup("go generate ./...").build() */
- setup(cmd: string, opts?: StepOptions): GoToolchain {
- return new GoToolchain(this.path, this._installed.sh(cmd, opts));
- }
-
- build(opts?: ActionOptions): Step {
- return this._installed.sh(`cd ${this.path} && go build ./...`, {
- label: ":go: build",
- ...opts,
- });
- }
-
- test(opts?: ActionOptions): Step {
- return this._installed.sh(`cd ${this.path} && go test ./...`, {
- label: ":go: test",
- ...opts,
- });
- }
-
- vet(opts?: ActionOptions): Step {
- return this._installed.sh(`cd ${this.path} && go vet ./...`, {
- label: ":go: vet",
- ...opts,
- });
- }
-
- fmt(opts?: ActionOptions): Step {
- return this._installed.sh(
- `cd ${this.path} && test -z "$(gofmt -l .)"`,
- { label: ":go: fmt", ...opts },
- );
- }
-}
-
-export function go(opts?: GoOptions): GoToolchain {
- const path = opts?.path ?? ".";
- const version = opts?.version ?? "1.23.2";
-
- if (!VERSION_RE.test(version)) {
- throw new Error(
- `hm.go: invalid version "${version}"\n → use a semver like "1.23" or "1.23.2"`,
- );
- }
-
- const installCmd = [
- `curl -fsSL https://go.dev/dl/go${version}.linux-amd64.tar.gz -o /tmp/go.tgz`,
- "rm -rf /usr/local/go && tar -C /usr/local -xzf /tmp/go.tgz",
- "ln -sf /usr/local/go/bin/go /usr/local/bin/go",
- "ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt",
- "go version",
- ].join(" && ");
-
- const installed = makeInstallChain({
- aptPackages: [...APT_PACKAGES],
- installCmd,
- installCache: forever(),
- langTag: "go",
- installTag: "install",
- image: opts?.image,
- base: opts?.base,
- });
-
- return new GoToolchain(path, installed);
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/toolchains/index.ts b/crates/hm-dsl-engine/harmont-ts/src/toolchains/index.ts
deleted file mode 100644
index c65a8d49..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/toolchains/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-export { js, ts, JsProject, type JsOptions } from "./js.js";
-export { go, GoToolchain, type GoOptions } from "./go.js";
-export { rust, RustToolchain, RustProject, type RustToolchainOptions, type RustProjectOptions, type FeaturePowersetOptions } from "./rust.js";
-export { python, PythonToolchain, type PythonOptions } from "./python.js";
-export {
- cmake,
- CMakeToolchain,
- CMakeProject,
- type CMakeToolchainOptions,
- type CMakeProjectOptions,
- type CMakeOptions,
-} from "./cmake.js";
-export {
- zig,
- ZigToolchain,
- ZigProject,
- type ZigOptions,
-} from "./zig.js";
-export * as py from "./py/index.js";
-export { elixir, ElixirProject, type ElixirOptions } from "./elixir.js";
diff --git a/crates/hm-dsl-engine/harmont-ts/src/toolchains/js.ts b/crates/hm-dsl-engine/harmont-ts/src/toolchains/js.ts
deleted file mode 100644
index db196327..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/toolchains/js.ts
+++ /dev/null
@@ -1,228 +0,0 @@
-import type { Step, StepOptions } from "../step.js";
-import { forever, onChange } from "../cache.js";
-import {
- makeInstallChain,
- nodeInstallCmd,
- bunInstallCmd,
- denoInstallCmd,
-} from "./shared.js";
-import { detect } from "./detect.js";
-
-// Runtimes execute JS/TS; package managers install dependencies. `deno` is a
-// runtime only — its dependency management is intrinsic, so it is not a `pm`
-// value (see makeProject). yarn's classic/berry split is two pm values rather
-// than a version axis because the lockfile install flag differs between them.
-type Runtime = "node" | "bun" | "deno";
-type PackageManager = "npm" | "pnpm" | "yarn-classic" | "yarn-berry" | "bun" | "deno";
-type ActionOptions = Omit;
-
-export interface JsOptions {
- readonly path?: string;
- readonly pm?: PackageManager;
- readonly runtime?: Runtime;
- /** Runtime version — Node major ("22"/"22.x") or Bun/Deno semver ("1.2.3").
- * PM versions are pinned by the project's `packageManager` field. */
- readonly version?: string;
- readonly image?: string;
- readonly base?: Step;
-}
-
-const NODE_VERSION_RE = /^[0-9]+(\.x)?$/;
-const SEMVER_RE = /^[0-9]+\.[0-9]+(\.[0-9]+)?$/;
-
-const LOCKFILES: Record = {
- npm: "package-lock.json",
- pnpm: "pnpm-lock.yaml",
- "yarn-classic": "yarn.lock",
- "yarn-berry": "yarn.lock",
- bun: "bun.lock",
- deno: "deno.lock",
-};
-
-const DEPS_CMD: Record = {
- npm: "npm ci",
- pnpm: "pnpm install --frozen-lockfile",
- "yarn-classic": "yarn install --frozen-lockfile",
- "yarn-berry": "yarn install --immutable",
- bun: "bun install --frozen-lockfile",
- deno: "deno install",
-};
-
-const RUN_PREFIX: Record = {
- npm: "npm run",
- pnpm: "pnpm run",
- "yarn-classic": "yarn run",
- "yarn-berry": "yarn run",
- bun: "bun run",
- deno: "deno task",
-};
-
-/** Command to bring `pm` onto the runtime image, or null when the PM already
- * ships with the runtime (npm with node, bun with the bun runtime). */
-function pmBootstrap(pm: PackageManager, runtime: Runtime, version?: string): string | null {
- switch (pm) {
- case "npm":
- return null; // bundled with node
- case "bun":
- return runtime === "bun" ? null : bunInstallCmd();
- case "deno":
- return null; // bundled with deno
- case "pnpm":
- return version != null
- ? `corepack enable pnpm && corepack install -g pnpm@${version}`
- : "corepack enable pnpm";
- case "yarn-classic":
- case "yarn-berry":
- return version != null
- ? `corepack enable yarn && corepack install -g yarn@${version}`
- : "corepack enable";
- }
-}
-
-export class JsProject {
- readonly path: string;
- private readonly _installed: Step;
- private readonly _runPrefix: string;
- private readonly _tag: string;
- private readonly _pm: PackageManager;
-
- constructor(path: string, installed: Step, pm: PackageManager, tag: string) {
- this.path = path;
- this._installed = installed;
- this._runPrefix = RUN_PREFIX[pm];
- this._tag = tag;
- this._pm = pm;
- }
-
- /** The dependency-install step (`npm ci`, `bun install`, `deno install`, …).
- * Every action attaches to it so installation is shared across CI jobs. */
- install(): Step {
- return this._installed;
- }
-
- /** Append a post-install command and return an advanced project; chainable.
- * For prep steps the toolchain's actions must depend on but the SDK does not
- * model natively (codegen, fixtures, extra tooling). Action methods on the
- * returned object fork from this step.
- * @example hm.js.project({ path: "web" }).setup("npm run codegen").run("build") */
- setup(cmd: string, opts?: StepOptions): JsProject {
- return new JsProject(this.path, this._installed.sh(cmd, opts), this._pm, this._tag);
- }
-
- /** Run a package.json script / deno.json task by name.
- * This is the uniform action across all PMs — for native tooling
- * (`deno test`, `bun test`) define a script or drop to `.sh()`. */
- run(script: string, opts?: ActionOptions): Step {
- return this._installed.sh(
- `cd ${this.path} && ${this._runPrefix} ${script}`,
- {
- label: `:${this._tag}: ${script}`,
- ...opts,
- },
- );
- }
-}
-
-function validateVersion(runtime: Runtime, version: string): void {
- if (runtime === "node") {
- if (!NODE_VERSION_RE.test(version)) {
- throw new Error(
- `js.project: invalid version "${version}"\n → use a Node major version like "22" or "22.x"`,
- );
- }
- } else if (!SEMVER_RE.test(version)) {
- throw new Error(
- `js.project: invalid version "${version}"\n → use a semver version like "1.2" or "1.2.0"`,
- );
- }
-}
-
-function makeProject(opts?: JsOptions): JsProject {
- const path = opts?.path ?? ".";
- const detected =
- opts?.runtime == null && opts?.pm == null ? detect(path) : {};
- const runtime = opts?.runtime ?? detected.runtime ?? "node";
-
- if (opts?.version != null) {
- validateVersion(runtime, opts.version);
- }
-
- // --- Deno: built-in PM, no pm option ---
- if (runtime === "deno") {
- if (opts?.pm != null) {
- throw new Error(
- `js.project: runtime="deno" manages its own dependencies — do not set pm`,
- );
- }
- const runtimeInstalled = makeInstallChain({
- aptPackages: ["curl", "ca-certificates", "unzip"],
- installCmd: denoInstallCmd(opts?.version),
- installCache: forever(),
- langTag: "deno",
- installTag: "install",
- image: opts?.image,
- base: opts?.base,
- });
- const depsInstalled = runtimeInstalled.sh(`cd ${path} && ${DEPS_CMD.deno}`, {
- label: ":deno: deps",
- cache: onChange(`${path}/${LOCKFILES.deno}`),
- });
- return new JsProject(path, depsInstalled, "deno", "deno");
- }
-
- // --- Node / Bun runtime ---
- const pm: PackageManager = opts?.pm ?? detected.pm ?? (runtime === "bun" ? "bun" : "npm");
- const pmVersion = detected.pmVersion;
-
- if (pm === "deno") {
- throw new Error(
- 'js.project: pm="deno" is not valid — use runtime="deno" instead',
- );
- }
-
- if (runtime === "bun" && pm !== "bun") {
- throw new Error(`js.project: runtime="bun" only supports pm="bun"`);
- }
-
- const aptPkgs: string[] = ["curl", "ca-certificates"];
- if (runtime === "bun" || pm === "bun") {
- aptPkgs.push("unzip"); // bun's installer needs unzip
- }
-
- const langTag = runtime === "bun" ? "bun" : "node";
- const runtimeCmd =
- runtime === "bun"
- ? bunInstallCmd(opts?.version)
- : nodeInstallCmd(opts?.version ?? "22");
-
- const runtimeInstalled = makeInstallChain({
- aptPackages: aptPkgs,
- installCmd: runtimeCmd,
- installCache: forever(),
- langTag,
- installTag: "install",
- image: opts?.image,
- base: opts?.base,
- });
-
- // Layer the package manager onto the runtime image when it isn't bundled.
- const bootstrap = pmBootstrap(pm, runtime, pmVersion);
- const bootstrapCache = pmVersion != null ? onChange(`${path}/package.json`) : forever();
- const pmReady =
- bootstrap == null
- ? runtimeInstalled
- : runtimeInstalled.sh(bootstrap, {
- label: `:${langTag}: ${pm}`,
- cache: bootstrapCache,
- });
-
- const depsInstalled = pmReady.sh(`cd ${path} && ${DEPS_CMD[pm]}`, {
- label: `:${langTag}: deps`,
- cache: onChange(`${path}/${LOCKFILES[pm]}`),
- });
-
- return new JsProject(path, depsInstalled, pm, langTag);
-}
-
-export const js = { project: makeProject };
-export const ts = js;
diff --git a/crates/hm-dsl-engine/harmont-ts/src/toolchains/py/index.ts b/crates/hm-dsl-engine/harmont-ts/src/toolchains/py/index.ts
deleted file mode 100644
index ee526dc5..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/toolchains/py/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { uv, UvProject, type UvOptions } from "./uv.js";
diff --git a/crates/hm-dsl-engine/harmont-ts/src/toolchains/py/uv.ts b/crates/hm-dsl-engine/harmont-ts/src/toolchains/py/uv.ts
deleted file mode 100644
index 289dc242..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/toolchains/py/uv.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import type { Step, StepOptions } from "../../step.js";
-import { forever, onChange } from "../../cache.js";
-import { makeInstallChain } from "../shared.js";
-
-const APT_PACKAGES = [
- "curl",
- "ca-certificates",
- "python3",
- "python3-venv",
-] as const;
-const VERSION_RE = /^([0-9]+\.[0-9]+\.[0-9]+|latest)$/;
-
-function resolvePaths(paths?: string | string[]): string {
- if (paths == null) return ".";
- return Array.isArray(paths) ? paths.join(" ") : paths;
-}
-
-export interface UvOptions {
- readonly path?: string;
- readonly version?: string;
- readonly image?: string;
- readonly base?: Step;
-}
-
-type ActionOptions = Omit;
-
-export class UvProject {
- readonly path: string;
- private readonly _installed: Step;
-
- constructor(path: string, installed: Step) {
- this.path = path;
- this._installed = installed;
- }
-
- install(): Step {
- return this._installed;
- }
-
- test(opts?: ActionOptions): Step {
- return this._installed.sh(`cd ${this.path} && uv run pytest`, {
- label: ":python: test",
- ...opts,
- });
- }
-
- lint(opts?: ActionOptions): Step {
- return this._installed.sh(`cd ${this.path} && uv run ruff check .`, {
- label: ":python: lint",
- ...opts,
- });
- }
-
- fmt(opts?: ActionOptions): Step {
- return this._installed.sh(
- `cd ${this.path} && uv run ruff format --check .`,
- { label: ":python: fmt", ...opts },
- );
- }
-
- typecheck(opts?: ActionOptions & { paths?: string | string[] }): Step {
- const target = resolvePaths(opts?.paths);
- const { paths: _, ...rest } = opts ?? {};
- return this._installed.sh(`cd ${this.path} && uv run ty check ${target}`, {
- label: ":python: typecheck",
- ...rest,
- });
- }
-
- run(cmd: string, opts?: ActionOptions): Step {
- const firstWord = cmd.split(/\s+/)[0] ?? "run";
- return this._installed.sh(`cd ${this.path} && uv run ${cmd}`, {
- label: `:python: ${firstWord}`,
- ...opts,
- });
- }
-
- build(opts?: ActionOptions): Step {
- return this._installed.sh(`cd ${this.path} && uv build`, {
- label: ":python: build",
- ...opts,
- });
- }
-
- lockCheck(opts?: ActionOptions): Step {
- return this._installed.sh(`cd ${this.path} && uv lock --check`, {
- label: ":python: lock-check",
- ...opts,
- });
- }
-
- publish(opts?: ActionOptions): Step {
- return this._installed.sh(`cd ${this.path} && uv publish`, {
- label: ":python: publish",
- ...opts,
- });
- }
-}
-
-export function uv(opts?: UvOptions): UvProject {
- const path = opts?.path ?? ".";
- const version = opts?.version ?? "latest";
-
- if (!VERSION_RE.test(version)) {
- throw new Error(
- `py.uv: invalid version "${version}"\n → use "latest" or a semver like "0.4.18"`,
- );
- }
-
- const uvEnvPrefix =
- version === "latest" ? "" : `UV_VERSION=${version} `;
- const uvInstallCmd = [
- `${uvEnvPrefix}curl -LsSf https://astral.sh/uv/install.sh | sh`,
- "ln -sf /root/.local/bin/uv /usr/local/bin/uv",
- "uv --version",
- ].join(" && ");
-
- const uvInstalled = makeInstallChain({
- aptPackages: [...APT_PACKAGES],
- installCmd: uvInstallCmd,
- installCache: forever(),
- langTag: "python",
- installTag: "uv-install",
- image: opts?.image,
- base: opts?.base,
- });
-
- const synced = uvInstalled.sh(`cd ${path} && uv sync --all-extras`, {
- label: ":python: uv-sync",
- cache: onChange(`${path}/uv.lock`, `${path}/pyproject.toml`),
- });
-
- return new UvProject(path, synced);
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/toolchains/python.ts b/crates/hm-dsl-engine/harmont-ts/src/toolchains/python.ts
deleted file mode 100644
index 6c16e1ad..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/toolchains/python.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import type { Step, StepOptions } from "../step.js";
-import { forever, onChange } from "../cache.js";
-import { makeInstallChain } from "./shared.js";
-
-const APT_PACKAGES = [
- "curl",
- "ca-certificates",
- "python3",
- "python3-venv",
-] as const;
-const VERSION_RE = /^([0-9]+\.[0-9]+\.[0-9]+|latest)$/;
-
-function resolvePaths(paths?: string | string[]): string {
- if (paths == null) return ".";
- return Array.isArray(paths) ? paths.join(" ") : paths;
-}
-
-export interface PythonOptions {
- readonly path?: string;
- readonly uvVersion?: string;
- readonly image?: string;
- readonly base?: Step;
-}
-
-type ActionOptions = Omit;
-
-export class PythonToolchain {
- readonly path: string;
- private readonly _installed: Step;
-
- constructor(path: string, installed: Step) {
- this.path = path;
- this._installed = installed;
- }
-
- install(): Step {
- return this._installed;
- }
-
- /** Append a post-install command and return an advanced toolchain; chainable.
- * For prep steps the toolchain's actions must depend on but the SDK does not
- * model natively (codegen, fixtures, extra tooling). Action methods on the
- * returned object fork from this step.
- * @example hm.python({ path: "." }).setup("uv run python gen.py").test() */
- setup(cmd: string, opts?: StepOptions): PythonToolchain {
- return new PythonToolchain(this.path, this._installed.sh(cmd, opts));
- }
-
- test(opts?: ActionOptions): Step {
- return this._installed.sh(`cd ${this.path} && uv run pytest`, {
- label: ":python: test",
- ...opts,
- });
- }
-
- lint(opts?: ActionOptions): Step {
- return this._installed.sh(`cd ${this.path} && uv run ruff check .`, {
- label: ":python: lint",
- ...opts,
- });
- }
-
- fmt(opts?: ActionOptions): Step {
- return this._installed.sh(
- `cd ${this.path} && uv run ruff format --check .`,
- { label: ":python: fmt", ...opts },
- );
- }
-
- typecheck(opts?: ActionOptions & { paths?: string | string[] }): Step {
- const target = resolvePaths(opts?.paths);
- const { paths: _, ...rest } = opts ?? {};
- return this._installed.sh(`cd ${this.path} && uv run ty check ${target}`, {
- label: ":python: typecheck",
- ...rest,
- });
- }
-}
-
-export function python(opts?: PythonOptions): PythonToolchain {
- const path = opts?.path ?? ".";
- const uvVersion = opts?.uvVersion ?? "latest";
-
- if (!VERSION_RE.test(uvVersion)) {
- throw new Error(
- `hm.python: invalid uv version "${uvVersion}"\n → use "latest" or a semver like "0.2.0"`,
- );
- }
-
- const uvEnvPrefix =
- uvVersion === "latest" ? "" : `UV_VERSION=${uvVersion} `;
- const uvInstallCmd = [
- `${uvEnvPrefix}curl -LsSf https://astral.sh/uv/install.sh | sh`,
- "ln -sf /root/.local/bin/uv /usr/local/bin/uv",
- "uv --version",
- ].join(" && ");
-
- const uvInstalled = makeInstallChain({
- aptPackages: [...APT_PACKAGES],
- installCmd: uvInstallCmd,
- installCache: forever(),
- langTag: "python",
- installTag: "uv-install",
- image: opts?.image,
- base: opts?.base,
- });
-
- const synced = uvInstalled.sh(`cd ${path} && uv sync --all-extras`, {
- label: ":python: uv-sync",
- cache: onChange(`${path}/uv.lock`, `${path}/pyproject.toml`),
- });
-
- return new PythonToolchain(path, synced);
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/toolchains/rust.ts b/crates/hm-dsl-engine/harmont-ts/src/toolchains/rust.ts
deleted file mode 100644
index f160eae6..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/toolchains/rust.ts
+++ /dev/null
@@ -1,406 +0,0 @@
-import type { Step, StepOptions } from "../step.js";
-import { type CachePolicy, forever, onChange } from "../cache.js";
-import { makeInstallChain } from "./shared.js";
-import { type CargoOpts, cargoFlags, shQuote } from "./cargo.js";
-
-const APT_PACKAGES = [
- "curl",
- "ca-certificates",
- "build-essential",
- "pkg-config",
- "libssl-dev",
-] as const;
-const VERSION_RE = /^[a-z0-9.-]+$/;
-
-export interface RustToolchainOptions {
- readonly path?: string;
- readonly version?: string;
- readonly image?: string;
- readonly components?: readonly string[];
- readonly base?: Step;
-}
-
-export interface RustProjectOptions extends RustToolchainOptions {
- readonly cache?: CachePolicy;
-}
-
-type ActionOptions = Omit;
-type CargoActionOptions = CargoOpts & ActionOptions;
-
-export interface FeaturePowersetOptions extends ActionOptions {
- readonly subcommand?: string;
- readonly depth?: number;
- readonly eachFeature?: boolean;
- readonly noDevDeps?: boolean;
- readonly skip?: readonly string[];
- readonly includeFeatures?: readonly string[];
- readonly keepGoing?: boolean;
- readonly flags?: readonly string[];
-}
-
-// --- pure command builders (shared by both classes) ---
-
-function splitCargo(opts: CargoActionOptions | undefined): {
- cargo: CargoOpts;
- step: ActionOptions;
-} {
- const o = (opts ?? {}) as CargoActionOptions;
- const {
- workspace,
- packages,
- exclude,
- allFeatures,
- noDefaultFeatures,
- features,
- target,
- allTargets,
- release,
- profile,
- locked,
- flags,
- ...step
- } = o;
- return {
- cargo: {
- workspace,
- packages,
- exclude,
- allFeatures,
- noDefaultFeatures,
- features,
- target,
- allTargets,
- release,
- profile,
- locked,
- flags,
- },
- step: step as ActionOptions,
- };
-}
-
-function buildCmd(c: CargoOpts): string {
- return `cargo build${cargoFlags(c)}`;
-}
-function testCmd(c: CargoOpts, nextest: boolean): string {
- return `${nextest ? "cargo nextest run" : "cargo test"}${cargoFlags(c)}`;
-}
-function doctestCmd(c: CargoOpts): string {
- return `cargo test${cargoFlags(c)} --doc`;
-}
-function clippyCmd(
- c: CargoOpts,
- denyWarnings: boolean,
- extraLints: readonly string[],
-): string {
- const mid = cargoFlags(c);
- const trail = [...(denyWarnings ? ["-D warnings"] : []), ...extraLints];
- return `cargo clippy${mid}${trail.length ? " -- " + trail.join(" ") : ""}`;
-}
-function fmtCmd(all: boolean, check: boolean, flags: readonly string[]): string {
- const toks = ["cargo fmt"];
- if (all) toks.push("--all");
- if (check) toks.push("--check");
- toks.push(...flags);
- return toks.join(" ");
-}
-function docCmd(c: CargoOpts, noDeps: boolean, privateItems: boolean): string {
- const pre: string[] = [];
- if (noDeps) pre.push("--no-deps");
- if (privateItems) pre.push("--document-private-items");
- return `cargo doc${pre.length ? " " + pre.join(" ") : ""}${cargoFlags(c)}`;
-}
-function hackCmd(o: FeaturePowersetOptions): string {
- const toks = ["cargo hack", o.subcommand ?? "check"];
- if (o.eachFeature) toks.push("--each-feature");
- else toks.push("--feature-powerset", "--depth", String(o.depth ?? 2));
- if (o.noDevDeps ?? true) toks.push("--no-dev-deps");
- if (o.skip?.length) toks.push("--skip " + o.skip.map(shQuote).join(","));
- if (o.includeFeatures?.length)
- toks.push("--include-features " + o.includeFeatures.map(shQuote).join(","));
- if (o.keepGoing) toks.push("--keep-going");
- toks.push(...(o.flags ?? []));
- return toks.join(" ");
-}
-function denyDocEnv(step: ActionOptions, deny: boolean): ActionOptions {
- if (!deny) return step;
- return { ...step, env: { RUSTDOCFLAGS: "-D warnings", ...(step.env ?? {}) } };
-}
-
-function withTargetAdd(cmd: string, target: string | undefined, addTarget: boolean): string {
- return target !== undefined && addTarget
- ? `rustup target add ${shQuote(target)} && ${cmd}`
- : cmd;
-}
-
-// --- classes ---
-
-export class RustToolchain {
- readonly path: string;
- private readonly _installed: Step;
-
- constructor(path: string, installed: Step) {
- this.path = path;
- this._installed = installed;
- }
-
- install(): Step {
- return this._installed;
- }
-
- /** Append a post-install command and return an advanced toolchain; chainable.
- * For prep steps the toolchain's actions must depend on but the SDK does not
- * model natively (codegen, fixtures, extra tooling). Action methods on the
- * returned object fork from this step. (On warmup-based projects, splice prep
- * here, pre-warmup: hm.rust.toolchain().setup("…").project({ path: "." }).)
- * @example hm.rust.toolchain().setup("cargo install sqlx-cli").build() */
- setup(cmd: string, opts?: StepOptions): RustToolchain {
- return new RustToolchain(this.path, this._installed.sh(cmd, opts));
- }
-
- _cargo(cmd: string, label: string, opts?: ActionOptions): Step {
- return this._installed.sh(
- `. $HOME/.cargo/env && cd ${this.path} && ${cmd}`,
- { label, ...opts },
- );
- }
-
- build(opts?: CargoActionOptions & { addTarget?: boolean }): Step {
- const { addTarget, ...rest } = opts ?? {};
- const { cargo, step } = splitCargo(rest);
- const cmd = withTargetAdd(buildCmd(cargo), cargo.target, addTarget ?? true);
- return this._cargo(cmd, ":rust: build", step);
- }
-
- test(opts?: CargoActionOptions & { nextest?: boolean; addTarget?: boolean }): Step {
- const { nextest, addTarget, ...rest } = opts ?? {};
- const { cargo, step } = splitCargo(rest);
- const cmd = withTargetAdd(testCmd(cargo, nextest ?? false), cargo.target, addTarget ?? true);
- return this._cargo(cmd, ":rust: test", step);
- }
-
- doctest(opts?: CargoActionOptions & { addTarget?: boolean }): Step {
- const { addTarget, ...rest } = opts ?? {};
- const { cargo, step } = splitCargo(rest);
- const cmd = withTargetAdd(doctestCmd(cargo), cargo.target, addTarget ?? true);
- return this._cargo(cmd, ":rust: doctest", step);
- }
-
- clippy(
- opts?: CargoActionOptions & {
- denyWarnings?: boolean;
- extraLints?: readonly string[];
- addTarget?: boolean;
- },
- ): Step {
- const { denyWarnings, extraLints, addTarget, ...rest } = opts ?? {};
- const { cargo, step } = splitCargo({ allTargets: true, ...rest });
- const cmd = withTargetAdd(clippyCmd(cargo, denyWarnings ?? true, extraLints ?? []), cargo.target, addTarget ?? true);
- return this._cargo(cmd, ":rust: clippy", step);
- }
-
- fmt(
- opts?: ActionOptions & {
- all?: boolean;
- check?: boolean;
- flags?: readonly string[];
- },
- ): Step {
- const { all, check, flags, ...step } = opts ?? {};
- return this._cargo(
- fmtCmd(all ?? true, check ?? true, flags ?? []),
- ":rust: fmt",
- step,
- );
- }
-
- doc(
- opts?: CargoActionOptions & {
- noDeps?: boolean;
- documentPrivateItems?: boolean;
- denyWarnings?: boolean;
- addTarget?: boolean;
- },
- ): Step {
- const { noDeps, documentPrivateItems, denyWarnings, addTarget, ...rest } = opts ?? {};
- const { cargo, step } = splitCargo(rest);
- const cmd = withTargetAdd(docCmd(cargo, noDeps ?? true, documentPrivateItems ?? false), cargo.target, addTarget ?? true);
- return this._cargo(cmd, ":rust: doc", denyDocEnv(step, denyWarnings ?? true));
- }
-
- warmup(opts?: ActionOptions): Step {
- return this._cargo(
- "cargo build --workspace --tests --locked",
- ":rust: warmup",
- opts,
- );
- }
-
- featurePowerset(opts?: FeaturePowersetOptions): Step {
- const {
- subcommand,
- depth,
- eachFeature,
- noDevDeps,
- skip,
- includeFeatures,
- keepGoing,
- flags,
- ...step
- } = opts ?? {};
- // Global install — no crate dir, so don't cd; keeps the forever-cache key
- // identical across toolchains regardless of path.
- const installedHack = this._installed.sh(
- ". $HOME/.cargo/env && cargo install cargo-hack --locked",
- { label: ":rust: install cargo-hack", cache: forever() },
- );
- return installedHack.sh(
- `. $HOME/.cargo/env && cd ${this.path} && ${hackCmd({ subcommand, depth, eachFeature, noDevDeps, skip, includeFeatures, keepGoing, flags })}`,
- { label: ":rust: feature-powerset", ...step },
- );
- }
-}
-
-export class RustProject {
- readonly toolchain: RustToolchain;
- readonly warmup: Step;
-
- constructor(toolchain: RustToolchain, warmup: Step) {
- this.toolchain = toolchain;
- this.warmup = warmup;
- }
-
- private _emit(cmd: string, label: string, step: ActionOptions): Step {
- return this.warmup.sh(
- `. $HOME/.cargo/env && cd ${this.toolchain.path} && ${cmd}`,
- { label, ...step },
- );
- }
-
- build(opts?: CargoActionOptions & { addTarget?: boolean }): Step {
- const { addTarget, ...rest } = opts ?? {};
- const { cargo, step } = splitCargo({ workspace: true, ...rest });
- const cmd = withTargetAdd(buildCmd(cargo), cargo.target, addTarget ?? true);
- return this._emit(cmd, ":rust: build", step);
- }
-
- test(opts?: CargoActionOptions & { nextest?: boolean; addTarget?: boolean }): Step {
- const { nextest, addTarget, ...rest } = opts ?? {};
- const { cargo, step } = splitCargo({ workspace: true, ...rest });
- const cmd = withTargetAdd(testCmd(cargo, nextest ?? false), cargo.target, addTarget ?? true);
- return this._emit(cmd, ":rust: test", step);
- }
-
- doctest(opts?: CargoActionOptions & { addTarget?: boolean }): Step {
- const { addTarget, ...rest } = opts ?? {};
- const { cargo, step } = splitCargo({ workspace: true, ...rest });
- const cmd = withTargetAdd(doctestCmd(cargo), cargo.target, addTarget ?? true);
- return this._emit(cmd, ":rust: doctest", step);
- }
-
- clippy(
- opts?: CargoActionOptions & {
- denyWarnings?: boolean;
- extraLints?: readonly string[];
- addTarget?: boolean;
- },
- ): Step {
- const { denyWarnings, extraLints, addTarget, ...rest } = opts ?? {};
- const { cargo, step } = splitCargo({ workspace: true, allTargets: true, ...rest });
- const cmd = withTargetAdd(clippyCmd(cargo, denyWarnings ?? true, extraLints ?? []), cargo.target, addTarget ?? true);
- return this._emit(cmd, ":rust: clippy", step);
- }
-
- fmt(
- opts?: ActionOptions & {
- all?: boolean;
- check?: boolean;
- flags?: readonly string[];
- },
- ): Step {
- // fmt has no warmup dependency; chain off install like the toolchain does.
- return this.toolchain.fmt(opts);
- }
-
- doc(
- opts?: CargoActionOptions & {
- noDeps?: boolean;
- documentPrivateItems?: boolean;
- denyWarnings?: boolean;
- addTarget?: boolean;
- },
- ): Step {
- const { noDeps, documentPrivateItems, denyWarnings, addTarget, ...rest } = opts ?? {};
- const { cargo, step } = splitCargo({ workspace: true, ...rest });
- const cmd = withTargetAdd(docCmd(cargo, noDeps ?? true, documentPrivateItems ?? false), cargo.target, addTarget ?? true);
- return this._emit(cmd, ":rust: doc", denyDocEnv(step, denyWarnings ?? true));
- }
-
- featurePowerset(opts?: FeaturePowersetOptions): Step {
- return this.toolchain.featurePowerset(opts);
- }
-
- ci(opts?: { nextest?: boolean; doc?: boolean }): Step[] {
- const nextest = opts?.nextest ?? false;
- const steps: Step[] = [this.test({ nextest })];
- if (nextest) steps.push(this.doctest());
- steps.push(this.clippy());
- steps.push(this.fmt());
- if (opts?.doc) steps.push(this.doc());
- return steps;
- }
-}
-
-function makeToolchain(opts?: RustToolchainOptions): RustToolchain {
- const path = opts?.path ?? ".";
- const version = opts?.version ?? "stable";
- const components = opts?.components ?? ["clippy", "rustfmt"];
-
- if (!VERSION_RE.test(version)) {
- throw new Error(
- `rust.toolchain: invalid version "${version}"\n → use "stable", "nightly", or a semver like "1.81.0"`,
- );
- }
-
- const componentFlag =
- components.length > 0 ? ` --component ${components.join(",")}` : "";
- const installCmd = [
- `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain ${version} --profile minimal${componentFlag}`,
- `. $HOME/.cargo/env && rustc --version && cargo --version`,
- ].join(" && ");
-
- const installed = makeInstallChain({
- aptPackages: [...APT_PACKAGES],
- installCmd,
- installCache: forever(),
- langTag: "rust",
- installTag: "rustup",
- image: opts?.image,
- base: opts?.base,
- });
-
- return new RustToolchain(path, installed);
-}
-
-function makeProject(opts?: RustProjectOptions): RustProject {
- const path = opts?.path ?? ".";
- const tc = makeToolchain(opts);
-
- const lockPath = path !== "." ? `${path}/Cargo.lock` : "Cargo.lock";
- const tomlGlob = path !== "." ? `${path}/**/Cargo.toml` : "**/Cargo.toml";
- const rsGlob = path !== "." ? `${path}/**/*.rs` : "**/*.rs";
- const warmupCache = opts?.cache ?? onChange(lockPath, tomlGlob, rsGlob);
-
- const warm = tc._cargo(
- "cargo build --workspace --tests --locked",
- ":rust: warmup",
- { cache: warmupCache },
- );
-
- return new RustProject(tc, warm);
-}
-
-export const rust = {
- toolchain: makeToolchain,
- project: makeProject,
-};
diff --git a/crates/hm-dsl-engine/harmont-ts/src/toolchains/shared.ts b/crates/hm-dsl-engine/harmont-ts/src/toolchains/shared.ts
deleted file mode 100644
index d2365931..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/toolchains/shared.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { scratch, type Step, type StepOptions } from "../step.js";
-import { ttl, type CachePolicy } from "../cache.js";
-
-const APT_TTL_SECONDS = 86400; // 1 day
-
-export function aptInstallCmd(packages: readonly string[]): string {
- return `apt-get update && apt-get install -y ${packages.join(" ")}`;
-}
-
-export function nodeInstallCmd(version: string): string {
- const major = version.replace(/\.x$/, "");
- return `curl -fsSL https://deb.nodesource.com/setup_${major}.x | bash - && apt-get install -y nodejs`;
-}
-
-export function bunInstallCmd(version?: string): string {
- const versionArg = version != null ? ` -s "bun-v${version}"` : "";
- return `curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr/local bash${versionArg}`;
-}
-
-export function denoInstallCmd(version?: string): string {
- const versionArg = version != null ? ` -s "v${version}"` : "";
- return `curl -fsSL https://deno.land/install.sh | sh${versionArg} && ln -sf $HOME/.deno/bin/deno /usr/local/bin/deno`;
-}
-
-export function aptBase(opts: {
- packages: readonly string[];
- image?: string;
- label?: string;
-}): Step {
- return scratch({ image: opts.image }).sh(aptInstallCmd(opts.packages), {
- label: opts.label ?? ":apt: base",
- cache: ttl(APT_TTL_SECONDS),
- });
-}
-
-export function makeInstallChain(opts: {
- aptPackages: readonly string[];
- installCmd: string;
- installCache: CachePolicy;
- langTag: string;
- installTag: string;
- image?: string;
- base?: Step;
-}): Step {
- let parent: Step;
- if (opts.base == null) {
- parent = scratch({ image: opts.image }).sh(aptInstallCmd(opts.aptPackages), {
- label: `:${opts.langTag}: apt-base`,
- cache: ttl(APT_TTL_SECONDS),
- });
- } else {
- parent = opts.base;
- }
- return parent.sh(opts.installCmd, {
- label: `:${opts.langTag}: ${opts.installTag}`,
- cache: opts.installCache,
- });
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/toolchains/zig.ts b/crates/hm-dsl-engine/harmont-ts/src/toolchains/zig.ts
deleted file mode 100644
index f61d846e..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/toolchains/zig.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import type { Step, StepOptions } from "../step.js";
-import { forever } from "../cache.js";
-import { makeInstallChain } from "./shared.js";
-
-const APT_PACKAGES = ["curl", "ca-certificates", "xz-utils"] as const;
-const VERSION_RE = /^[0-9]+\.[0-9]+\.[0-9]+$/;
-const NEW_URL_FORMAT_VERSION = [0, 14, 1] as const;
-
-function versionAtLeast(
- version: string,
- min: readonly [number, number, number],
-): boolean {
- const parts = version.split(".").map(Number);
- for (let i = 0; i < 3; i++) {
- if ((parts[i] ?? 0) !== min[i]) return (parts[i] ?? 0) > min[i];
- }
- return true;
-}
-
-export interface ZigOptions {
- readonly path?: string;
- readonly version?: string;
- readonly image?: string;
- readonly base?: Step;
-}
-
-type ActionOptions = Omit;
-
-export class ZigToolchain {
- private readonly _installed: Step;
-
- constructor(installed: Step) {
- this._installed = installed;
- }
-
- install(): Step {
- return this._installed;
- }
-
- /** Append a post-install command and return an advanced toolchain; chainable.
- * For prep steps the toolchain's actions must depend on but the SDK does not
- * model natively (codegen, fixtures, extra tooling). Action methods on the
- * returned object fork from this step.
- * @example hm.zig().setup("zig build gen").project(".") */
- setup(cmd: string, opts?: StepOptions): ZigToolchain {
- return new ZigToolchain(this._installed.sh(cmd, opts));
- }
-
- project(path: string = "."): ZigProject {
- return new ZigProject(path, this._installed);
- }
-}
-
-export class ZigProject {
- readonly path: string;
- private readonly _installed: Step;
-
- constructor(path: string, installed: Step) {
- this.path = path;
- this._installed = installed;
- }
-
- install(): Step {
- return this._installed;
- }
-
- /** Append a post-install command and return an advanced project; chainable.
- * For prep steps the toolchain's actions must depend on but the SDK does not
- * model natively (codegen, fixtures, extra tooling). Action methods on the
- * returned object fork from this step.
- * @example hm.zig({ path: "." }).setup("zig build gen").build() */
- setup(cmd: string, opts?: StepOptions): ZigProject {
- return new ZigProject(this.path, this._installed.sh(cmd, opts));
- }
-
- build(opts?: ActionOptions): Step {
- return this._installed.sh(`cd ${this.path} && zig build`, {
- label: `:zig: ${this.path} build`,
- ...opts,
- });
- }
-
- test(opts?: ActionOptions): Step {
- return this._installed.sh(`cd ${this.path} && zig build test`, {
- label: `:zig: ${this.path} test`,
- ...opts,
- });
- }
-
- fmt(opts?: ActionOptions): Step {
- return this._installed.sh(`cd ${this.path} && zig fmt --check .`, {
- label: `:zig: ${this.path} fmt`,
- ...opts,
- });
- }
-}
-
-export function zig(opts: ZigOptions & { path: string }): ZigProject;
-export function zig(opts?: ZigOptions): ZigToolchain;
-export function zig(opts?: ZigOptions): ZigToolchain | ZigProject {
- const version = opts?.version ?? "0.14.1";
-
- if (!VERSION_RE.test(version)) {
- throw new Error(
- `hm.zig: invalid version "${version}"\n → use a semver like "0.14.1"`,
- );
- }
-
- const tarball = versionAtLeast(version, NEW_URL_FORMAT_VERSION)
- ? `zig-x86_64-linux-${version}.tar.xz`
- : `zig-linux-x86_64-${version}.tar.xz`;
-
- const installCmd = [
- `curl -fsSL https://ziglang.org/download/${version}/${tarball} -o /tmp/zig.tar.xz`,
- "rm -rf /usr/local/zig && mkdir -p /usr/local/zig",
- "tar -xJf /tmp/zig.tar.xz -C /usr/local/zig --strip-components=1",
- "ln -sf /usr/local/zig/zig /usr/local/bin/zig",
- "zig version",
- ].join(" && ");
-
- const installed = makeInstallChain({
- aptPackages: [...APT_PACKAGES],
- installCmd,
- installCache: forever(),
- langTag: "zig",
- installTag: "install",
- image: opts?.image,
- base: opts?.base,
- });
-
- const toolchain = new ZigToolchain(installed);
- return opts?.path != null ? toolchain.project(opts.path) : toolchain;
-}
diff --git a/crates/hm-dsl-engine/harmont-ts/src/triggers.ts b/crates/hm-dsl-engine/harmont-ts/src/triggers.ts
deleted file mode 100644
index ac8da7ba..00000000
--- a/crates/hm-dsl-engine/harmont-ts/src/triggers.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-export type Trigger = PushTrigger | PullRequestTrigger;
-
-function normalizeGlobs(
- value: string | readonly string[] | undefined,
-): string[] | undefined {
- if (value === undefined) return undefined;
- if (typeof value === "string") return [value];
- return [...value];
-}
-
-export class PushTrigger {
- readonly branches: string[] | undefined;
- readonly tags: string[] | undefined;
-
- constructor(branches: string[] | undefined, tags: string[] | undefined) {
- this.branches = branches;
- this.tags = tags;
- }
-
- toJSON(): Record {
- const out: Record = { event: "push" };
- if (this.branches !== undefined) out.branches = this.branches;
- if (this.tags !== undefined) out.tags = this.tags;
- return out;
- }
-}
-
-export function push(
- opts: { branch: string | string[]; tag?: undefined } | { tag: string | string[]; branch?: undefined },
-): PushTrigger {
- const branch = "branch" in opts ? opts.branch : undefined;
- const tag = "tag" in opts ? opts.tag : undefined;
- const branches = normalizeGlobs(branch);
- const tags = normalizeGlobs(tag);
- if ((branches === undefined) === (tags === undefined)) {
- throw new Error(
- 'hm.push: pass exactly one of branch or tag\n → e.g. push({ branch: "main" }) or push({ tag: "v*" })',
- );
- }
- return new PushTrigger(branches, tags);
-}
-
-const PR_TYPES = new Set([
- "opened",
- "synchronize",
- "reopened",
- "closed",
- "ready_for_review",
-] as const);
-
-type PrEventType = "opened" | "synchronize" | "reopened" | "closed" | "ready_for_review";
-
-const DEFAULT_PR_TYPES: PrEventType[] = ["opened", "synchronize", "reopened"];
-
-export class PullRequestTrigger {
- readonly branches: string[] | undefined;
- readonly types: string[];
-
- constructor(branches: string[] | undefined, types: string[]) {
- this.branches = branches;
- this.types = types;
- }
-
- toJSON(): Record {
- const out: Record = { event: "pull_request" };
- if (this.branches !== undefined) out.branches = this.branches;
- out.types = this.types;
- return out;
- }
-}
-
-export function pullRequest(opts?: {
- branches?: string | string[];
- types?: PrEventType[];
-}): PullRequestTrigger {
- const types = opts?.types ?? DEFAULT_PR_TYPES;
- if (types.length === 0) {
- throw new Error("hm.pullRequest: types must be non-empty");
- }
- for (const t of types) {
- if (!PR_TYPES.has(t as any)) {
- const valid = [...PR_TYPES].sort().join(", ");
- throw new Error(`unknown pull_request type "${t}"\n → valid: ${valid}`);
- }
- }
- return new PullRequestTrigger(normalizeGlobs(opts?.branches), [...types]);
-}
-
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/cache.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/cache.test.ts
deleted file mode 100644
index cee168de..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/cache.test.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { forever, ttl, onChange, compose, type CachePolicy } from "../src/cache.js";
-
-describe("forever", () => {
- it("creates a forever policy with no env keys", () => {
- const p = forever();
- expect(p).toEqual({ kind: "forever", envKeys: [] });
- });
-
- it("accepts env keys", () => {
- const p = forever({ envKeys: ["NODE_ENV"] });
- expect(p.envKeys).toEqual(["NODE_ENV"]);
- });
-});
-
-describe("ttl", () => {
- it("creates a ttl policy with duration in seconds", () => {
- const p = ttl(3600);
- expect(p).toEqual({ kind: "ttl", durationSeconds: 3600, envKeys: [] });
- });
-
- it("accepts env keys", () => {
- const p = ttl(86400, { envKeys: ["CI"] });
- expect(p.envKeys).toEqual(["CI"]);
- });
-});
-
-describe("onChange", () => {
- it("creates an on_change policy with paths", () => {
- const p = onChange("src/", "package.json");
- expect(p).toEqual({ kind: "on_change", paths: ["src/", "package.json"] });
- });
-});
-
-describe("compose", () => {
- it("composes multiple policies", () => {
- const p = compose(ttl(86400), onChange("src/"));
- expect(p.kind).toBe("compose");
- expect(p.policies).toHaveLength(2);
- expect(p.policies[0].kind).toBe("ttl");
- expect(p.policies[1].kind).toBe("on_change");
- });
-});
-
-describe("type discrimination", () => {
- it("kind field enables type narrowing", () => {
- const p: CachePolicy = forever();
- switch (p.kind) {
- case "forever":
- expect(p.envKeys).toEqual([]);
- break;
- default:
- throw new Error("unexpected kind");
- }
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/duration.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/duration.test.ts
deleted file mode 100644
index 6da13749..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/duration.test.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-// tests/duration.test.ts
-import { describe, expect, it } from "vitest";
-
-import { parseDuration } from "../src/duration.js";
-
-describe("parseDuration", () => {
- it.each([
- ["30s", 30],
- ["5m", 300],
- ["1h", 3600],
- ["1h30m", 5400],
- ["2h15m30s", 8130],
- [45, 45],
- ])("parses %s -> %d", (value, expected) => {
- expect(parseDuration(value as string | number)).toBe(expected);
- });
-
- it.each(["", "30", "30 s", "1d", "m", "-5s", "0s", 0, -3, NaN])(
- "rejects %s",
- (bad) => {
- expect(() => parseDuration(bad as string | number)).toThrow();
- },
- );
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/e2e-fixtures.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/e2e-fixtures.test.ts
deleted file mode 100644
index c5d872c6..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/e2e-fixtures.test.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
-import { dirname, resolve } from "node:path";
-import { fileURLToPath } from "node:url";
-import { beforeEach, describe, expect, it } from "vitest";
-import { clearTargetCache } from "../src/target.js";
-import { pipeline } from "../src/pipeline.js";
-import { sh } from "../src/step.js";
-import { ttl } from "../src/cache.js";
-import { go } from "../src/toolchains/go.js";
-import { python } from "../src/toolchains/python.js";
-import { js } from "../src/toolchains/js.js";
-import { rust } from "../src/toolchains/rust.js";
-import { zig } from "../src/toolchains/zig.js";
-import { cmake } from "../src/toolchains/cmake.js";
-
-const __dir = dirname(fileURLToPath(import.meta.url));
-const FIXTURES_DIR = resolve(__dir, "../../../../tests/e2e/fixtures/ts");
-
-function deepSortKeys(obj: unknown): unknown {
- if (Array.isArray(obj)) return obj.map(deepSortKeys);
- if (obj !== null && typeof obj === "object") {
- const sorted: Record = {};
- for (const key of Object.keys(obj as Record).sort()) {
- sorted[key] = deepSortKeys((obj as Record)[key]);
- }
- return sorted;
- }
- return obj;
-}
-
-function assertFixture(name: string, ir: Record): void {
- const rendered = JSON.stringify(deepSortKeys(ir), null, 2) + "\n";
- const fixturePath = resolve(FIXTURES_DIR, `${name}.json`);
-
- if (process.env.UPDATE_E2E_FIXTURES) {
- mkdirSync(dirname(fixturePath), { recursive: true });
- writeFileSync(fixturePath, rendered);
- return;
- }
-
- if (!existsSync(fixturePath)) {
- throw new Error(
- `Fixture ${fixturePath} missing — run with UPDATE_E2E_FIXTURES=1`,
- );
- }
- const expected = JSON.parse(readFileSync(fixturePath, "utf-8"));
- const actual = JSON.parse(rendered);
- expect(actual).toEqual(expected);
-}
-
-describe("E2E pipeline fixtures", () => {
- beforeEach(() => {
- clearTargetCache();
- });
-
- it("monorepo-ci", () => {
- const goProject = go({ path: "services/api" });
- const pyProject = python({ path: "services/ml" });
- const webProject = js.project({ path: "web" });
-
- const ir = pipeline(
- [
- goProject.build(),
- goProject.test(),
- goProject.vet(),
- pyProject.test(),
- pyProject.lint(),
- pyProject.typecheck(),
- webProject.run("build"),
- webProject.run("test"),
- webProject.run("lint"),
- ],
- { env: { CI: "true" } },
- );
-
- expect(ir.version).toBe("0");
- const monorepoChildIdxs = new Set(
- ir.graph.edges.filter((e: any) => e[2] === "builds_in").map((e: any) => e[1]),
- );
- const monorepoRoots = ir.graph.nodes.filter((_: any, i: number) => !monorepoChildIdxs.has(i));
- expect(monorepoRoots.length).toBeGreaterThan(0);
- expect(monorepoRoots.every((n: any) => "image" in n.step)).toBe(true);
- expect(ir.graph.nodes.length).toBeGreaterThan(0);
- assertFixture("monorepo-ci", ir);
- });
-
- it("rust-release", () => {
- const project = rust.toolchain({ path: "." });
-
- const ir = pipeline(
- [project.build(), project.test(), project.clippy(), project.fmt(), project.doc()],
- { env: { CI: "true" } },
- );
-
- expect(ir.version).toBe("0");
- assertFixture("rust-release", ir);
- });
-
- it("zig-node-polyglot", () => {
- const base = sh(
- "apt-get update && apt-get install -y --no-install-recommends " +
- "curl ca-certificates xz-utils",
- { label: ":apt: base", cache: ttl(86400), image: "ubuntu:24.04" },
- );
- const zigTc = zig({ base });
- const projA = zigTc.project("zig-a");
- const projB = zigTc.project("zig-b");
- const web = js.project({ path: "web", base });
-
- const ir = pipeline(
- [
- projA.build(),
- projA.test(),
- projB.build(),
- projB.test(),
- web.run("build"),
- web.run("test"),
- web.run("lint"),
- ],
- { env: { CI: "true" } },
- );
-
- expect(ir.version).toBe("0");
- assertFixture("zig-node-polyglot", ir);
- });
-
- it("kitchen-sink", () => {
- const cProject = cmake({ path: "infra/agent" });
- const pyWeb = python({ path: "services/web" });
-
- const ir = pipeline(
- [cProject.build(), cProject.test(), cProject.fmt(), pyWeb.test(), pyWeb.lint()],
- { env: { CI: "true" } },
- );
-
- expect(ir.version).toBe("0");
- assertFixture("kitchen-sink", ir);
- });
-
- it("cmake-advanced", () => {
- const project = cmake({
- path: ".",
- compiler: "clang-18",
- defines: { CMAKE_BUILD_TYPE: "Release", CMAKE_CXX_STANDARD: "20" },
- });
-
- const ir = pipeline(
- [project.test(), project.lint(), project.fmt()],
- { env: { CI: "true" } },
- );
-
- expect(ir.version).toBe("0");
- expect(ir.graph.nodes.length).toBeGreaterThan(3);
- assertFixture("cmake-advanced", ir);
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/envelope.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/envelope.test.ts
deleted file mode 100644
index 58a3711f..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/envelope.test.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { mkdtempSync } from "node:fs";
-import { tmpdir } from "node:os";
-import { join } from "node:path";
-import { describe, expect, it } from "vitest";
-import { forever } from "../src/cache.js";
-import { renderEnvelope, type PipelineDefinition } from "../src/envelope.js";
-import { pipeline } from "../src/pipeline.js";
-import { sh } from "../src/step.js";
-import { push, pullRequest } from "../src/triggers.js";
-
-function makeDef(overrides?: Partial): PipelineDefinition {
- return {
- slug: "ci",
- pipeline: pipeline([sh("echo", { label: "test" })]),
- ...overrides,
- };
-}
-
-describe("renderEnvelope", () => {
- it("produces schema_version 1 envelope", () => {
- const json = renderEnvelope([makeDef()]);
- const parsed = JSON.parse(json);
- expect(parsed.schema_version).toBe("1");
- expect(parsed.pipelines).toHaveLength(1);
- });
-
- it("includes slug, name, allow_manual, triggers, definition", () => {
- const json = renderEnvelope([
- makeDef({
- slug: "my-pipeline",
- name: "My Pipeline",
- allowManual: false,
- triggers: [push({ branch: "main" })],
- }),
- ]);
- const parsed = JSON.parse(json);
- const p = parsed.pipelines[0];
- expect(p.slug).toBe("my-pipeline");
- expect(p.name).toBe("My Pipeline");
- expect(p.allow_manual).toBe(false);
- expect(p.triggers).toEqual([{ event: "push", branches: ["main"] }]);
- expect(p.definition.version).toBe("0");
- });
-
- it("defaults name to slug, allowManual to true, triggers to empty", () => {
- const json = renderEnvelope([makeDef({ slug: "ci" })]);
- const parsed = JSON.parse(json);
- const p = parsed.pipelines[0];
- expect(p.name).toBe("ci");
- expect(p.allow_manual).toBe(true);
- expect(p.triggers).toEqual([]);
- });
-
- it("handles multiple pipelines", () => {
- const json = renderEnvelope([
- makeDef({ slug: "ci" }),
- makeDef({ slug: "deploy" }),
- ]);
- const parsed = JSON.parse(json);
- expect(parsed.pipelines).toHaveLength(2);
- expect(parsed.pipelines[0].slug).toBe("ci");
- expect(parsed.pipelines[1].slug).toBe("deploy");
- });
-
- it("resolves cache keys when basePath is provided", () => {
- const tmp = mkdtempSync(join(tmpdir(), "envelope-test-"));
- const def: PipelineDefinition = {
- slug: "ci",
- pipeline: pipeline([sh("apt-get update", { label: "apt", cache: forever() })]),
- };
- const json = renderEnvelope([def], { basePath: tmp, now: 1000000 });
- const parsed = JSON.parse(json);
- const cache = parsed.pipelines[0].definition.graph.nodes[0].step.cache;
- expect(cache.key).toBeTypeOf("string");
- expect(cache.key.length).toBe(64);
- });
-
- it("throws when cached steps exist but basePath is missing", () => {
- const def: PipelineDefinition = {
- slug: "ci",
- pipeline: pipeline([sh("apt-get update", { label: "apt", cache: forever() })]),
- };
- expect(() => renderEnvelope([def])).toThrowError(/basePath/);
- });
-
- it("allows omitting basePath when no steps are cached", () => {
- const def: PipelineDefinition = {
- slug: "ci",
- pipeline: pipeline([sh("echo hello", { label: "greet" })]),
- };
- expect(() => renderEnvelope([def])).not.toThrow();
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/examples.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/examples.test.ts
deleted file mode 100644
index dec87182..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/examples.test.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { readdirSync, existsSync } from "node:fs";
-import { dirname, join, resolve } from "node:path";
-import { fileURLToPath } from "node:url";
-import { beforeEach, describe, expect, it } from "vitest";
-import { clearTargetCache } from "../src/target.js";
-
-const __dir = dirname(fileURLToPath(import.meta.url));
-const EXAMPLES_ROOT = resolve(__dir, "../../../../examples");
-
-function exampleDirs(): string[] {
- if (!existsSync(EXAMPLES_ROOT)) return [];
- return readdirSync(EXAMPLES_ROOT, { withFileTypes: true })
- .filter((d) => d.isDirectory())
- .filter((d) =>
- existsSync(join(EXAMPLES_ROOT, d.name, ".hm", "pipeline.ts")),
- )
- .map((d) => d.name)
- .sort();
-}
-
-const examples = exampleDirs();
-
-describe.skipIf(examples.length === 0)("examples render to v0 IR", () => {
- beforeEach(() => {
- clearTargetCache();
- });
-
- for (const name of examples) {
- it(`${name}: produces valid CI pipeline IR`, async () => {
- const pipelinePath = join(
- EXAMPLES_ROOT,
- name,
- ".hm",
- "pipeline.ts",
- );
- const mod = await import(pipelinePath);
- const definitions = mod.default ?? mod.pipelines;
-
- expect(Array.isArray(definitions)).toBe(true);
- expect(definitions.length).toBeGreaterThan(0);
-
- const ci = definitions.find((d: any) => d.slug === "ci");
- expect(ci).toBeDefined();
- expect(ci.pipeline.version).toBe("0");
- expect(ci.pipeline.graph.nodes.length).toBeGreaterThan(0);
- expect(ci.pipeline.graph.edge_property).toBe("directed");
-
- // Every root step (no builds_in parent) must have an image stamped on it
- const nodes = ci.pipeline.graph.nodes;
- const childIdxs = new Set(
- ci.pipeline.graph.edges.filter((e: any) => e[2] === "builds_in").map((e: any) => e[1]),
- );
- const roots = nodes.filter((_: any, i: number) => !childIdxs.has(i));
- expect(roots.length).toBeGreaterThan(0);
- expect(roots.every((n: any) => "image" in n.step)).toBe(true);
-
- // Verify all nodes have required fields
- for (const node of ci.pipeline.graph.nodes) {
- expect(node.step.key).toBeDefined();
- expect(node.step.cmd).toBeDefined();
- expect(typeof node.env).toBe("object");
- }
-
- // Verify edges reference valid node indices
- for (const [src, dst, kind] of ci.pipeline.graph.edges) {
- expect(src).toBeLessThan(ci.pipeline.graph.nodes.length);
- expect(dst).toBeLessThan(ci.pipeline.graph.nodes.length);
- expect(["builds_in", "depends_on"]).toContain(kind);
- }
- });
- }
-
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/integration.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/integration.test.ts
deleted file mode 100644
index 6c022bbd..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/integration.test.ts
+++ /dev/null
@@ -1,237 +0,0 @@
-import { describe, expect, it, beforeEach } from "vitest";
-import {
- Step,
- scratch,
- sh,
- wait,
- timeout,
- forever,
- ttl,
- onChange,
- compose,
- pipeline,
- target,
- clearTargetCache,
- renderEnvelope,
- push,
- pullRequest,
- PushTrigger,
- PullRequestTrigger,
- type PipelineDefinition,
-} from "../src/index.js";
-
-beforeEach(() => {
- clearTargetCache();
-});
-
-describe("full pipeline build", () => {
- it("creates install -> build -> test chain with cache, env", () => {
- const install = scratch()
- .sh("npm ci", { label: "install", cache: forever() });
- const build = install
- .sh("npm run build", { label: "build", env: { NODE_ENV: "production" } });
- const test = timeout(300, build
- .sh("npm test", { label: "test" }));
-
- const ir = pipeline([test], {
- env: { CI: "true" },
- });
-
- // version
- expect(ir.version).toBe("0");
-
- // node count
- expect(ir.graph.nodes).toHaveLength(3);
-
- // edge count — two builds_in edges (install->build, build->test)
- expect(ir.graph.edges).toHaveLength(2);
- expect(ir.graph.edges.every((e) => e[2] === "builds_in")).toBe(true);
-
- // env merge: pipeline env CI=true merges with step env
- const installNode = ir.graph.nodes[0];
- const buildNode = ir.graph.nodes[1];
- const testNode = ir.graph.nodes[2];
-
- const base = { DEBIAN_FRONTEND: "noninteractive", TERM: "dumb" };
- expect(installNode.env).toEqual({ ...base, CI: "true" });
- expect(buildNode.env).toEqual({ ...base, CI: "true", NODE_ENV: "production" });
- expect(testNode.env).toEqual({ ...base, CI: "true" });
-
- // ubuntu:24.04 is automatically stamped on root steps (no builds_in parent)
- const childIdxs = new Set(
- ir.graph.edges.filter((e) => e[2] === "builds_in").map((e) => e[1]),
- );
- const roots = ir.graph.nodes.filter((_n, i) => !childIdxs.has(i));
- expect(roots).toHaveLength(1);
- expect(roots[0].step.image).toBe("ubuntu:24.04");
- expect("image" in buildNode.step).toBe(false);
- expect("image" in testNode.step).toBe(false);
-
- // cache on install step
- expect(installNode.step.cache).toEqual({ policy: "forever", env_keys: [] });
-
- // timeout on test step
- expect(testNode.step.timeout_seconds).toBe(300);
- });
-});
-
-describe("wait barrier", () => {
- it("creates depends_on edges from pre-wait steps to post-wait steps", () => {
- const a = scratch().sh("step a", { label: "a" });
- const b = scratch().sh("step b", { label: "b" });
- const c = scratch().sh("step c", { label: "c" });
- const ir = pipeline([a, b, wait(), c]);
-
- const keys = ir.graph.nodes.map((n) => n.step.key);
- const idxA = keys.indexOf("a");
- const idxB = keys.indexOf("b");
- const idxC = keys.indexOf("c");
-
- const dependsOnEdges = ir.graph.edges.filter((e) => e[2] === "depends_on");
-
- // c depends_on both a and b
- expect(dependsOnEdges).toContainEqual([idxA, idxC, "depends_on"]);
- expect(dependsOnEdges).toContainEqual([idxB, idxC, "depends_on"]);
- expect(dependsOnEdges).toHaveLength(2);
- });
-});
-
-describe("target memoization", () => {
- it("shared target appears once in graph when used in two branches", () => {
- const nodeBase = target("node-base", () =>
- sh("apt-get install -y nodejs", {
- label: "node-base",
- cache: forever(),
- }),
- );
-
- const branchA = nodeBase().sh("npm run lint", { label: "lint" });
- const branchB = nodeBase().sh("npm test", { label: "test" });
-
- const ir = pipeline([branchA, branchB]);
-
- // node-base should appear exactly once (memoized)
- const keys = ir.graph.nodes.map((n) => n.step.key);
- expect(keys.filter((k) => k === "node-base")).toHaveLength(1);
-
- // total nodes: node-base, lint, test
- expect(ir.graph.nodes).toHaveLength(3);
-
- // both branches build from node-base
- const nodeBaseIdx = keys.indexOf("node-base");
- const lintIdx = keys.indexOf("lint");
- const testIdx = keys.indexOf("test");
-
- const buildsInEdges = ir.graph.edges.filter((e) => e[2] === "builds_in");
- expect(buildsInEdges).toContainEqual([nodeBaseIdx, lintIdx, "builds_in"]);
- expect(buildsInEdges).toContainEqual([nodeBaseIdx, testIdx, "builds_in"]);
- });
-});
-
-describe("envelope", () => {
- it("renders a complete envelope with triggers", () => {
- const def: PipelineDefinition = {
- slug: "my-ci",
- name: "My CI Pipeline",
- allowManual: false,
- triggers: [
- push({ branch: "main" }),
- pullRequest({ branches: "develop" }),
- ],
- pipeline: pipeline([sh("echo hello", { label: "hello" })]),
- };
-
- const json = renderEnvelope([def]);
- const parsed = JSON.parse(json);
-
- // schema_version
- expect(parsed.schema_version).toBe("1");
-
- // pipeline metadata
- expect(parsed.pipelines).toHaveLength(1);
- const p = parsed.pipelines[0];
- expect(p.slug).toBe("my-ci");
- expect(p.name).toBe("My CI Pipeline");
- expect(p.allow_manual).toBe(false);
-
- // triggers
- expect(p.triggers).toHaveLength(2);
- expect(p.triggers[0]).toEqual({ event: "push", branches: ["main"] });
- expect(p.triggers[1]).toEqual({
- event: "pull_request",
- branches: ["develop"],
- types: ["opened", "synchronize", "reopened"],
- });
- // definition is the IR
- expect(p.definition.version).toBe("0");
- expect(p.definition.graph.nodes).toHaveLength(1);
- });
-});
-
-describe("JSON snake_case output", () => {
- it("uses snake_case keys in IR, not camelCase", () => {
- const s = timeout(600, scratch().sh("make", {
- label: "build",
- cache: onChange("src/", "lib/"),
- }));
- const ir = pipeline([s]);
- const json = JSON.stringify(ir);
-
- // Root imageless step gets ubuntu:24.04 stamped on it
- expect(json).toContain('"image":"ubuntu:24.04"');
- // Must NOT contain the removed top-level default_image field
- expect(json).not.toContain('"default_image"');
-
- // Must contain snake_case keys
- expect(json).toContain('"timeout_seconds"');
- expect(json).toContain('"edge_property"');
- expect(json).toContain('"node_holes"');
- expect(json).toContain('"on_change"');
-
- // Must NOT contain camelCase equivalents
- expect(json).not.toContain('"defaultImage"');
- expect(json).not.toContain('"timeoutSeconds"');
- expect(json).not.toContain('"edgeProperty"');
- expect(json).not.toContain('"nodeHoles"');
- expect(json).not.toContain('"onChange"');
- });
-
- it("envelope uses snake_case keys", () => {
- const def: PipelineDefinition = {
- slug: "ci",
- allowManual: true,
- pipeline: pipeline([sh("echo")]),
- };
- const json = renderEnvelope([def]);
-
- expect(json).toContain('"schema_version"');
- expect(json).toContain('"allow_manual"');
- expect(json).not.toContain('"schemaVersion"');
- expect(json).not.toContain('"allowManual"');
- });
-});
-
-describe("public API completeness", () => {
- it("exports all expected symbols", () => {
- // Classes and functions are values
- expect(Step).toBeDefined();
- expect(typeof scratch).toBe("function");
- expect(typeof sh).toBe("function");
- expect(typeof wait).toBe("function");
- expect(typeof forever).toBe("function");
- expect(typeof ttl).toBe("function");
- expect(typeof onChange).toBe("function");
- expect(typeof compose).toBe("function");
- expect(typeof pipeline).toBe("function");
- expect(typeof target).toBe("function");
- expect(typeof clearTargetCache).toBe("function");
- expect(typeof renderEnvelope).toBe("function");
- expect(typeof push).toBe("function");
- expect(typeof pullRequest).toBe("function");
- // Trigger classes are exported as values for instanceof checks
- expect(PushTrigger).toBeDefined();
- expect(PullRequestTrigger).toBeDefined();
- const t = push({ branch: "main" });
- expect(t instanceof PushTrigger).toBe(true);
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/keygen.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/keygen.test.ts
deleted file mode 100644
index 9804a8a0..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/keygen.test.ts
+++ /dev/null
@@ -1,257 +0,0 @@
-import { createHash } from "node:crypto";
-import { mkdtempSync, writeFileSync, mkdirSync } from "node:fs";
-import { join } from "node:path";
-import { tmpdir } from "node:os";
-import { describe, expect, it, beforeEach } from "vitest";
-import { resolvePipelineCacheKeys, type CacheKeyOptions } from "../src/keygen.js";
-import { pipeline, type PipelineIR } from "../src/pipeline.js";
-import { sh } from "../src/step.js";
-import { forever, ttl, onChange } from "../src/cache.js";
-
-function sha256(s: string): string {
- return createHash("sha256").update(s, "utf8").digest("hex");
-}
-
-const NUL = "\0";
-
-function makeOpts(overrides?: Partial): CacheKeyOptions {
- return {
- pipelineOrg: "test-org",
- pipelineSlug: "ci",
- now: 1000000,
- basePath: "/tmp/test",
- env: {},
- ...overrides,
- };
-}
-
-describe("resolvePipelineCacheKeys", () => {
- it("adds cache.key to forever-cached steps", () => {
- const ir = pipeline([sh("echo hello", { label: "greet", cache: forever() })]);
- const opts = makeOpts();
-
- resolvePipelineCacheKeys(ir.graph, opts);
-
- const cache = ir.graph.nodes[0].step.cache as Record;
- expect(cache.key).toBeTypeOf("string");
- expect((cache.key as string).length).toBe(64);
- });
-
- it("produces deterministic keys", () => {
- const ir1 = pipeline([sh("echo hello", { label: "greet", cache: forever() })]);
- const ir2 = pipeline([sh("echo hello", { label: "greet", cache: forever() })]);
- const opts = makeOpts();
-
- resolvePipelineCacheKeys(ir1.graph, opts);
- resolvePipelineCacheKeys(ir2.graph, opts);
-
- const k1 = (ir1.graph.nodes[0].step.cache as Record).key;
- const k2 = (ir2.graph.nodes[0].step.cache as Record).key;
- expect(k1).toBe(k2);
- });
-
- it("different commands produce different keys", () => {
- const ir1 = pipeline([sh("echo a", { label: "a", cache: forever() })]);
- const ir2 = pipeline([sh("echo b", { label: "b", cache: forever() })]);
- const opts = makeOpts();
-
- resolvePipelineCacheKeys(ir1.graph, opts);
- resolvePipelineCacheKeys(ir2.graph, opts);
-
- const k1 = (ir1.graph.nodes[0].step.cache as Record).key;
- const k2 = (ir2.graph.nodes[0].step.cache as Record).key;
- expect(k1).not.toBe(k2);
- });
-
- it("different orgs produce different keys", () => {
- const ir1 = pipeline([sh("echo a", { label: "a", cache: forever() })]);
- const ir2 = pipeline([sh("echo a", { label: "a", cache: forever() })]);
-
- resolvePipelineCacheKeys(ir1.graph, makeOpts({ pipelineOrg: "org-a" }));
- resolvePipelineCacheKeys(ir2.graph, makeOpts({ pipelineOrg: "org-b" }));
-
- const k1 = (ir1.graph.nodes[0].step.cache as Record).key;
- const k2 = (ir2.graph.nodes[0].step.cache as Record).key;
- expect(k1).not.toBe(k2);
- });
-
- it("skips steps with no cache", () => {
- const ir = pipeline([sh("echo hello", { label: "greet" })]);
- resolvePipelineCacheKeys(ir.graph, makeOpts());
- expect(ir.graph.nodes[0].step.cache).toBeUndefined();
- });
-
- it("ttl bucket changes key", () => {
- const ir1 = pipeline([sh("apt-get", { label: "apt", cache: ttl(86400) })]);
- const ir2 = pipeline([sh("apt-get", { label: "apt", cache: ttl(86400) })]);
-
- resolvePipelineCacheKeys(ir1.graph, makeOpts({ now: 86400 * 10 }));
- resolvePipelineCacheKeys(ir2.graph, makeOpts({ now: 86400 * 11 }));
-
- const k1 = (ir1.graph.nodes[0].step.cache as Record).key;
- const k2 = (ir2.graph.nodes[0].step.cache as Record).key;
- expect(k1).not.toBe(k2);
- });
-
- it("ttl same bucket produces same key", () => {
- const ir1 = pipeline([sh("apt-get", { label: "apt", cache: ttl(86400) })]);
- const ir2 = pipeline([sh("apt-get", { label: "apt", cache: ttl(86400) })]);
-
- resolvePipelineCacheKeys(ir1.graph, makeOpts({ now: 86400 * 10 + 100 }));
- resolvePipelineCacheKeys(ir2.graph, makeOpts({ now: 86400 * 10 + 200 }));
-
- const k1 = (ir1.graph.nodes[0].step.cache as Record).key;
- const k2 = (ir2.graph.nodes[0].step.cache as Record).key;
- expect(k1).toBe(k2);
- });
-
- it("on_change hashes file contents", () => {
- const tmp = mkdtempSync(join(tmpdir(), "keygen-test-"));
- writeFileSync(join(tmp, "CMakeLists.txt"), "cmake_minimum_required(VERSION 3.20)");
-
- const ir = pipeline([
- sh("cmake ..", { label: "build", cache: onChange("./CMakeLists.txt") }),
- ]);
- resolvePipelineCacheKeys(ir.graph, makeOpts({ basePath: tmp }));
-
- const cache = ir.graph.nodes[0].step.cache as Record;
- expect(cache.key).toBeTypeOf("string");
- expect((cache.key as string).length).toBe(64);
- });
-
- it("on_change different file contents produce different keys", () => {
- const tmp1 = mkdtempSync(join(tmpdir(), "keygen-test-"));
- const tmp2 = mkdtempSync(join(tmpdir(), "keygen-test-"));
- writeFileSync(join(tmp1, "f.txt"), "version A");
- writeFileSync(join(tmp2, "f.txt"), "version B");
-
- const ir1 = pipeline([sh("cmd", { label: "x", cache: onChange("./f.txt") })]);
- const ir2 = pipeline([sh("cmd", { label: "x", cache: onChange("./f.txt") })]);
-
- resolvePipelineCacheKeys(ir1.graph, makeOpts({ basePath: tmp1 }));
- resolvePipelineCacheKeys(ir2.graph, makeOpts({ basePath: tmp2 }));
-
- const k1 = (ir1.graph.nodes[0].step.cache as Record).key;
- const k2 = (ir2.graph.nodes[0].step.cache as Record).key;
- expect(k1).not.toBe(k2);
- });
-
- it("forever key matches Python algorithm", () => {
- const ir = pipeline([sh("echo hi", { label: "test", cache: forever() })]);
- const opts = makeOpts({ pipelineOrg: "myorg", pipelineSlug: "myslug" });
-
- resolvePipelineCacheKeys(ir.graph, opts);
-
- const stepKey = ir.graph.nodes[0].step.key as string;
- const cmd = "echo hi";
- const policyRes = "forever-" + sha256(cmd + NUL + "");
- const expected = sha256(
- "myorg" + NUL + "myslug" + NUL + stepKey + NUL + "scratch" + NUL + policyRes,
- );
-
- const cache = ir.graph.nodes[0].step.cache as Record;
- expect(cache.key).toBe(expected);
- });
-
- it("child step uses parent resolved key", () => {
- const base = sh("apt-get install", { label: "apt", cache: forever() });
- const child = base.sh("make", { label: "build", cache: forever() });
-
- const ir = pipeline([child]);
- resolvePipelineCacheKeys(ir.graph, makeOpts());
-
- const parentCache = ir.graph.nodes[0].step.cache as Record;
- const childCache = ir.graph.nodes[1].step.cache as Record;
- expect(parentCache.key).toBeTypeOf("string");
- expect(childCache.key).toBeTypeOf("string");
- expect(parentCache.key).not.toBe(childCache.key);
- });
-
- it("golden hash: cross-SDK reference pipeline", () => {
- const graph: PipelineIR["graph"] = {
- nodes: [
- {
- step: {
- key: "build",
- cmd: "make build",
- cache: { policy: "forever", env_keys: [] },
- },
- env: {},
- },
- ],
- node_holes: [],
- edge_property: "directed",
- edges: [],
- };
-
- const opts: CacheKeyOptions = {
- pipelineOrg: "acme",
- pipelineSlug: "ci",
- now: 1000000,
- basePath: "/nonexistent",
- env: {},
- };
-
- resolvePipelineCacheKeys(graph, opts);
-
- const policyRes = "forever-" + sha256("make build" + NUL);
- const expected = sha256(
- "acme" + NUL + "ci" + NUL + "build" + NUL + "scratch" + NUL + policyRes,
- );
-
- const cache = graph.nodes[0].step.cache as Record;
- expect(cache.key).toBe(expected);
- });
-
- it("golden hash: cross-SDK chained pipeline", () => {
- const graph: PipelineIR["graph"] = {
- nodes: [
- {
- step: {
- key: "setup",
- cmd: "apt-get update && apt-get install -y gcc",
- cache: { policy: "forever", env_keys: [] },
- },
- env: {},
- },
- {
- step: {
- key: "compile",
- cmd: "gcc -o main main.c",
- cache: { policy: "forever", env_keys: [] },
- },
- env: {},
- },
- ],
- node_holes: [],
- edge_property: "directed",
- edges: [[0, 1, "builds_in"]],
- };
-
- const opts: CacheKeyOptions = {
- pipelineOrg: "acme",
- pipelineSlug: "ci",
- now: 1000000,
- basePath: "/nonexistent",
- env: {},
- };
-
- resolvePipelineCacheKeys(graph, opts);
-
- const parentPolicyRes =
- "forever-" + sha256("apt-get update && apt-get install -y gcc" + NUL);
- const parentKey = sha256(
- "acme" + NUL + "ci" + NUL + "setup" + NUL + "scratch" + NUL + parentPolicyRes,
- );
-
- const childPolicyRes = "forever-" + sha256("gcc -o main main.c" + NUL);
- const childKey = sha256(
- "acme" + NUL + "ci" + NUL + "compile" + NUL + parentKey + NUL + childPolicyRes,
- );
-
- const parentCache = graph.nodes[0].step.cache as Record;
- const childCache = graph.nodes[1].step.cache as Record;
- expect(parentCache.key).toBe(parentKey);
- expect(childCache.key).toBe(childKey);
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/keys.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/keys.test.ts
deleted file mode 100644
index 593468fd..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/keys.test.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { slugifyLabel, hashKey, resolveKeys } from "../src/keys.js";
-import { scratch, sh } from "../src/step.js";
-
-describe("slugifyLabel", () => {
- it("lowercases and replaces non-alnum with dashes", () => {
- expect(slugifyLabel("Hello World")).toBe("hello-world");
- });
-
- it("strips emoji shortcodes", () => {
- expect(slugifyLabel(":rust: build")).toBe("build");
- });
-
- it("trims leading/trailing dashes", () => {
- expect(slugifyLabel("--hello--")).toBe("hello");
- });
-
- it("returns empty string for non-ASCII-only labels", () => {
- expect(slugifyLabel("构建")).toBe("");
- });
-
- it("handles mixed emoji and text", () => {
- expect(slugifyLabel(":node: deps install")).toBe("deps-install");
- });
-});
-
-describe("hashKey", () => {
- it("returns a 12-char hex string", () => {
- const key = hashKey("parent", "echo hello", 0);
- expect(key).toMatch(/^[0-9a-f]{12}$/);
- });
-
- it("is deterministic", () => {
- expect(hashKey("p", "cmd", 1)).toBe(hashKey("p", "cmd", 1));
- });
-
- it("differs for different inputs", () => {
- expect(hashKey("p", "cmd1", 0)).not.toBe(hashKey("p", "cmd2", 0));
- });
-});
-
-describe("resolveKeys", () => {
- it("uses slugified label when unique", () => {
- const a = scratch().sh("install", { label: "install" });
- const b = a.sh("build", { label: "build" });
- const keys = resolveKeys([a, b]);
- expect(keys.get(a._id)).toBe("install");
- expect(keys.get(b._id)).toBe("build");
- });
-
- it("falls back to hash when label slugs collide", () => {
- const a = scratch().sh("cmd a", { label: "test" });
- const b = scratch().sh("cmd b", { label: "test" });
- const keys = resolveKeys([a, b]);
- expect(keys.get(a._id)).toMatch(/^[0-9a-f]{12}$/);
- expect(keys.get(b._id)).toMatch(/^[0-9a-f]{12}$/);
- expect(keys.get(a._id)).not.toBe(keys.get(b._id));
- });
-
- it("explicit key override wins over label", () => {
- const a = scratch().sh("echo", { label: "hello", key: "my-key" });
- const keys = resolveKeys([a]);
- expect(keys.get(a._id)).toBe("my-key");
- });
-
- it("explicit override reserves slug, colliding label falls back to hash", () => {
- const a = scratch().sh("cmd a", { label: "build", key: "build" });
- const b = scratch().sh("cmd b", { label: "build" });
- const keys = resolveKeys([a, b]);
- expect(keys.get(a._id)).toBe("build");
- expect(keys.get(b._id)).toMatch(/^[0-9a-f]{12}$/);
- });
-
- it("falls back to hash when label is empty after slugify", () => {
- const a = scratch().sh("echo", { label: "构建" });
- const keys = resolveKeys([a]);
- expect(keys.get(a._id)).toMatch(/^[0-9a-f]{12}$/);
- });
-
- it("uses parent key for hash computation", () => {
- const parent = scratch().sh("install", { label: "install" });
- const child = parent.sh("build");
- const keys = resolveKeys([parent, child]);
- expect(keys.get(parent._id)).toBe("install");
- const expected = hashKey("install", "build", 1);
- expect(keys.get(child._id)).toBe(expected);
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/pipeline.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/pipeline.test.ts
deleted file mode 100644
index 81a604ff..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/pipeline.test.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { pipeline } from "../src/pipeline.js";
-import { scratch, sh, wait, timeout } from "../src/step.js";
-import { forever, onChange } from "../src/cache.js";
-
-function stepKeys(ir: any): string[] {
- return ir.graph.nodes.map((n: any) => n.step.key);
-}
-
-function dependsOnEdges(ir: any): [number, number][] {
- return ir.graph.edges
- .filter((e: any) => e[2] === "depends_on")
- .map((e: any) => [e[0], e[1]]);
-}
-
-function parentKeyMap(ir: any): Record {
- const keyByIdx: Record = {};
- for (let i = 0; i < ir.graph.nodes.length; i++) {
- keyByIdx[i] = ir.graph.nodes[i].step.key;
- }
- const result: Record = {};
- for (const n of ir.graph.nodes) {
- result[n.step.key] = null;
- }
- for (const [src, dst, kind] of ir.graph.edges) {
- if (kind === "builds_in") {
- result[keyByIdx[dst]] = keyByIdx[src];
- }
- }
- return result;
-}
-
-describe("pipeline", () => {
- it("returns v0 IR dict", () => {
- const p = pipeline([scratch().sh("echo", { label: "echo" })]);
- expect(p.version).toBe("0");
- expect(p.graph).toBeDefined();
- expect(p.graph.nodes).toHaveLength(1);
- });
-
- it("rejects no leaves", () => {
- expect(() => pipeline([])).toThrow("at least one leaf");
- });
-
-});
-
-describe("lowering: single chain", () => {
- it("emits nodes in parent-first order with builds_in edges", () => {
- const a = scratch().sh("step a", { label: "a" });
- const b = a.sh("step b", { label: "b" });
- const c = b.sh("step c", { label: "c" });
- const ir = pipeline([c]);
- expect(stepKeys(ir)).toEqual(["a", "b", "c"]);
- const parents = parentKeyMap(ir);
- expect(parents.a).toBeNull();
- expect(parents.b).toBe("a");
- expect(parents.c).toBe("b");
- });
-});
-
-describe("lowering: fork", () => {
- it("fork nodes are not emitted, children inherit grandparent", () => {
- const base = scratch().sh("install", { label: "install" });
- const branch = base.fork({ label: "branch-a" });
- const leaf = branch.sh("test", { label: "test" });
- const ir = pipeline([leaf]);
- expect(stepKeys(ir)).toEqual(["install", "test"]);
- const parents = parentKeyMap(ir);
- expect(parents.install).toBeNull();
- expect(parents.test).toBe("install");
- });
-
- it("two branches share parent", () => {
- const base = scratch().sh("install", { label: "install" });
- const a = base.fork().sh("test-a", { label: "test-a" });
- const b = base.fork().sh("test-b", { label: "test-b" });
- const ir = pipeline([a, b]);
- const parents = parentKeyMap(ir);
- expect(parents["test-a"]).toBe("install");
- expect(parents["test-b"]).toBe("install");
- });
-});
-
-describe("lowering: wait", () => {
- it("emits depends_on edges from pre-wait to post-wait steps", () => {
- const a = scratch().sh("a", { label: "a" });
- const b = scratch().sh("b", { label: "b" });
- const c = scratch().sh("c", { label: "c" });
- const ir = pipeline([a, b, wait(), c]);
- const keys = stepKeys(ir);
- const idxA = keys.indexOf("a");
- const idxB = keys.indexOf("b");
- const idxC = keys.indexOf("c");
- const deps = dependsOnEdges(ir);
- expect(deps).toContainEqual([idxA, idxC]);
- expect(deps).toContainEqual([idxB, idxC]);
- });
-});
-
-describe("lowering: env merge", () => {
- it("merges pipeline env with per-step env", () => {
- const s = scratch().sh("make", { env: { STEP: "1" } });
- const ir = pipeline([s], { env: { PIPE: "true" } });
- expect(ir.graph.nodes[0].env).toEqual({
- DEBIAN_FRONTEND: "noninteractive",
- TERM: "dumb",
- PIPE: "true",
- STEP: "1",
- });
- });
-
- it("step env overrides pipeline env", () => {
- const s = scratch().sh("make", { env: { X: "step" } });
- const ir = pipeline([s], { env: { X: "pipe" } });
- expect(ir.graph.nodes[0].env.X).toBe("step");
- });
-});
-
-describe("lowering: optional fields", () => {
- it("omits label/timeout/cache when unset", () => {
- const s = scratch().sh("make");
- const ir = pipeline([s]);
- const step = ir.graph.nodes[0].step;
- expect(step.key).toBeDefined();
- expect(step.cmd).toBe("make");
- expect("label" in step).toBe(false);
- expect("timeout_seconds" in step).toBe(false);
- expect("cache" in step).toBe(false);
- });
-
- it("includes label/timeout/cache when set", () => {
- const s = timeout(600, scratch().sh("make", {
- label: "build",
- cache: forever(),
- }));
- const ir = pipeline([s]);
- const step = ir.graph.nodes[0].step;
- expect(step.label).toBe("build");
- expect(step.timeout_seconds).toBe(600);
- expect(step.cache).toEqual({ policy: "forever", env_keys: [] });
- });
-});
-
-describe("lowering: cache serialization", () => {
- it("serializes forever cache", () => {
- const s = sh("echo", { cache: forever({ envKeys: ["CI"] }) });
- const ir = pipeline([s]);
- expect(ir.graph.nodes[0].step.cache).toEqual({
- policy: "forever",
- env_keys: ["CI"],
- });
- });
-
- it("serializes onChange cache", () => {
- const s = sh("echo", { cache: onChange("src/", "lib/") });
- const ir = pipeline([s]);
- expect(ir.graph.nodes[0].step.cache).toEqual({
- policy: "on_change",
- paths: ["src/", "lib/"],
- });
- });
-});
-
-describe("lowering: dedup", () => {
- it("shared ancestor appears once when reachable from multiple leaves", () => {
- const base = scratch().sh("install", { label: "install" });
- const a = base.sh("a", { label: "a" });
- const b = base.sh("b", { label: "b" });
- const ir = pipeline([a, b]);
- const keys = stepKeys(ir);
- expect(keys.filter((k) => k === "install")).toHaveLength(1);
- });
-});
-
-describe("lowering: default image", () => {
- it("stamps ubuntu:24.04 on an imageless root", () => {
- const s = scratch().sh("echo hi", { label: "a" });
- const ir = pipeline([s]);
- expect(ir.graph.nodes[0].step.image).toBe("ubuntu:24.04");
- });
-
- it("preserves an explicit root image", () => {
- const s = scratch({ image: "alpine:3.20" }).sh("echo hi", { label: "a" });
- const ir = pipeline([s]);
- expect(ir.graph.nodes[0].step.image).toBe("alpine:3.20");
- });
-
- it("leaves child steps imageless", () => {
- const root = scratch().sh("echo p", { label: "p" });
- const child = root.sh("echo c", { label: "c" });
- const ir = pipeline([child]);
- const byKey = Object.fromEntries(
- ir.graph.nodes.map((n) => [n.step.key as string, n.step]),
- );
- expect("image" in byKey["c"]).toBe(false);
- });
-
- it("emits no top-level default_image key", () => {
- const ir = pipeline([scratch().sh("echo hi", { label: "a" })]);
- expect("default_image" in ir).toBe(false);
- });
-});
-
-describe("lowering: graph structure", () => {
- it("emits petgraph-serde structure", () => {
- const s = scratch().sh("echo", { label: "hello" });
- const ir = pipeline([s]);
- expect(ir.graph.node_holes).toEqual([]);
- expect(ir.graph.edge_property).toBe("directed");
- });
-});
-
-describe("pipeline: timeout", () => {
- it("emits a top-level timeout_seconds when set", () => {
- const ir = pipeline([sh("x")], { timeout: "30m" });
- expect(ir.timeout_seconds).toBe(1800);
- });
-
- it("omits timeout_seconds when unset", () => {
- const ir = pipeline([sh("x")]);
- expect(ir.timeout_seconds).toBeUndefined();
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/setup.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/setup.test.ts
deleted file mode 100644
index 709ef5b2..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/setup.test.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { pipeline } from "@harmont/hm";
-import * as hm from "@harmont/hm/toolchains";
-
-function cmdsOf(leaf: any): string[] {
- const ir = pipeline([leaf]);
- return ir.graph.nodes.map((n: any) => n.step.cmd).filter(Boolean);
-}
-
-describe("toolchain .setup()", () => {
- it("advances the install chain and is immutable", () => {
- const proj = hm.elixir({ path: "." });
- const before = proj.install();
- const advanced = proj.setup("echo __SETUP_MARKER__");
- expect(advanced).not.toBe(proj);
- expect(advanced.install()).not.toBe(before);
- const cmds = cmdsOf(advanced.install());
- expect(cmds.some((c) => c.includes("__SETUP_MARKER__"))).toBe(true);
- });
-
- it("is chainable", () => {
- const proj = hm.elixir({ path: "." }).setup("echo __ONE__").setup("echo __TWO__");
- const cmds = cmdsOf(proj.install());
- expect(cmds.some((c) => c.includes("__ONE__"))).toBe(true);
- expect(cmds.some((c) => c.includes("__TWO__"))).toBe(true);
- });
-});
-
-const FACTORIES: Array<[string, () => any]> = [
- ["elixir", () => hm.elixir({ path: "." })],
- ["python", () => hm.python({ path: "." })],
- ["go", () => hm.go({ path: "." })],
- ["js", () => hm.js.project({ path: "." })],
- ["zigProject", () => hm.zig({ path: "." })],
- ["zigToolchain", () => hm.zig()],
- ["rustToolchain", () => hm.rust.toolchain()],
- ["cmakeToolchain", () => hm.cmake()],
-];
-
-describe.each(FACTORIES)("%s .setup()", (_label, make) => {
- it("advances the chain (renders the setup cmd)", () => {
- const advanced = make().setup("echo __MARK__");
- const ir = pipeline([advanced.install()]);
- const cmds = ir.graph.nodes.map((n: any) => n.step.cmd).filter(Boolean);
- expect(cmds.some((c: string) => c.includes("__MARK__"))).toBe(true);
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/step.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/step.test.ts
deleted file mode 100644
index 6cb3ba06..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/step.test.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { scratch, sh, wait, Step, timeout } from "../src/step.js";
-
-describe("scratch", () => {
- it("creates a root step with no cmd or parent", () => {
- const s = scratch();
- expect(s).toBeInstanceOf(Step);
- expect(s._cmd).toBeNull();
- expect(s._parent).toBeNull();
- expect(s._isWait).toBe(false);
- });
-});
-
-describe("sh", () => {
- it("creates a step with cmd and implicit scratch parent", () => {
- const s = sh("echo hello");
- expect(s._cmd).toBe("echo hello");
- expect(s._parent).not.toBeNull();
- expect(s._parent!._cmd).toBeNull();
- });
-
- it("passes options through", () => {
- const s = timeout(600, sh("make", {
- label: "build",
- env: { CI: "true" },
- image: "ubuntu:24.04",
- key: "my-key",
- }));
- expect(s._label).toBe("build");
- expect(s._timeoutSeconds).toBe(600);
- expect(s._env).toEqual({ CI: "true" });
- expect(s._image).toBe("ubuntu:24.04");
- expect(s._keyOverride).toBe("my-key");
- });
-
- it("prepends cd when cwd is set", () => {
- const s = sh("npm test", { cwd: "packages/app" });
- expect(s._cmd).toBe("cd packages/app && npm test");
- });
-
- it("rejects empty cwd", () => {
- expect(() => sh("echo", { cwd: "" })).toThrow("cwd must be a non-empty path");
- });
-});
-
-describe("Step.sh", () => {
- it("chains a child step with parent pointer", () => {
- const parent = sh("install");
- const child = parent.sh("build");
- expect(child._cmd).toBe("build");
- expect(child._parent).toBe(parent);
- });
-
- it("inherits image from scratch parent", () => {
- const base = scratch({ image: "alpine:3.20" });
- const child = base.sh("echo");
- expect(child._image).toBe("alpine:3.20");
- });
-
- it("does not inherit image from command parent", () => {
- const parent = sh("install", { image: "ubuntu:24.04" });
- const child = parent.sh("build");
- expect(child._image).toBeUndefined();
- });
-
- it("explicit image overrides inherited image", () => {
- const base = scratch({ image: "alpine:3.20" });
- const child = base.sh("echo", { image: "ubuntu:24.04" });
- expect(child._image).toBe("ubuntu:24.04");
- });
-});
-
-describe("Step.fork", () => {
- it("creates a cmd-less step with parent pointer", () => {
- const parent = sh("install");
- const branch = parent.fork({ label: "branch-a" });
- expect(branch._cmd).toBeNull();
- expect(branch._parent).toBe(parent);
- expect(branch._label).toBe("branch-a");
- });
-});
-
-describe("wait", () => {
- it("creates a wait step", () => {
- const w = wait();
- expect(w._isWait).toBe(true);
- expect(w._continueOnFailure).toBe(false);
- });
-
- it("accepts continueOnFailure", () => {
- const w = wait({ continueOnFailure: true });
- expect(w._continueOnFailure).toBe(true);
- });
-});
-
-describe("step identity", () => {
- it("each step gets a unique id", () => {
- const a = sh("a");
- const b = sh("b");
- expect(a._id).not.toBe(b._id);
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/target.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/target.test.ts
deleted file mode 100644
index 5460ee6f..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/target.test.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { describe, expect, it, beforeEach } from "vitest";
-import { target, clearTargetCache } from "../src/target.js";
-import { sh } from "../src/step.js";
-import { forever } from "../src/cache.js";
-
-beforeEach(() => {
- clearTargetCache();
-});
-
-describe("target", () => {
- it("returns a factory function", () => {
- const nodeBase = target("node-base", () => {
- return sh("apt-get install -y nodejs", { cache: forever() });
- });
- expect(typeof nodeBase).toBe("function");
- });
-
- it("factory returns the step", () => {
- const nodeBase = target("node-base", () => {
- return sh("apt-get install -y nodejs");
- });
- const step = nodeBase();
- expect(step._cmd).toBe("apt-get install -y nodejs");
- });
-
- it("memoizes return value", () => {
- let callCount = 0;
- const nodeBase = target("node-base", () => {
- callCount++;
- return sh("install");
- });
- const a = nodeBase();
- const b = nodeBase();
- expect(a).toBe(b);
- expect(callCount).toBe(1);
- });
-
- it("clearTargetCache resets memoization", () => {
- let callCount = 0;
- const nodeBase = target("node-base", () => {
- callCount++;
- return sh("install");
- });
- nodeBase();
- clearTargetCache();
- nodeBase();
- expect(callCount).toBe(2);
- });
-
- it("different targets are independent", () => {
- const a = target("a", () => sh("cmd-a"));
- const b = target("b", () => sh("cmd-b"));
- expect(a()._cmd).toBe("cmd-a");
- expect(b()._cmd).toBe("cmd-b");
- });
-
- it("target can build on another target", () => {
- const base = target("base", () => sh("install base"));
- const app = target("app", () => base().sh("install app"));
- const step = app();
- expect(step._cmd).toBe("install app");
- expect(step._parent!._cmd).toBe("install base");
- });
-
- it("memoizes non-Step values (generic)", () => {
- const factory = target("my-obj", () => ({ value: Math.random() }));
- const a = factory();
- const b = factory();
- expect(a).toBe(b);
- expect(a.value).toBe(b.value);
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/timeout.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/timeout.test.ts
deleted file mode 100644
index 6fa36e0c..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/timeout.test.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-// tests/timeout.test.ts
-import { describe, expect, it } from "vitest";
-
-import { sh, timeout, wait } from "../src/index.js";
-
-describe("timeout", () => {
- it("sets the timeout in seconds on a step", () => {
- const step = timeout("30s", sh("echo foo"));
- expect(step._timeoutSeconds).toBe(30);
- expect(step._cmd).toBe("echo foo");
- });
-
- it("accepts a number of seconds", () => {
- expect(timeout(45, sh("x"))._timeoutSeconds).toBe(45);
- });
-
- it("does not mutate the original and last-wins on re-wrap", () => {
- const base = sh("x");
- const wrapped = timeout("5m", base);
- expect(base._timeoutSeconds).toBeUndefined();
- expect(timeout("1m", wrapped)._timeoutSeconds).toBe(60);
- });
-
- it("rejects wrapping a wait barrier", () => {
- expect(() => timeout("30s", wait())).toThrow(/wait/);
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/cargo.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/cargo.test.ts
deleted file mode 100644
index 99929ae2..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/cargo.test.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { cargoFlags, shQuote } from "../../src/toolchains/cargo.js";
-
-describe("shQuote", () => {
- it("leaves simple identifiers alone", () => {
- expect(shQuote("harmont-core")).toBe("harmont-core");
- });
- it("quotes values with shell metacharacters", () => {
- expect(shQuote("a; rm -rf /")).toBe("'a; rm -rf /'");
- });
- it("escapes embedded single quotes like shlex.quote", () => {
- expect(shQuote("a'b")).toBe("'a'\"'\"'b'");
- });
- it("quotes the empty string", () => {
- expect(shQuote("")).toBe("''");
- });
-});
-
-describe("cargoFlags", () => {
- it("emits only --locked for empty opts", () => {
- expect(cargoFlags({})).toBe(" --locked");
- });
- it("locked can be disabled", () => {
- expect(cargoFlags({ locked: false })).toBe("");
- });
- it("workspace scope", () => {
- expect(cargoFlags({ workspace: true })).toBe(" --workspace --locked");
- });
- it("packages take precedence and quote values", () => {
- expect(cargoFlags({ workspace: true, packages: ["a", "b c"] })).toBe(
- " -p a -p 'b c' --locked",
- );
- });
- it("exclude pairs with workspace", () => {
- expect(cargoFlags({ workspace: true, exclude: ["b"] })).toBe(
- " --workspace --exclude b --locked",
- );
- });
- it("exclude without workspace throws", () => {
- expect(() => cargoFlags({ exclude: ["b"] })).toThrow("workspace");
- });
- it("exclude with packages throws", () => {
- expect(() => cargoFlags({ packages: ["a"], exclude: ["b"] })).toThrow("exclude");
- });
- it("all-features", () => {
- expect(cargoFlags({ allFeatures: true })).toBe(" --all-features --locked");
- });
- it("features joined comma", () => {
- expect(cargoFlags({ features: ["x", "y"] })).toBe(" --features x,y --locked");
- });
- it("no-default-features with features", () => {
- expect(cargoFlags({ noDefaultFeatures: true, features: ["x"] })).toBe(
- " --no-default-features --features x --locked",
- );
- });
- it("full token order", () => {
- expect(
- cargoFlags({
- packages: ["core"],
- allTargets: true,
- noDefaultFeatures: true,
- features: ["a", "b"],
- target: "x86_64-unknown-linux-gnu",
- profile: "ci",
- flags: ["--keep-going"],
- }),
- ).toBe(
- " -p core --all-targets --no-default-features --features a,b" +
- " --target x86_64-unknown-linux-gnu --profile ci --locked --keep-going",
- );
- });
- it("flags are verbatim", () => {
- expect(cargoFlags({ locked: false, flags: ["--features=a b"] })).toBe(
- " --features=a b",
- );
- });
- it("throws on all-features + features conflict", () => {
- expect(() => cargoFlags({ allFeatures: true, features: ["x"] })).toThrow(
- "all-features",
- );
- });
- it("throws on release + profile conflict", () => {
- expect(() => cargoFlags({ release: true, profile: "ci" })).toThrow("profile");
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/cmake.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/cmake.test.ts
deleted file mode 100644
index 1143fde1..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/cmake.test.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { cmake, CMakeToolchain, CMakeProject } from "../../src/toolchains/cmake.js";
-import { pipeline } from "../../src/pipeline.js";
-
-describe("cmake factory", () => {
- it("returns a CMakeToolchain when no path is given", () => {
- const tc = cmake();
- expect(tc).toBeInstanceOf(CMakeToolchain);
- expect(tc.install()._cmd).toContain("cmake --version");
- expect(tc.install()._cmd).toContain("ninja --version");
- });
-
- it("returns a CMakeProject when path is given", () => {
- const proj = cmake({ path: "." });
- expect(proj).toBeInstanceOf(CMakeProject);
- expect(proj.path).toBe(".");
- });
-
- it("rejects invalid compiler", () => {
- expect(() => cmake({ compiler: "msvc" })).toThrow("invalid compiler");
- });
-
- it("rejects invalid generator", () => {
- expect(() => cmake({ generator: "borland" as any })).toThrow(
- "invalid generator",
- );
- });
-
- it("accepts gcc compiler", () => {
- const tc = cmake({ compiler: "gcc-14" });
- expect(tc.install()._cmd).toContain("gcc-14 --version");
- });
-
- it("accepts clang compiler", () => {
- const tc = cmake({ compiler: "clang-18" });
- expect(tc.install()._cmd).toContain("clang-18 --version");
- });
-
- it("disables ccache", () => {
- const tc = cmake({ ccache: false });
- expect(tc.install()._cmd).not.toContain("ccache");
- });
-});
-
-describe("cmake toolchain.project()", () => {
- it("creates a CMakeProject from a toolchain", () => {
- const tc = cmake();
- const proj = tc.project({ path: "lib" });
- expect(proj).toBeInstanceOf(CMakeProject);
- expect(proj.path).toBe("lib");
- });
-});
-
-describe("cmake project actions", () => {
- it("build returns the warmup step", () => {
- const proj = cmake({ path: "." });
- expect(proj.build()._cmd).toContain("cmake --build");
- expect(proj.build()._label).toBe(":cmake: build");
- });
-
- it("test runs ctest off built", () => {
- const proj = cmake({ path: "." });
- const step = proj.test();
- expect(step._cmd).toContain("ctest --test-dir ./build --output-on-failure");
- expect(step._label).toBe(":cmake: test");
- });
-
- it("install runs cmake --install off built", () => {
- const proj = cmake({ path: "." });
- const step = proj.install();
- expect(step._cmd).toContain("cmake --install ./build");
- expect(step._label).toBe(":cmake: install");
- });
-
- it("fmt runs clang-format off toolchain.install()", () => {
- const proj = cmake({ path: "." });
- const step = proj.fmt();
- expect(step._cmd).toContain("clang-format --dry-run --Werror");
- expect(step._label).toBe(":cmake: fmt");
- // fmt branches off toolchain.install(), not built
- expect(step._parent).toBe(proj.toolchain.install());
- });
-
- it("lint runs run-clang-tidy off built", () => {
- const proj = cmake({ path: "." });
- const step = proj.lint();
- expect(step._cmd).toContain("run-clang-tidy -p build");
- expect(step._label).toBe(":cmake: lint");
- });
-
- it("package runs cpack off built", () => {
- const proj = cmake({ path: "." });
- const step = proj.package();
- expect(step._cmd).toContain("cpack");
- expect(step._label).toBe(":cmake: package");
- });
-});
-
-describe("cmake with preset", () => {
- it("configure uses --preset when preset is given", () => {
- const proj = cmake({ path: ".", preset: "release" });
- expect(proj.build()._cmd).toContain("cmake --preset release");
- expect(proj.build()._cmd).not.toContain("-DCMAKE_BUILD_TYPE");
- });
-});
-
-describe("cmake with vcpkg", () => {
- it("inserts a vcpkg step", () => {
- const proj = cmake({ path: ".", deps: "vcpkg" });
- // The built step's parent should be the vcpkg step (warmup parent)
- const builtStep = proj.build();
- expect(builtStep._parent?._cmd).toContain("vcpkg install");
- expect(builtStep._parent?._label).toBe(":cmake: vcpkg");
- });
-});
-
-describe("cmake in pipeline", () => {
- it("produces valid IR", () => {
- const proj = cmake({ path: "." });
- const ir = pipeline([proj.build(), proj.test()]);
- expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(3);
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/detect.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/detect.test.ts
deleted file mode 100644
index 6e95da2e..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/detect.test.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-import { describe, expect, it, beforeEach, afterEach } from "vitest";
-import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
-import { join } from "node:path";
-import { tmpdir } from "node:os";
-import {
- detectFromPackageJson,
- detectFromLockfiles,
- detect,
-} from "../../src/toolchains/detect.js";
-
-// ---------------------------------------------------------------------------
-// detectFromPackageJson
-// ---------------------------------------------------------------------------
-
-describe("detectFromPackageJson", () => {
- it("returns empty for empty object", () => {
- expect(detectFromPackageJson({})).toEqual({});
- });
-
- it("detects runtime=node from engines.node", () => {
- expect(detectFromPackageJson({ engines: { node: ">=18" } })).toEqual({
- runtime: "node",
- });
- });
-
- it("detects runtime=bun and pm=bun from engines.bun", () => {
- expect(detectFromPackageJson({ engines: { bun: ">=1.0" } })).toEqual({
- runtime: "bun",
- pm: "bun",
- });
- });
-
- it("detects runtime=deno from engines.deno", () => {
- expect(detectFromPackageJson({ engines: { deno: ">=2.0" } })).toEqual({
- runtime: "deno",
- });
- });
-
- it("detects pm=pnpm from packageManager field", () => {
- expect(
- detectFromPackageJson({ packageManager: "pnpm@8.15.4" }),
- ).toEqual({ pm: "pnpm", pmVersion: "8.15.4" });
- });
-
- it("detects pm=bun from packageManager field", () => {
- expect(detectFromPackageJson({ packageManager: "bun@1.1.0" })).toEqual({
- pm: "bun",
- pmVersion: "1.1.0",
- });
- });
-
- it("detects pm=npm from packageManager field", () => {
- expect(
- detectFromPackageJson({ packageManager: "npm@10.2.4" }),
- ).toEqual({ pm: "npm", pmVersion: "10.2.4" });
- });
-
- it("detects yarn-classic from packageManager yarn@1.x", () => {
- expect(
- detectFromPackageJson({ packageManager: "yarn@1.22.22" }),
- ).toEqual({ pm: "yarn-classic", pmVersion: "1.22.22" });
- });
-
- it("detects yarn-berry from packageManager yarn@4.x", () => {
- expect(
- detectFromPackageJson({ packageManager: "yarn@4.0.0" }),
- ).toEqual({ pm: "yarn-berry", pmVersion: "4.0.0" });
- });
-
- it("engines.bun overrides packageManager for pm", () => {
- expect(
- detectFromPackageJson({
- engines: { bun: ">=1.0" },
- packageManager: "pnpm@8",
- }),
- ).toEqual({ runtime: "bun", pm: "bun" });
- });
-
- it("engines.node + packageManager=pnpm both contribute", () => {
- expect(
- detectFromPackageJson({
- engines: { node: ">=18" },
- packageManager: "pnpm@8",
- }),
- ).toEqual({ runtime: "node", pm: "pnpm", pmVersion: "8" });
- });
-
- it("omits pmVersion when packageManager has no @version", () => {
- expect(
- detectFromPackageJson({ packageManager: "pnpm" }),
- ).toEqual({ pm: "pnpm" });
- });
-});
-
-// ---------------------------------------------------------------------------
-// detectFromLockfiles
-// ---------------------------------------------------------------------------
-
-describe("detectFromLockfiles", () => {
- it("returns empty for no files", () => {
- expect(detectFromLockfiles([])).toEqual({});
- });
-
- it("detects bun from bun.lock", () => {
- expect(detectFromLockfiles(["bun.lock"])).toEqual({
- pm: "bun",
- runtime: "bun",
- });
- });
-
- it("detects bun from bun.lockb (legacy binary format)", () => {
- expect(detectFromLockfiles(["bun.lockb"])).toEqual({
- pm: "bun",
- runtime: "bun",
- });
- });
-
- it("detects pnpm from pnpm-lock.yaml", () => {
- expect(detectFromLockfiles(["pnpm-lock.yaml"])).toEqual({ pm: "pnpm" });
- });
-
- it("detects deno from deno.lock", () => {
- expect(detectFromLockfiles(["deno.lock"])).toEqual({ runtime: "deno" });
- });
-
- it("detects npm from package-lock.json", () => {
- expect(detectFromLockfiles(["package-lock.json"])).toEqual({
- pm: "npm",
- });
- });
-
- it("detects yarn-classic from yarn.lock", () => {
- expect(detectFromLockfiles(["yarn.lock"])).toEqual({ pm: "yarn-classic" });
- });
-
- it("bun.lock takes priority over package-lock.json", () => {
- expect(
- detectFromLockfiles(["package-lock.json", "bun.lock"]),
- ).toEqual({ pm: "bun", runtime: "bun" });
- });
-});
-
-// ---------------------------------------------------------------------------
-// detect (filesystem integration)
-// ---------------------------------------------------------------------------
-
-describe("detect", () => {
- let tmp: string;
-
- beforeEach(() => {
- tmp = mkdtempSync(join(tmpdir(), "hm-detect-"));
- });
-
- afterEach(() => {
- rmSync(tmp, { recursive: true, force: true });
- });
-
- it("returns empty for directory with no package.json or lockfiles", () => {
- expect(detect(tmp)).toEqual({});
- });
-
- it("detects from package.json engines", () => {
- writeFileSync(
- join(tmp, "package.json"),
- JSON.stringify({ engines: { bun: ">=1.0" } }),
- );
- expect(detect(tmp)).toEqual({ runtime: "bun", pm: "bun" });
- });
-
- it("detects from lockfile", () => {
- writeFileSync(join(tmp, "pnpm-lock.yaml"), "");
- expect(detect(tmp)).toEqual({ pm: "pnpm" });
- });
-
- it("package.json pm takes priority over lockfile pm", () => {
- writeFileSync(
- join(tmp, "package.json"),
- JSON.stringify({ packageManager: "pnpm@8" }),
- );
- writeFileSync(join(tmp, "bun.lock"), "");
- const result = detect(tmp);
- expect(result.pm).toBe("pnpm");
- expect(result.runtime).toBe("bun");
- expect(result.pmVersion).toBe("8");
- });
-
- it("merges package.json runtime with lockfile pm", () => {
- writeFileSync(
- join(tmp, "package.json"),
- JSON.stringify({ engines: { node: ">=18" } }),
- );
- writeFileSync(join(tmp, "pnpm-lock.yaml"), "");
- expect(detect(tmp)).toEqual({ runtime: "node", pm: "pnpm" });
- });
-
- it("returns empty for nonexistent path", () => {
- expect(detect(join(tmp, "does-not-exist"))).toEqual({});
- });
-
- it("detects yarn-berry from packageManager field", () => {
- writeFileSync(
- join(tmp, "package.json"),
- JSON.stringify({ packageManager: "yarn@4.5.0" }),
- );
- writeFileSync(join(tmp, "yarn.lock"), "");
- expect(detect(tmp)).toEqual({ pm: "yarn-berry", pmVersion: "4.5.0" });
- });
-
- it("passes pmVersion through from package.json", () => {
- writeFileSync(
- join(tmp, "package.json"),
- JSON.stringify({ packageManager: "pnpm@9.1.0" }),
- );
- const result = detect(tmp);
- expect(result.pmVersion).toBe("9.1.0");
- });
-
- it("omits pmVersion when no packageManager field", () => {
- writeFileSync(join(tmp, "pnpm-lock.yaml"), "");
- const result = detect(tmp);
- expect(result.pmVersion).toBeUndefined();
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/elixir.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/elixir.test.ts
deleted file mode 100644
index b5f9c7a0..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/elixir.test.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { elixir } from "../../src/toolchains/elixir.js";
-import { sh, timeout } from "../../src/step.js";
-import { pipeline } from "../../src/pipeline.js";
-
-describe("elixir factory", () => {
- it("returns an ElixirProject with defaults", () => {
- const ex = elixir();
- expect(ex.path).toBe(".");
- expect(ex.install()._cmd).toContain("mix deps.get");
- });
-
- it("accepts path and versions", () => {
- const ex = elixir({ path: "apps/api", elixirVersion: "1.18.3", otpVersion: "27.3.3" });
- expect(ex.path).toBe("apps/api");
- expect(ex.install()._parent!._cmd).toContain("1.18.3");
- });
-
- it("rejects invalid elixir version", () => {
- expect(() => elixir({ elixirVersion: "abc" })).toThrow("invalid elixir version");
- });
-
- it("rejects invalid otp version", () => {
- expect(() => elixir({ otpVersion: "xyz" })).toThrow("invalid otp version");
- });
-});
-
-describe("elixir actions", () => {
- it("compile runs mix compile --warnings-as-errors", () => {
- expect(elixir().compile()._cmd).toContain("mix compile --warnings-as-errors");
- });
-
- it("test runs mix test", () => {
- expect(elixir().test()._cmd).toContain("mix test");
- });
-
- it("format runs mix format --check-formatted", () => {
- expect(elixir().format()._cmd).toContain("mix format --check-formatted");
- });
-
- it("credo runs mix credo --strict", () => {
- expect(elixir().credo()._cmd).toContain("mix credo --strict");
- });
-
- it("plt builds PLT with onChange cache", () => {
- const ex = elixir();
- const step = ex.plt();
- expect(step._cmd).toContain("mix dialyzer --plt");
- expect(step._label).toBe(":ex: plt");
- expect(step._cache).toEqual({ kind: "on_change", paths: ["./mix.lock"] });
- });
-
- it("dialyzer chains through plt step", () => {
- const ex = elixir();
- const step = ex.dialyzer();
- expect(step._cmd).toContain("mix dialyzer");
- expect(step._cmd).not.toContain("--plt");
- expect(step._parent!._label).toBe(":ex: plt");
- });
-
- it("plt is memoized per project instance", () => {
- const ex = elixir();
- expect(ex.plt()).toBe(ex.plt());
- });
-
- it("sobelow runs mix sobelow --exit", () => {
- expect(elixir().sobelow()._cmd).toContain("mix sobelow --exit");
- });
-
- it("depsAudit runs mix deps.audit", () => {
- expect(elixir().depsAudit()._cmd).toContain("mix deps.audit");
- });
-
- it("hexAudit runs mix hex.audit", () => {
- expect(elixir().hexAudit()._cmd).toContain("mix hex.audit");
- });
-
- it("release runs MIX_ENV=prod mix release", () => {
- const step = elixir().release();
- expect(step._cmd).toContain("MIX_ENV=prod mix release");
- });
-
- it("test with cover flag", () => {
- expect(elixir().test({ cover: true })._cmd).toContain("--cover");
- });
-
- it("test with partitions flag", () => {
- expect(elixir().test({ partitions: 4 })._cmd).toContain("--partitions 4");
- });
-
- it("release with custom env", () => {
- expect(elixir().release({ mixEnv: "staging" })._cmd).toContain("MIX_ENV=staging");
- });
-
- it("actions chain from install step", () => {
- const ex = elixir();
- expect(ex.compile()._parent).toBe(ex.install());
- });
-
- it("accepts step options", () => {
- const ex = elixir();
- const t = timeout(600, ex.test({ label: "my test" }));
- expect(t._label).toBe("my test");
- expect(t._timeoutSeconds).toBe(600);
- });
-
- it("default labels use :ex: prefix", () => {
- const ex = elixir();
- expect(ex.compile()._label).toBe(":ex: compile");
- expect(ex.test()._label).toBe(":ex: test");
- expect(ex.format()._label).toBe(":ex: format");
- expect(ex.credo()._label).toBe(":ex: credo");
- expect(ex.dialyzer()._label).toBe(":ex: dialyzer");
- });
-});
-
-describe("elixir install chain", () => {
- it("chain is: scratch → apt-base → erlang → elixir → mix-deps", () => {
- const ex = elixir();
- const deps = ex.install();
- expect(deps._label).toBe(":ex: mix-deps");
-
- const elixirInstall = deps._parent!;
- expect(elixirInstall._label).toBe(":ex: elixir-install");
-
- const erlangInstall = elixirInstall._parent!;
- expect(erlangInstall._label).toBe(":ex: erlang-install");
- });
-
- it("accepts base step", () => {
- const base = sh("custom base");
- const ex = elixir({ base });
- // chain: base → erlang-install → elixir-install → mix-deps
- const elixirInstall = ex.install()._parent!;
- const erlangInstall = elixirInstall._parent!;
- expect(erlangInstall._parent).toBe(base);
- });
-
- it("accepts custom image", () => {
- const ex = elixir({ image: "debian:12" });
- const deps = ex.install();
- const elixirStep = deps._parent!;
- const erlangStep = elixirStep._parent!;
- const aptBase = erlangStep._parent!;
- const root = aptBase._parent!;
- expect(root._image).toBe("debian:12");
- });
-});
-
-describe("elixir in pipeline", () => {
- it("produces valid IR", () => {
- const ex = elixir();
- const ir = pipeline([ex.compile(), ex.test(), ex.format()]);
- expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(5);
- expect(ir.version).toBe("0");
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/go.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/go.test.ts
deleted file mode 100644
index 7b0e1f09..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/go.test.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { go } from "../../src/toolchains/go.js";
-import { sh, timeout } from "../../src/step.js";
-import { pipeline } from "../../src/pipeline.js";
-
-describe("go factory", () => {
- it("returns a GoToolchain with defaults", () => {
- const g = go();
- expect(g.path).toBe(".");
- expect(g.install()._cmd).toContain("go version");
- });
-
- it("accepts path and version", () => {
- const g = go({ path: "cmd/server", version: "1.22" });
- expect(g.path).toBe("cmd/server");
- expect(g.install()._cmd).toContain("go1.22");
- });
-
- it("rejects invalid version", () => {
- expect(() => go({ version: "abc" })).toThrow("invalid version");
- });
-
- it("accepts two-part version", () => {
- expect(() => go({ version: "1.23" })).not.toThrow();
- });
-});
-
-describe("go actions", () => {
- it("build runs go build", () => {
- const g = go();
- expect(g.build()._cmd).toContain("go build ./...");
- });
-
- it("test runs go test", () => {
- const g = go();
- expect(g.test()._cmd).toContain("go test ./...");
- });
-
- it("vet runs go vet", () => {
- const g = go();
- expect(g.vet()._cmd).toContain("go vet ./...");
- });
-
- it("fmt runs gofmt check", () => {
- const g = go();
- expect(g.fmt()._cmd).toContain("gofmt -l");
- });
-
- it("actions chain from install step", () => {
- const g = go();
- expect(g.build()._parent).toBe(g.install());
- });
-
- it("accepts step options", () => {
- const g = go();
- const t = timeout(300, g.test({ label: "my test" }));
- expect(t._label).toBe("my test");
- expect(t._timeoutSeconds).toBe(300);
- });
-
- it("default labels use :go: prefix", () => {
- const g = go();
- expect(g.build()._label).toBe(":go: build");
- expect(g.test()._label).toBe(":go: test");
- expect(g.vet()._label).toBe(":go: vet");
- expect(g.fmt()._label).toBe(":go: fmt");
- });
-});
-
-describe("go install chain", () => {
- it("chain is: scratch → apt-base → go-install", () => {
- const g = go();
- const install = g.install();
- expect(install._cmd).toContain("go version");
-
- const aptBase = install._parent!;
- expect(aptBase._cmd).toContain("apt-get");
-
- const root = aptBase._parent!;
- expect(root._cmd).toBeNull();
- });
-
- it("accepts base step", () => {
- const base = sh("custom base");
- const g = go({ base });
- expect(g.install()._parent).toBe(base);
- });
-
- it("accepts custom image", () => {
- const g = go({ image: "debian:12" });
- const install = g.install();
- const aptBase = install._parent!;
- const root = aptBase._parent!;
- expect(root._image).toBe("debian:12");
- });
-});
-
-describe("go in pipeline", () => {
- it("produces valid IR", () => {
- const g = go();
- const ir = pipeline([g.build(), g.test()]);
- expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(3);
- expect(ir.version).toBe("0");
- });
-});
diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/js.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/js.test.ts
deleted file mode 100644
index d80772b5..00000000
--- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/js.test.ts
+++ /dev/null
@@ -1,606 +0,0 @@
-import { describe, expect, it, beforeEach, afterEach } from "vitest";
-import { js, ts, JsProject } from "../../src/toolchains/js.js";
-import { sh, timeout } from "../../src/step.js";
-import { pipeline } from "../../src/pipeline.js";
-import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
-import { join } from "node:path";
-import { tmpdir } from "node:os";
-
-// ---------------------------------------------------------------------------
-// Factory defaults
-// ---------------------------------------------------------------------------
-
-describe("js.project factory defaults", () => {
- it("returns JsProject with defaults (node + npm), path '.'", () => {
- const p = js.project();
- expect(p).toBeInstanceOf(JsProject);
- expect(p.path).toBe(".");
- expect(p.install()._cmd).toContain("npm ci");
- });
-
- it("accepts path option", () => {
- const p = js.project({ path: "packages/app" });
- expect(p.path).toBe("packages/app");
- expect(p.install()._cmd).toContain("packages/app");
- });
-
- it("accepts node version", () => {
- const p = js.project({ version: "22" });
- expect(p.install()._parent!._cmd).toContain("setup_22");
- });
-
- it("defaults pm to bun when runtime=bun", () => {
- const p = js.project({ runtime: "bun" });
- expect(p.install()._cmd).toContain("bun install");
- });
-});
-
-// ---------------------------------------------------------------------------
-// Version validation
-// ---------------------------------------------------------------------------
-
-describe("js.project version validation", () => {
- it("rejects invalid node version", () => {
- expect(() => js.project({ version: "abc" })).toThrow("invalid version");
- });
-
- it("accepts node version with .x suffix", () => {
- expect(() => js.project({ version: "22.x" })).not.toThrow();
- });
-
- it("rejects invalid bun version", () => {
- expect(() => js.project({ runtime: "bun", version: "abc" })).toThrow(
- "invalid version",
- );
- });
-
- it("accepts bun two-part semver", () => {
- expect(() => js.project({ runtime: "bun", version: "1.2" })).not.toThrow();
- });
-
- it("accepts bun three-part semver", () => {
- expect(() =>
- js.project({ runtime: "bun", version: "1.2.3" }),
- ).not.toThrow();
- });
-
- it("rejects invalid deno version", () => {
- expect(() => js.project({ runtime: "deno", version: "abc" })).toThrow(
- "invalid version",
- );
- });
-
- it("accepts deno semver", () => {
- expect(() =>
- js.project({ runtime: "deno", version: "2.0.0" }),
- ).not.toThrow();
- });
-});
-
-// ---------------------------------------------------------------------------
-// PM / runtime validation
-// ---------------------------------------------------------------------------
-
-describe("js.project PM/runtime validation", () => {
- it("rejects pm=npm with runtime=bun", () => {
- expect(() => js.project({ pm: "npm", runtime: "bun" })).toThrow(
- 'runtime="bun" only supports pm="bun"',
- );
- });
-
- it("rejects pm=pnpm with runtime=bun", () => {
- expect(() => js.project({ pm: "pnpm", runtime: "bun" })).toThrow(
- 'runtime="bun" only supports pm="bun"',
- );
- });
-
- it("rejects pm option with runtime=deno", () => {
- expect(() => js.project({ pm: "npm", runtime: "deno" })).toThrow(
- "do not set pm",
- );
- });
-
- it("allows pm=bun with runtime=node", () => {
- expect(() => js.project({ pm: "bun", runtime: "node" })).not.toThrow();
- });
-
- it("allows pm=bun with runtime=bun", () => {
- expect(() => js.project({ pm: "bun", runtime: "bun" })).not.toThrow();
- });
-
- it("allows yarn-classic / yarn-berry with runtime=node", () => {
- expect(() => js.project({ pm: "yarn-classic" })).not.toThrow();
- expect(() => js.project({ pm: "yarn-berry" })).not.toThrow();
- });
-
- it("rejects yarn with runtime=bun", () => {
- expect(() => js.project({ pm: "yarn-berry", runtime: "bun" })).toThrow(
- 'runtime="bun" only supports pm="bun"',
- );
- });
-
- it("rejects pm=deno with non-deno runtime", () => {
- expect(() => js.project({ pm: "deno", runtime: "node" })).toThrow(
- 'pm="deno" is not valid',
- );
- expect(() => js.project({ pm: "deno", runtime: "bun" })).toThrow(
- 'pm="deno" is not valid',
- );
- });
-});
-
-// ---------------------------------------------------------------------------
-// Install chain structure — node + npm
-// ---------------------------------------------------------------------------
-
-describe("js install chain: node + npm", () => {
- it("chain is: scratch → apt-base → node-install → npm-ci", () => {
- const p = js.project();
- const npmCi = p.install();
- expect(npmCi._cmd).toContain("npm ci");
-
- const nodeInstall = npmCi._parent!;
- expect(nodeInstall._cmd).toContain("nodejs");
- expect(nodeInstall._cache).toBeDefined();
-
- const aptBase = nodeInstall._parent!;
- expect(aptBase._cmd).toContain("apt-get");
-
- const root = aptBase._parent!;
- expect(root._cmd).toBeNull(); // scratch
- });
-});
-
-// ---------------------------------------------------------------------------
-// Install chain structure — node + pnpm
-// ---------------------------------------------------------------------------
-
-describe("js install chain: node + pnpm", () => {
- it("chain is: scratch → apt-base → node-install → corepack-pnpm → pnpm-deps", () => {
- const p = js.project({ pm: "pnpm" });
- const pnpmDeps = p.install();
- expect(pnpmDeps._cmd).toContain("pnpm install --frozen-lockfile");
-
- const corepack = pnpmDeps._parent!;
- expect(corepack._cmd).toContain("corepack enable pnpm");
-
- const nodeInstall = corepack._parent!;
- expect(nodeInstall._cmd).toContain("nodejs");
-
- const aptBase = nodeInstall._parent!;
- expect(aptBase._cmd).toContain("apt-get");
-
- const root = aptBase._parent!;
- expect(root._cmd).toBeNull();
- });
-});
-
-// ---------------------------------------------------------------------------
-// Install chain structure — node + yarn (classic + berry)
-// ---------------------------------------------------------------------------
-
-describe("js install chain: node + yarn", () => {
- it("yarn-classic: corepack enable → yarn install --frozen-lockfile", () => {
- const p = js.project({ pm: "yarn-classic" });
- const deps = p.install();
- expect(deps._cmd).toContain("yarn install --frozen-lockfile");
- expect(deps._cache).toBeDefined();
-
- const corepack = deps._parent!;
- expect(corepack._cmd).toContain("corepack enable");
-
- const nodeInstall = corepack._parent!;
- expect(nodeInstall._cmd).toContain("nodejs");
- });
-
- it("yarn-berry: corepack enable → yarn install --immutable", () => {
- const p = js.project({ pm: "yarn-berry" });
- const deps = p.install();
- expect(deps._cmd).toContain("yarn install --immutable");
-
- const corepack = deps._parent!;
- expect(corepack._cmd).toContain("corepack enable");
- });
-
- it("both yarn variants watch yarn.lock", () => {
- for (const pm of ["yarn-classic", "yarn-berry"] as const) {
- const deps = js.project({ pm }).install();
- const cache = deps._cache as { paths?: string[] };
- expect(JSON.stringify(cache)).toContain("yarn.lock");
- }
- });
-});
-
-// ---------------------------------------------------------------------------
-// Install chain structure — bun + bun
-// ---------------------------------------------------------------------------
-
-describe("js install chain: bun + bun", () => {
- it("chain is: scratch → apt-base(+unzip) → bun-install → bun-deps", () => {
- const p = js.project({ runtime: "bun" });
- const bunDeps = p.install();
- expect(bunDeps._cmd).toContain("bun install --frozen-lockfile");
-
- const bunSetup = bunDeps._parent!;
- expect(bunSetup._cmd).toContain("bun.sh/install");
- expect(bunSetup._cache).toBeDefined();
-
- const aptBase = bunSetup._parent!;
- expect(aptBase._cmd).toContain("apt-get");
- expect(aptBase._cmd).toContain("unzip");
-
- const root = aptBase._parent!;
- expect(root._cmd).toBeNull();
- });
-});
-
-// ---------------------------------------------------------------------------
-// Install chain structure — node + bun (PM)
-// ---------------------------------------------------------------------------
-
-describe("js install chain: node + bun (as PM)", () => {
- it("chain is: scratch → apt-base(+unzip) → node-install → bun-pm-install → bun-deps", () => {
- const p = js.project({ runtime: "node", pm: "bun" });
- const bunDeps = p.install();
- expect(bunDeps._cmd).toContain("bun install --frozen-lockfile");
-
- const bunPmInstall = bunDeps._parent!;
- expect(bunPmInstall._cmd).toContain("bun.sh/install");
-
- const nodeInstall = bunPmInstall._parent!;
- expect(nodeInstall._cmd).toContain("nodejs");
-
- const aptBase = nodeInstall._parent!;
- expect(aptBase._cmd).toContain("apt-get");
- expect(aptBase._cmd).toContain("unzip");
-
- const root = aptBase._parent!;
- expect(root._cmd).toBeNull();
- });
-});
-
-// ---------------------------------------------------------------------------
-// Install chain structure — deno
-// ---------------------------------------------------------------------------
-
-describe("js install chain: deno", () => {
- it("chain is: scratch → apt-base(+unzip) → deno-install → deno-deps", () => {
- const p = js.project({ runtime: "deno" });
- const denoDeps = p.install();
- expect(denoDeps._cmd).toContain("deno install");
-
- const denoSetup = denoDeps._parent!;
- expect(denoSetup._cmd).toContain("deno.land/install.sh");
- expect(denoSetup._cache).toBeDefined();
-
- const aptBase = denoSetup._parent!;
- expect(aptBase._cmd).toContain("apt-get");
- expect(aptBase._cmd).toContain("unzip");
-
- const root = aptBase._parent!;
- expect(root._cmd).toBeNull();
- });
-});
-
-// ---------------------------------------------------------------------------
-// Install chain — base step and custom image
-// ---------------------------------------------------------------------------
-
-describe("js install chain: base step and custom image", () => {
- it("accepts base step", () => {
- const customBase = sh("custom base");
- const p = js.project({ base: customBase });
- const npmCi = p.install();
- const nodeInstall = npmCi._parent!;
- expect(nodeInstall._parent).toBe(customBase);
- });
-
- it("accepts custom image", () => {
- const p = js.project({ image: "debian:12" });
- const npmCi = p.install();
- const nodeInstall = npmCi._parent!;
- const aptBase = nodeInstall._parent!;
- const root = aptBase._parent!;
- expect(root._image).toBe("debian:12");
- });
-});
-
-// ---------------------------------------------------------------------------
-// Actions — uniform run() across all PMs/runtimes
-// ---------------------------------------------------------------------------
-
-describe("js.project actions", () => {
- it("run() executes arbitrary script via npm run", () => {
- const p = js.project();
- const r = p.run("typecheck");
- expect(r._cmd).toContain("npm run typecheck");
- });
-
- it("run() uses pnpm run for pnpm PM", () => {
- const p = js.project({ pm: "pnpm" });
- expect(p.run("typecheck")._cmd).toContain("pnpm run typecheck");
- });
-
- it("run() uses yarn run for yarn PMs", () => {
- expect(js.project({ pm: "yarn-classic" }).run("test")._cmd).toContain(
- "yarn run test",
- );
- expect(js.project({ pm: "yarn-berry" }).run("test")._cmd).toContain(
- "yarn run test",
- );
- });
-
- it("run() uses bun run for bun runtime", () => {
- const p = js.project({ runtime: "bun" });
- expect(p.run("typecheck")._cmd).toContain("bun run typecheck");
- });
-
- it("run() uses deno task for deno runtime", () => {
- const p = js.project({ runtime: "deno" });
- expect(p.run("typecheck")._cmd).toContain("deno task typecheck");
- });
-
- it("actions attach to install (fan-out)", () => {
- const p = js.project();
- expect(p.run("test")._parent).toBe(p.install());
- expect(p.run("lint")._parent).toBe(p.install());
- });
-
- it("actions respect custom path", () => {
- const p = js.project({ path: "packages/ui" });
- expect(p.run("test")._cmd).toContain("cd packages/ui");
- });
-
- it("actions accept step options (label, timeoutSeconds)", () => {
- const p = js.project();
- const t = timeout(300, p.run("test", { label: "my test" }));
- expect(t._label).toBe("my test");
- expect(t._timeoutSeconds).toBe(300);
- });
-
- it("default label is ::