@stixxert/pi-docker-sandbox 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/boundary.md +10 -0
- package/index.ts +74 -16
- package/package.json +7 -1
- package/sandbox/README.md +277 -0
- package/sandbox/e2e.mjs +469 -0
- package/sandbox/index.ts +232 -0
- package/sandbox/operations.ts +496 -0
- package/sandbox/package.json +11 -0
- package/sandbox/transport.ts +377 -0
- package/sandbox/try.sh +157 -0
- package/security.md +40 -1
- package/template/Dockerfile +34 -0
- package/template/README.md +92 -0
- package/template/build.sh +168 -0
- package/template/install.sh +77 -0
- package/test-loader.mjs +53 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Lightweight sandbox template
|
|
2
|
+
|
|
3
|
+
A minimal base image for sandboxes, built automatically. **Measured**, not
|
|
4
|
+
estimated — `docker build` + `du` on this machine:
|
|
5
|
+
|
|
6
|
+
| Profile | On disk | docker reports | What's in it |
|
|
7
|
+
|---|---|---|---|
|
|
8
|
+
| `core` | **383 MB** | 547 MB | git, curl, ripgrep, jq, node 22, pnpm, vim-tiny |
|
|
9
|
+
| `core + build` *(default)* | **648 MB** | 920 MB | the above **+** `build-essential`, `python3` (native node modules) |
|
|
10
|
+
| *the existing pi template* | ~2.2 GB `/usr` | — | that, **plus** pi, a global `opencode-ai`, JVM, Go, Python, Rust |
|
|
11
|
+
|
|
12
|
+
So the default profile is **~3× smaller**, and the two ingredients that account
|
|
13
|
+
for most of the difference are simply *not installing pi* (136 MB) and *not
|
|
14
|
+
shipping a second coding agent* (353 MB, `opencode-ai`), a JVM (332 MB), Go
|
|
15
|
+
(72 MB), Python (49 MB) and Rust (11 MB).
|
|
16
|
+
|
|
17
|
+
## Files
|
|
18
|
+
|
|
19
|
+
| File | Purpose |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `install.sh` | the recipe. Contains **no `sbx` commands**, so the exact same script runs in a docker build (for measuring/CI) and inside a sandbox (for the real template) |
|
|
22
|
+
| `Dockerfile` | `install.sh` in docker-buildable form: `docker build -f template/Dockerfile -t sbx-lite .` |
|
|
23
|
+
| `build.sh` | builds the sbx template from `install.sh`, hashes the recipe into the tag, saves, **verifies**, and records the ref |
|
|
24
|
+
|
|
25
|
+
## The user does nothing
|
|
26
|
+
|
|
27
|
+
That is a hard requirement, and it drives the design:
|
|
28
|
+
|
|
29
|
+
1. **The tag is a hash of the recipe.** Editing `install.sh` produces a *new*
|
|
30
|
+
template tag. A stale snapshot can never be silently reused, and no version
|
|
31
|
+
bookkeeping is needed.
|
|
32
|
+
2. **`build.sh` is idempotent and cheap when current** — one `sbx template ls`
|
|
33
|
+
decides whether there is any work, so it is safe to call on every launch
|
|
34
|
+
(`bash template/build.sh`, or a launcher doing it for you). No-op: ~1 s.
|
|
35
|
+
3. **The builder records the ref** in
|
|
36
|
+
`${XDG_CACHE_HOME:-~/.cache}/pi-sbx-lite/template-ref`.
|
|
37
|
+
4. **The extension adopts it automatically** — it reads that file at session
|
|
38
|
+
start, *verifies the template still exists*, and uses it. No configuration.
|
|
39
|
+
5. **Nothing about this can fail a session.** Missing file, unreadable file,
|
|
40
|
+
template deleted since it was recorded, sbx not answering, the build itself
|
|
41
|
+
failing — every one of those degrades to the stock base image. The sandbox
|
|
42
|
+
still works; it is simply bigger. The user is never blocked and never sees an
|
|
43
|
+
error they have to act on.
|
|
44
|
+
6. **An explicit `DOCKER_SANDBOX_TEMPLATE` always wins**, so this is an opinion,
|
|
45
|
+
not a policy.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
first launch → stock base (works, bigger) [no template recorded yet]
|
|
49
|
+
build.sh runs → builds + verifies, records the ref [once, ~2 min]
|
|
50
|
+
later launches → lightweight template, automatically
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Build it
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm run template # build if missing/stale, else no-op
|
|
57
|
+
npm run template:check # report status, change nothing
|
|
58
|
+
bash template/build.sh --force
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Tunables: `SBX_LITE_BASE` (base image), `SBX_LITE_TAG` (override the tag),
|
|
62
|
+
`NODE_VERSION`, `SBX_LITE_BUILD=off` (drop `build-essential`+`python3`, −265 MB),
|
|
63
|
+
`SBX_LITE_PNPM=off`.
|
|
64
|
+
|
|
65
|
+
## Verifying without `sbx`
|
|
66
|
+
|
|
67
|
+
`build.sh` needs a host that can run `sbx`. The recipe does not: it is plain
|
|
68
|
+
apt + curl + npm, so it can be built and **measured** anywhere docker runs.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
docker build -f template/Dockerfile -t sbx-lite .
|
|
72
|
+
docker run --rm sbx-lite sh -c 'node -v; pnpm -v; git --version; rg --version'
|
|
73
|
+
docker run --rm sbx-lite du -sh /
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## What is deliberately not here
|
|
77
|
+
|
|
78
|
+
- **pi.** With the sandbox execution backend, pi runs on the host and routes its
|
|
79
|
+
tools in. Baking it into the template was 136 MB *and* a maintenance
|
|
80
|
+
treadmill: every pi release invalidated the template for every project.
|
|
81
|
+
- **A browser.** Playwright's Chromium headless shell is ~270 MB. It is genuinely
|
|
82
|
+
needed for UI work and genuinely wasted for everything else, so it belongs in a
|
|
83
|
+
separate `webdev` variant, not in every sandbox.
|
|
84
|
+
- **Language toolchains nobody asked for.** The default profile is what an
|
|
85
|
+
ordinary project needs; `build-essential` + `python3` are the one concession,
|
|
86
|
+
because a failing `pnpm install` of a native module would break the
|
|
87
|
+
"user does nothing" guarantee. Turn them off if your projects don't need them.
|
|
88
|
+
|
|
89
|
+
## See also
|
|
90
|
+
|
|
91
|
+
`sandbox/README.md` for the execution backend, and the repo README for how this
|
|
92
|
+
fits the two modes.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# build.sh — build the LIGHTWEIGHT sandbox template, automatically.
|
|
4
|
+
#
|
|
5
|
+
# bash template/build.sh # build if missing/stale, else no-op (fast)
|
|
6
|
+
# bash template/build.sh --check # report status, change nothing
|
|
7
|
+
# bash template/build.sh --ref # print the template ref, or nothing
|
|
8
|
+
# bash template/build.sh --force # rebuild even if the current one exists
|
|
9
|
+
#
|
|
10
|
+
# Design goal: **the user never runs anything and never sees a failure.**
|
|
11
|
+
#
|
|
12
|
+
# * Idempotent and cheap when current: a single `sbx template ls` decides
|
|
13
|
+
# whether to do anything, so it is safe to call on every launch.
|
|
14
|
+
# * The tag embeds a hash of the recipe, so editing install.sh produces a NEW
|
|
15
|
+
# template instead of silently reusing a stale snapshot.
|
|
16
|
+
# * Self-verifying, like the pi template builder it replaces: after saving, it
|
|
17
|
+
# boots a throwaway sandbox FROM THE SAVED TEMPLATE and asserts the tools are
|
|
18
|
+
# really there. A template that silently lacks node/pnpm/git can't be
|
|
19
|
+
# produced.
|
|
20
|
+
# * Records the resolved ref where the extension looks for it, so the sandbox
|
|
21
|
+
# starts using it with no configuration. If that file is missing or the
|
|
22
|
+
# template is gone, the extension simply creates sandboxes from the stock
|
|
23
|
+
# base — degraded, never broken.
|
|
24
|
+
#
|
|
25
|
+
# What it does NOT contain: pi (pi runs on the HOST now and routes its tools in),
|
|
26
|
+
# editors for pi, or language toolchains nobody asked for. See install.sh.
|
|
27
|
+
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
|
|
30
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
31
|
+
BASE="${SBX_LITE_BASE:-docker.io/docker/sandbox-templates:shell}"
|
|
32
|
+
|
|
33
|
+
# Portability: macOS has shasum, Linux has sha256sum.
|
|
34
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
35
|
+
HASHER="sha256sum"
|
|
36
|
+
else
|
|
37
|
+
HASHER="shasum -a 256"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# NOTE the braces: `cat f; echo x | hash` would pipe ONLY the echo and let the
|
|
41
|
+
# cat output leak into the captured value (giving a tag containing the entire
|
|
42
|
+
# recipe). The whole group must be the pipeline's input.
|
|
43
|
+
RECIPE_HASH="$( { cat "$HERE/install.sh"; printf 'base=%s\n' "$BASE"; } | $HASHER | cut -c1-8 )"
|
|
44
|
+
|
|
45
|
+
# A malformed tag would produce a template nobody can ever match, so fail loudly
|
|
46
|
+
# here rather than writing a nonsense ref that the extension then ignores.
|
|
47
|
+
case "$RECIPE_HASH" in
|
|
48
|
+
[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]) ;;
|
|
49
|
+
*)
|
|
50
|
+
echo "build: could not compute a recipe hash (got '${RECIPE_HASH:0:40}…')" >&2
|
|
51
|
+
exit 1
|
|
52
|
+
;;
|
|
53
|
+
esac
|
|
54
|
+
|
|
55
|
+
TAG="${SBX_LITE_TAG:-pi-sbx-lite:${RECIPE_HASH}}"
|
|
56
|
+
REF="docker.io/library/${TAG}"
|
|
57
|
+
STATE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/pi-sbx-lite"
|
|
58
|
+
STATE_FILE="$STATE_DIR/template-ref"
|
|
59
|
+
|
|
60
|
+
TPL_SANDBOX="pi-sbx-lite-build-$$"
|
|
61
|
+
VERIFY_SANDBOX="pi-sbx-lite-verify-$$"
|
|
62
|
+
# Build in a scratch dir, never the caller's cwd: mounting the caller's project
|
|
63
|
+
# into the snapshot would bake project files into the shared image.
|
|
64
|
+
SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/pi-sbx-lite.XXXXXX")"
|
|
65
|
+
|
|
66
|
+
MODE="ensure"
|
|
67
|
+
case "${1:-}" in
|
|
68
|
+
--check) MODE="check" ;;
|
|
69
|
+
--ref) MODE="ref" ;;
|
|
70
|
+
--force) MODE="force" ;;
|
|
71
|
+
"") ;;
|
|
72
|
+
*)
|
|
73
|
+
echo "usage: $0 [--check|--ref|--force]" >&2
|
|
74
|
+
exit 2
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
|
|
78
|
+
cleanup() {
|
|
79
|
+
rm -rf "$SCRATCH"
|
|
80
|
+
sbx rm --force "$TPL_SANDBOX" >/dev/null 2>&1 || true
|
|
81
|
+
sbx rm --force "$VERIFY_SANDBOX" >/dev/null 2>&1 || true
|
|
82
|
+
}
|
|
83
|
+
trap cleanup EXIT
|
|
84
|
+
|
|
85
|
+
# `sbx template ls` differs between sbx versions (table vs flat), so match on the
|
|
86
|
+
# repository basename + version rather than on a column position — the same
|
|
87
|
+
# approach the existing pi-template detector needed.
|
|
88
|
+
template_present() {
|
|
89
|
+
sbx template ls 2>/dev/null | awk -v tag="${TAG}" '
|
|
90
|
+
{
|
|
91
|
+
sub(/^[[:space:]]+/, "")
|
|
92
|
+
if ($0 == "" || $0 ~ /^REPOSITORY/) next
|
|
93
|
+
r = $1; sub(/^.*\//, "", r)
|
|
94
|
+
if (r == tag && $2 != "") found = 1 # table row: <repo> <version>
|
|
95
|
+
if ($1 == "docker.io/library/" tag) found = 1
|
|
96
|
+
if (r == tag) found = 1
|
|
97
|
+
}
|
|
98
|
+
END { exit(found ? 0 : 1) }'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
record_ref() {
|
|
102
|
+
mkdir -p "$STATE_DIR"
|
|
103
|
+
printf '%s\n' "$REF" >"$STATE_FILE"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case "$MODE" in
|
|
107
|
+
ref)
|
|
108
|
+
[ -f "$STATE_FILE" ] && cat "$STATE_FILE"
|
|
109
|
+
exit 0
|
|
110
|
+
;;
|
|
111
|
+
check)
|
|
112
|
+
if template_present; then
|
|
113
|
+
echo "current: $REF"
|
|
114
|
+
exit 0
|
|
115
|
+
fi
|
|
116
|
+
echo "missing: $REF (would build from $BASE)"
|
|
117
|
+
exit 1
|
|
118
|
+
;;
|
|
119
|
+
ensure)
|
|
120
|
+
if template_present; then
|
|
121
|
+
record_ref
|
|
122
|
+
echo "template up to date: $REF"
|
|
123
|
+
exit 0
|
|
124
|
+
fi
|
|
125
|
+
;;
|
|
126
|
+
force) : ;;
|
|
127
|
+
esac
|
|
128
|
+
|
|
129
|
+
echo "building lightweight sandbox template"
|
|
130
|
+
echo " base: $BASE"
|
|
131
|
+
echo " tag: $TAG (recipe hash ${RECIPE_HASH})"
|
|
132
|
+
echo " recipe: $HERE/install.sh"
|
|
133
|
+
echo
|
|
134
|
+
|
|
135
|
+
# Leftovers from an interrupted build would make `sbx create` fail.
|
|
136
|
+
sbx rm --force "$TPL_SANDBOX" >/dev/null 2>&1 || true
|
|
137
|
+
sbx template rm "$TAG" >/dev/null 2>&1 || true
|
|
138
|
+
|
|
139
|
+
echo "creating throwaway sandbox…"
|
|
140
|
+
sbx create -q --name "$TPL_SANDBOX" -t "$BASE" shell "$SCRATCH"
|
|
141
|
+
|
|
142
|
+
# The recipe is passed as a single argv element; no output-masking pipe, so a
|
|
143
|
+
# failure inside the install surfaces as a non-zero exit and `set -e` aborts.
|
|
144
|
+
echo "installing baseline (this is the part that takes a couple of minutes)…"
|
|
145
|
+
sbx exec "$TPL_SANDBOX" -- bash -lc "$(cat "$HERE/install.sh")"
|
|
146
|
+
|
|
147
|
+
echo "saving template…"
|
|
148
|
+
sbx template save "$TPL_SANDBOX" "$TAG" # no `|| true` — a failed save must abort
|
|
149
|
+
sbx rm --force "$TPL_SANDBOX" >/dev/null 2>&1 || true
|
|
150
|
+
|
|
151
|
+
# The definitive check: boot FROM THE SAVED TEMPLATE and assert the toolchain.
|
|
152
|
+
# Asserting in the build sandbox would prove nothing about the saved image.
|
|
153
|
+
echo "verifying the saved template boots with a working toolchain…"
|
|
154
|
+
sbx create -q --name "$VERIFY_SANDBOX" -t "$REF" shell "$SCRATCH"
|
|
155
|
+
sbx exec "$VERIFY_SANDBOX" -- bash -lc '
|
|
156
|
+
set -e
|
|
157
|
+
for c in node pnpm git rg; do
|
|
158
|
+
command -v "$c" >/dev/null || { echo "verify: missing $c" >&2; exit 1; }
|
|
159
|
+
done
|
|
160
|
+
node -e "require(\"child_process\")" 2>/dev/null || { echo "verify: node is broken" >&2; exit 1; }
|
|
161
|
+
printf "verify: node %s, pnpm %s, git %s, rg %s\n" \
|
|
162
|
+
"$(node -v)" "$(pnpm -v)" "$(git --version | awk "{print \$3}")" "$(rg --version | head -1 | awk "{print \$2}")"'
|
|
163
|
+
sbx rm --force "$VERIFY_SANDBOX" >/dev/null 2>&1 || true
|
|
164
|
+
|
|
165
|
+
record_ref
|
|
166
|
+
echo
|
|
167
|
+
echo "done. recorded $REF in $STATE_FILE"
|
|
168
|
+
echo "the sandbox extension picks this up automatically; override with DOCKER_SANDBOX_TEMPLATE."
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# install.sh — the lightweight sandbox baseline.
|
|
4
|
+
#
|
|
5
|
+
# Runs INSIDE the sandbox (or inside a docker build). Deliberately contains no
|
|
6
|
+
# `sbx` commands so the exact same recipe can be validated with plain docker
|
|
7
|
+
# (`docker build -f template/Dockerfile …`) on machines where sbx cannot run.
|
|
8
|
+
#
|
|
9
|
+
# What it is: a general Linux + git + node/pnpm + search box. That is all a
|
|
10
|
+
# sandbox needs once pi runs on the HOST and merely routes its tools here —
|
|
11
|
+
# pi itself, an editor stack for pi, and every language toolchain somebody
|
|
12
|
+
# might want are all gone.
|
|
13
|
+
#
|
|
14
|
+
# Tunables (env):
|
|
15
|
+
# NODE_VERSION=22.22.1 node LTS to install
|
|
16
|
+
# SBX_LITE_BUILD=off drop build-essential + python3 (~250 MB, needed
|
|
17
|
+
# for native node modules / node-gyp)
|
|
18
|
+
# SBX_LITE_PNPM=off drop the pnpm global install
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
NODE_VERSION="${NODE_VERSION:-22.22.1}"
|
|
23
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
24
|
+
|
|
25
|
+
SUDO=""
|
|
26
|
+
if [ "$(id -u)" != "0" ]; then SUDO="sudo"; fi
|
|
27
|
+
|
|
28
|
+
# ── base packages ─────────────────────────────────────────────────────────
|
|
29
|
+
# git/curl/ca-certificates: cloning and fetching over TLS.
|
|
30
|
+
# ripgrep: the agent's search tool, and much faster than grep -r.
|
|
31
|
+
# build-essential + python3: native node modules (node-gyp) — without these a
|
|
32
|
+
# `pnpm install` in an ordinary project can fail, which would break the
|
|
33
|
+
# "the user does nothing" guarantee. Opt out with SBX_LITE_BUILD=off.
|
|
34
|
+
CORE="ca-certificates curl git gnupg ripgrep jq unzip xz-utils procps less vim-tiny"
|
|
35
|
+
EXTRA=""
|
|
36
|
+
if [ "${SBX_LITE_BUILD:-on}" != "off" ]; then EXTRA="build-essential python3"; fi
|
|
37
|
+
|
|
38
|
+
echo "install: apt packages (core${EXTRA:+ + $EXTRA})"
|
|
39
|
+
# shellcheck disable=SC2086
|
|
40
|
+
$SUDO apt-get update -qq
|
|
41
|
+
# shellcheck disable=SC2086
|
|
42
|
+
$SUDO apt-get install -y -qq --no-install-recommends $CORE $EXTRA
|
|
43
|
+
|
|
44
|
+
# ── apt hygiene ───────────────────────────────────────────────────────────
|
|
45
|
+
# ~55 MB in a typical Ubuntu image, and pure waste in a snapshot: the lists are
|
|
46
|
+
# stale the moment the image is saved and every sandbox re-downloads them.
|
|
47
|
+
$SUDO rm -rf /var/lib/apt/lists/* /usr/share/doc/* /usr/share/man/* /var/cache/apt/*
|
|
48
|
+
|
|
49
|
+
# ── node + pnpm ───────────────────────────────────────────────────────────
|
|
50
|
+
# The official tarball into /usr/local rather than a distro/node image layer:
|
|
51
|
+
# no apt repository to configure, no extra base layers to carry.
|
|
52
|
+
case "$(uname -m)" in
|
|
53
|
+
aarch64 | arm64) NODE_ARCH="arm64" ;;
|
|
54
|
+
x86_64 | amd64) NODE_ARCH="x64" ;;
|
|
55
|
+
*)
|
|
56
|
+
echo "install: unsupported architecture $(uname -m)" >&2
|
|
57
|
+
exit 1
|
|
58
|
+
;;
|
|
59
|
+
esac
|
|
60
|
+
|
|
61
|
+
echo "install: node v${NODE_VERSION} (${NODE_ARCH})"
|
|
62
|
+
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" -o /tmp/node.tar.xz
|
|
63
|
+
$SUDO tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1
|
|
64
|
+
rm -f /tmp/node.tar.xz
|
|
65
|
+
|
|
66
|
+
# The tarball also ships node's C++ headers (~65 MB). node-gyp fetches its own
|
|
67
|
+
# headers into a cache, so these are dead weight in a snapshot.
|
|
68
|
+
$SUDO rm -rf /usr/local/include/node
|
|
69
|
+
|
|
70
|
+
if [ "${SBX_LITE_PNPM:-on}" != "off" ]; then
|
|
71
|
+
echo "install: pnpm"
|
|
72
|
+
$SUDO npm install -g --no-audit --no-fund --loglevel=error pnpm
|
|
73
|
+
fi
|
|
74
|
+
# The npm cache is ~30-60 MB of tarballs that a snapshot never needs again.
|
|
75
|
+
$SUDO npm cache clean --force >/dev/null 2>&1 || true
|
|
76
|
+
|
|
77
|
+
echo "install: done — node $(node -v), pnpm $(pnpm -v 2>/dev/null || echo absent)"
|
package/test-loader.mjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a TypeScript module from a plain Node script.
|
|
3
|
+
*
|
|
4
|
+
* Node >= 23.6 strips types natively and can import `.ts` directly. Older
|
|
5
|
+
* Node (22.x) cannot (`ERR_NO_TYPESCRIPT`) or does not know the extension at
|
|
6
|
+
* all (`ERR_UNKNOWN_FILE_EXTENSION`), so fall back to `jiti` — the same
|
|
7
|
+
* loader pi itself uses for extensions. jiti is resolved from pi's own
|
|
8
|
+
* dependency tree so no extra dependency is needed.
|
|
9
|
+
*
|
|
10
|
+
* Shared by smoke-test.mjs and sandbox/e2e.mjs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const repoRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
let jiti;
|
|
21
|
+
|
|
22
|
+
function makeJiti() {
|
|
23
|
+
if (jiti) return jiti;
|
|
24
|
+
for (const candidate of [
|
|
25
|
+
path.join(repoRoot, "node_modules/@earendil-works/pi-coding-agent/node_modules/jiti"),
|
|
26
|
+
"jiti",
|
|
27
|
+
]) {
|
|
28
|
+
try {
|
|
29
|
+
const mod = require(candidate);
|
|
30
|
+
const createJiti = mod.createJiti ?? mod.default?.createJiti;
|
|
31
|
+
if (createJiti) {
|
|
32
|
+
jiti = createJiti(repoRoot + path.sep);
|
|
33
|
+
return jiti;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
/* try the next candidate */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw new Error("could not load jiti (needed to import .ts on Node < 23.6)");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Import a `.ts` (or `.mjs`) module, by repo-relative or absolute path. */
|
|
43
|
+
export async function loadTs(relativeOrAbsolute) {
|
|
44
|
+
const absolute = path.isAbsolute(relativeOrAbsolute)
|
|
45
|
+
? relativeOrAbsolute
|
|
46
|
+
: path.join(repoRoot, relativeOrAbsolute);
|
|
47
|
+
try {
|
|
48
|
+
return await import(absolute);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err?.code !== "ERR_UNKNOWN_FILE_EXTENSION" && err?.code !== "ERR_NO_TYPESCRIPT") throw err;
|
|
51
|
+
return makeJiti()(absolute);
|
|
52
|
+
}
|
|
53
|
+
}
|