bc-pkg 1.0.0
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 +163 -0
- package/bin/bc-pkg.js +668 -0
- package/package.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# bc-pkg
|
|
2
|
+
|
|
3
|
+
Run [babashka](https://babashka.org) (`bb`) without installing anything first.
|
|
4
|
+
On its first invocation this package downloads a pinned babashka binary **and**
|
|
5
|
+
an Eclipse Temurin JDK into a shared user cache, then forwards every argument
|
|
6
|
+
to `bb`.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npx bc-pkg@latest tasks # -> bb tasks
|
|
12
|
+
npx bc-pkg@latest <args...> # -> bb <args...>
|
|
13
|
+
npx bc-pkg@latest <owner>/<project> <args...> # bootstrap/validate bb.edn, then -> bb <args...>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The npm package is unscoped `bc-pkg` and exposes a single `bc-pkg` bin. All
|
|
17
|
+
other arguments (including flags) are passed through verbatim, and `bb` runs in
|
|
18
|
+
your current working directory, so it picks up the local `bb.edn`. If
|
|
19
|
+
the first argument has the shape `owner/project`, it is consumed as repo
|
|
20
|
+
identity and never forwarded to `bb`: it bootstraps a missing `bb.edn` or
|
|
21
|
+
validates the existing `bb.edn`'s top-level `:repo`.
|
|
22
|
+
|
|
23
|
+
## What happens on first run
|
|
24
|
+
|
|
25
|
+
1. The host OS/CPU are resolved to the matching babashka release asset and
|
|
26
|
+
Adoptium API parameters.
|
|
27
|
+
2. babashka is downloaded from its GitHub releases and cached.
|
|
28
|
+
3. A Temurin JDK is downloaded from the Adoptium API and cached.
|
|
29
|
+
4. On Linux, `git` is installed via the system package manager if it is not on
|
|
30
|
+
`PATH`.
|
|
31
|
+
5. If a `<owner>/<project>` slug is the first argument, it is consumed; it
|
|
32
|
+
bootstraps a missing `bb.edn` or validates an existing `bb.edn`'s `:repo`.
|
|
33
|
+
6. `bb` is launched with `JAVA_HOME` / `PATH` pointing at the cached JDK (and
|
|
34
|
+
the cached `bb`, so nested `bb` calls work) — the environment change applies
|
|
35
|
+
**only** to the `bb` subprocess.
|
|
36
|
+
|
|
37
|
+
Subsequent runs reuse the cache and start immediately.
|
|
38
|
+
|
|
39
|
+
## git (Linux only)
|
|
40
|
+
|
|
41
|
+
On Linux, if `git` is not on `PATH`, it is installed via the system package
|
|
42
|
+
manager (`apt-get`, `dnf`, `yum`, `zypper`, `pacman`, or `apk`), using `sudo`
|
|
43
|
+
when not running as root. This is **skipped** when git is already present, and
|
|
44
|
+
is a **no-op on macOS/Windows**. Unlike babashka/JDK, this modifies the system
|
|
45
|
+
and may prompt for a sudo password; in non-interactive environments without
|
|
46
|
+
passwordless sudo it will fail with an actionable message — pre-install git to
|
|
47
|
+
avoid this entirely.
|
|
48
|
+
|
|
49
|
+
## bb.edn bootstrap (optional)
|
|
50
|
+
|
|
51
|
+
If the current directory has **no `bb.edn`** and the first argument has the
|
|
52
|
+
shape `owner/project`, that repo's `bb.edn` is downloaded (pinned to its
|
|
53
|
+
default branch's latest commit), top-level `:repo "owner/project"` is ensured,
|
|
54
|
+
and the repo itself is added to `:deps` as
|
|
55
|
+
`io.github.<owner>/<project> {:git/sha "<sha>"}`. The edit is done with
|
|
56
|
+
`borkdude/rewrite-edn`, so existing comments and formatting are preserved.
|
|
57
|
+
The slug is consumed; remaining arguments are forwarded to `bb`.
|
|
58
|
+
|
|
59
|
+
If a `bb.edn` already exists, the same slug is still consumed and compared
|
|
60
|
+
exactly with top-level `:repo`. The slug may be omitted when `bb.edn` exists.
|
|
61
|
+
|
|
62
|
+
Any dependency using `:local/root` (in `:deps` or a task's `:extra-deps`) is
|
|
63
|
+
removed first, since those paths don't exist once the file is downloaded.
|
|
64
|
+
Valid Maven/git deps are kept.
|
|
65
|
+
|
|
66
|
+
- Skipped if no slug is given and no `bb.edn` needs to be created.
|
|
67
|
+
- Fatal error, when a slug is supplied, if an existing `bb.edn` is invalid,
|
|
68
|
+
lacks `:repo`, or has a different `:repo`.
|
|
69
|
+
- Fatal error if the repo is missing/inaccessible, has no `bb.edn`, or its
|
|
70
|
+
`bb.edn` declares a different `:repo`.
|
|
71
|
+
- Set `GITHUB_TOKEN` for private repos or to avoid GitHub's unauthenticated
|
|
72
|
+
API rate limit.
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
npx bc-pkg@latest my-org/shared-tasks tasks
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Cache location
|
|
79
|
+
|
|
80
|
+
A single shared directory, reused across all projects:
|
|
81
|
+
|
|
82
|
+
| Platform | Path |
|
|
83
|
+
| ------------- | -------------------------------------------- |
|
|
84
|
+
| macOS / Linux | `$XDG_CACHE_HOME` or `~/.cache` → `bc-pkg/` |
|
|
85
|
+
| Windows | `%LOCALAPPDATA%` → `bc-pkg/` |
|
|
86
|
+
|
|
87
|
+
Delete that directory to force a clean reinstall.
|
|
88
|
+
|
|
89
|
+
## Configuration
|
|
90
|
+
|
|
91
|
+
| Env var | Default | Effect |
|
|
92
|
+
| --------------------- | ---------- | ----------------------------------------------- |
|
|
93
|
+
| `BB_VERSION` | `1.12.196` | babashka release version to install |
|
|
94
|
+
| `JDK_VERSION` | `21` | Temurin feature version (e.g. `17`, `21`, `25`) |
|
|
95
|
+
| `GITHUB_TOKEN` | _(unset)_ | Used for the bb.edn bootstrap (private repos / higher API rate limit) |
|
|
96
|
+
| `REWRITE_EDN_VERSION` | `0.5.9` | `borkdude/rewrite-edn` version used to edit the `bb.edn` |
|
|
97
|
+
|
|
98
|
+
## Supported platforms
|
|
99
|
+
|
|
100
|
+
macOS arm64, macOS x64, Linux x64, Linux arm64, Windows x64.
|
|
101
|
+
|
|
102
|
+
Notes:
|
|
103
|
+
|
|
104
|
+
- Linux x64 uses babashka's glibc build (may not run on musl distros such as
|
|
105
|
+
Alpine). Linux arm64 uses babashka's static build, which runs on both glibc
|
|
106
|
+
and musl.
|
|
107
|
+
- Extraction uses the system `tar` (present on macOS, Linux, and Windows
|
|
108
|
+
10+); Windows falls back to PowerShell `Expand-Archive` for `.zip` if `tar`
|
|
109
|
+
is unavailable.
|
|
110
|
+
|
|
111
|
+
## Development Docker image
|
|
112
|
+
|
|
113
|
+
This repository also includes a `Dockerfile` and `bb.edn` tasks for a
|
|
114
|
+
throwaway development shell. The former Makefile workflow now lives in
|
|
115
|
+
`bb.edn`. The image is based on Ubuntu 24.04 and installs Node.js, the pi
|
|
116
|
+
coding agent, Claude, `ripgrep`, `fd`, and `sudo`. Requires Docker. Commands
|
|
117
|
+
below assume `bb` is on `PATH`; use `node bin/bc-pkg.js <task>` to exercise the
|
|
118
|
+
local launcher instead.
|
|
119
|
+
|
|
120
|
+
If these tasks are bootstrapped into an empty directory by passing
|
|
121
|
+
`bigconfig-ai/bc-pkg` as the first argument, the missing `Dockerfile` is
|
|
122
|
+
downloaded into that directory from the same pinned GitHub SHA as the
|
|
123
|
+
bootstrapped `bb.edn`:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
mkdir empty && cd empty
|
|
127
|
+
npx bc-pkg@latest bigconfig-ai/bc-pkg shell
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
bb tasks # list repository tasks
|
|
132
|
+
bb build # build bc-pkg:dev
|
|
133
|
+
bb build --no-cache # rebuild without Docker layer cache
|
|
134
|
+
bb shell # build, create a generated home, then open bash
|
|
135
|
+
bb shell --skip-build # reuse the existing image
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`bb shell` creates a writable host directory under `homes/<random-name>` and
|
|
139
|
+
mounts it at `/home/developer` in the container. Before starting Docker it
|
|
140
|
+
copies `~/.pi/agent/auth.json` and `~/.pi/agent/settings.json` into that
|
|
141
|
+
generated home when those files exist; missing files are skipped. Use
|
|
142
|
+
`--project-subdir PATH` to mount a specific host directory instead, and
|
|
143
|
+
`bb homes` / `bb clean --all` to list or remove generated homes.
|
|
144
|
+
|
|
145
|
+
Common options are available as flags or environment variables:
|
|
146
|
+
|
|
147
|
+
| Option / env | Default | Effect |
|
|
148
|
+
| ------------ | ------- | ------ |
|
|
149
|
+
| `--image` / `IMAGE` | `bc-pkg` | Docker image name |
|
|
150
|
+
| `--tag` / `TAG` | `dev` | Docker image tag |
|
|
151
|
+
| `--node-major` / `NODE_MAJOR` | `24` | Node.js major version build arg |
|
|
152
|
+
| `--no-cache` | `false` | Pass `--no-cache` to `docker build` |
|
|
153
|
+
| `--workdir` / `WORKDIR` | `/home/developer` | Container working directory |
|
|
154
|
+
| `--name` / `DOCKER_STYLE_RANDOM_NAME` | random | Container hostname and generated home name |
|
|
155
|
+
| `--project-subdir` / `PROJECT_SUBDIR` | `homes/<name>` | Host directory mounted into the container |
|
|
156
|
+
| `--docker-run-arg ARG` | _(none)_ | Extra `docker run` argument; repeat as needed |
|
|
157
|
+
| `--dry-run` | `false` | Print commands without executing them |
|
|
158
|
+
|
|
159
|
+
Run `bb options` for the full option list.
|
|
160
|
+
|
|
161
|
+
## Requirements
|
|
162
|
+
|
|
163
|
+
Node.js >= 18.
|
package/bin/bc-pkg.js
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// bc-pkg — bootstraps babashka + a Temurin JDK on first use, then
|
|
5
|
+
// forwards all arguments to `bb`. Single-file launcher, no build step.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawn, spawnSync } = require('child_process');
|
|
11
|
+
const { Readable } = require('stream');
|
|
12
|
+
const { pipeline } = require('stream/promises');
|
|
13
|
+
|
|
14
|
+
// Pinned, known-good versions. Overridable via env.
|
|
15
|
+
const DEFAULT_BB_VERSION = process.env.BB_VERSION || '1.12.196';
|
|
16
|
+
const DEFAULT_JDK_VERSION = process.env.JDK_VERSION || '21';
|
|
17
|
+
const REWRITE_EDN_VERSION = process.env.REWRITE_EDN_VERSION || '0.5.9';
|
|
18
|
+
|
|
19
|
+
const TAG = '[bc-pkg]';
|
|
20
|
+
|
|
21
|
+
function log(msg) {
|
|
22
|
+
// stderr so stdout stays clean for bb's own output.
|
|
23
|
+
process.stderr.write(`${TAG} ${msg}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- Platform resolution -------------------------------------------------
|
|
27
|
+
|
|
28
|
+
// Maps the host OS/arch to babashka release asset + Adoptium API parameters.
|
|
29
|
+
function resolvePlatform() {
|
|
30
|
+
const plat = process.platform; // 'darwin' | 'linux' | 'win32'
|
|
31
|
+
const arch = process.arch; // 'arm64' | 'x64'
|
|
32
|
+
const exeSuffix = plat === 'win32' ? '.exe' : '';
|
|
33
|
+
|
|
34
|
+
let bbOs;
|
|
35
|
+
let jdkOs;
|
|
36
|
+
let archiveExt;
|
|
37
|
+
if (plat === 'darwin') {
|
|
38
|
+
bbOs = 'macos';
|
|
39
|
+
jdkOs = 'mac';
|
|
40
|
+
archiveExt = 'tar.gz';
|
|
41
|
+
} else if (plat === 'linux') {
|
|
42
|
+
bbOs = 'linux';
|
|
43
|
+
jdkOs = 'linux';
|
|
44
|
+
archiveExt = 'tar.gz';
|
|
45
|
+
} else if (plat === 'win32') {
|
|
46
|
+
bbOs = 'windows';
|
|
47
|
+
jdkOs = 'windows';
|
|
48
|
+
archiveExt = 'zip';
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error(`Unsupported OS: ${plat}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let bbArch;
|
|
54
|
+
let jdkArch;
|
|
55
|
+
if (arch === 'arm64') {
|
|
56
|
+
bbArch = 'aarch64';
|
|
57
|
+
jdkArch = 'aarch64';
|
|
58
|
+
} else if (arch === 'x64') {
|
|
59
|
+
bbArch = 'amd64';
|
|
60
|
+
jdkArch = 'x64';
|
|
61
|
+
} else {
|
|
62
|
+
throw new Error(`Unsupported CPU architecture: ${arch}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// babashka ships only a *static* (musl) build for Linux arm64 — there is no
|
|
66
|
+
// dynamic linux-aarch64 asset. The static build also runs on glibc.
|
|
67
|
+
const bbArchToken =
|
|
68
|
+
plat === 'linux' && arch === 'arm64' ? 'aarch64-static' : bbArch;
|
|
69
|
+
|
|
70
|
+
if (plat === 'win32' && arch === 'arm64') {
|
|
71
|
+
throw new Error('babashka has no prebuilt Windows arm64 binary');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
exeSuffix,
|
|
76
|
+
archiveExt,
|
|
77
|
+
jdkOs,
|
|
78
|
+
jdkArch,
|
|
79
|
+
bbAssetName(version) {
|
|
80
|
+
return `babashka-${version}-${bbOs}-${bbArchToken}.${archiveExt}`;
|
|
81
|
+
},
|
|
82
|
+
jdkArchiveName: `jdk.${archiveExt}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function cacheRoot() {
|
|
87
|
+
const base =
|
|
88
|
+
process.platform === 'win32'
|
|
89
|
+
? process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
|
|
90
|
+
: process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
91
|
+
return path.join(base, 'bc-pkg');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- Filesystem helpers --------------------------------------------------
|
|
95
|
+
|
|
96
|
+
function rmrf(target) {
|
|
97
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Runs `install(tmpDir)` into a private temp dir, then atomically renames it
|
|
101
|
+
// into place. Concurrent first-runs race on the rename; the loser is discarded
|
|
102
|
+
// so the final dir is never left half-written.
|
|
103
|
+
async function installOnce(finalDir, install) {
|
|
104
|
+
if (fs.existsSync(finalDir)) return;
|
|
105
|
+
const tmp = `${finalDir}.tmp-${process.pid}-${Date.now()}`;
|
|
106
|
+
fs.mkdirSync(tmp, { recursive: true });
|
|
107
|
+
try {
|
|
108
|
+
await install(tmp);
|
|
109
|
+
if (fs.existsSync(finalDir)) {
|
|
110
|
+
rmrf(tmp); // another process won the race
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
fs.mkdirSync(path.dirname(finalDir), { recursive: true });
|
|
114
|
+
try {
|
|
115
|
+
fs.renameSync(tmp, finalDir);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (fs.existsSync(finalDir)) {
|
|
118
|
+
rmrf(tmp);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
rmrf(tmp);
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function download(url, destFile) {
|
|
130
|
+
const res = await fetch(url, {
|
|
131
|
+
redirect: 'follow',
|
|
132
|
+
headers: { 'user-agent': 'bc-pkg' },
|
|
133
|
+
});
|
|
134
|
+
if (!res.ok || !res.body) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Download failed (HTTP ${res.status} ${res.statusText})\n ${url}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
await fs.promises.mkdir(path.dirname(destFile), { recursive: true });
|
|
140
|
+
await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(destFile));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Extracts .tar.gz / .zip. System `tar` (GNU on Linux, bsdtar on macOS &
|
|
144
|
+
// Windows 10+) auto-detects gzip and handles zip; PowerShell is a Windows
|
|
145
|
+
// fallback when `tar` is absent.
|
|
146
|
+
function extract(archive, destDir) {
|
|
147
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
148
|
+
let r = spawnSync('tar', ['-xf', archive, '-C', destDir], {
|
|
149
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
150
|
+
});
|
|
151
|
+
if (r.error && r.error.code === 'ENOENT') {
|
|
152
|
+
if (process.platform === 'win32' && archive.endsWith('.zip')) {
|
|
153
|
+
r = spawnSync(
|
|
154
|
+
'powershell',
|
|
155
|
+
[
|
|
156
|
+
'-NoProfile',
|
|
157
|
+
'-Command',
|
|
158
|
+
`Expand-Archive -LiteralPath '${archive}' -DestinationPath '${destDir}' -Force`,
|
|
159
|
+
],
|
|
160
|
+
{ stdio: ['ignore', 'inherit', 'inherit'] }
|
|
161
|
+
);
|
|
162
|
+
if (r.status !== 0) {
|
|
163
|
+
throw new Error(`Failed to extract ${archive} (PowerShell fallback)`);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`'tar' not found on PATH; cannot extract ${archive}`);
|
|
168
|
+
}
|
|
169
|
+
if (r.status !== 0) {
|
|
170
|
+
throw new Error(`Failed to extract ${archive} (tar exit ${r.status})`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Locates JAVA_HOME inside an extracted JDK. Layout differs per OS
|
|
175
|
+
// (linux: <root>/bin/java, macOS: <root>/Contents/Home/bin/java).
|
|
176
|
+
function findJavaHome(root, exeSuffix) {
|
|
177
|
+
const javaRel = path.join('bin', `java${exeSuffix}`);
|
|
178
|
+
const stack = [root];
|
|
179
|
+
while (stack.length) {
|
|
180
|
+
const dir = stack.pop();
|
|
181
|
+
if (fs.existsSync(path.join(dir, javaRel))) return dir;
|
|
182
|
+
let entries;
|
|
183
|
+
try {
|
|
184
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
185
|
+
} catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
for (const e of entries) {
|
|
189
|
+
if (e.isDirectory()) stack.push(path.join(dir, e.name));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --- Bootstrap steps -----------------------------------------------------
|
|
196
|
+
|
|
197
|
+
async function ensureBabashka(p) {
|
|
198
|
+
const version = DEFAULT_BB_VERSION;
|
|
199
|
+
const finalDir = path.join(cacheRoot(), 'bb', version);
|
|
200
|
+
const bbPath = path.join(finalDir, `bb${p.exeSuffix}`);
|
|
201
|
+
const asset = p.bbAssetName(version);
|
|
202
|
+
|
|
203
|
+
await installOnce(finalDir, async (tmp) => {
|
|
204
|
+
const archive = path.join(tmp, asset);
|
|
205
|
+
const url = `https://github.com/babashka/babashka/releases/download/v${version}/${asset}`;
|
|
206
|
+
log(`Installing babashka ${version} (set BB_VERSION to override)...`);
|
|
207
|
+
try {
|
|
208
|
+
await download(url, archive);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
throw new Error(`${err.message}\n (override with BB_VERSION=<version>)`);
|
|
211
|
+
}
|
|
212
|
+
extract(archive, tmp);
|
|
213
|
+
fs.unlinkSync(archive);
|
|
214
|
+
const exe = path.join(tmp, `bb${p.exeSuffix}`);
|
|
215
|
+
if (!fs.existsSync(exe)) {
|
|
216
|
+
throw new Error('babashka binary not found after extraction');
|
|
217
|
+
}
|
|
218
|
+
if (process.platform !== 'win32') fs.chmodSync(exe, 0o755);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (!fs.existsSync(bbPath)) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`babashka cache looks corrupt; remove ${finalDir} and retry`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (process.platform !== 'win32') {
|
|
227
|
+
try {
|
|
228
|
+
fs.chmodSync(bbPath, 0o755);
|
|
229
|
+
} catch {
|
|
230
|
+
/* already executable */
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return bbPath;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function ensureJdk(p) {
|
|
237
|
+
const feature = DEFAULT_JDK_VERSION;
|
|
238
|
+
const finalDir = path.join(cacheRoot(), 'jdk', feature);
|
|
239
|
+
const marker = path.join(finalDir, '.javahome');
|
|
240
|
+
|
|
241
|
+
await installOnce(finalDir, async (tmp) => {
|
|
242
|
+
const archive = path.join(tmp, p.jdkArchiveName);
|
|
243
|
+
const url =
|
|
244
|
+
`https://api.adoptium.net/v3/binary/latest/${feature}/ga/` +
|
|
245
|
+
`${p.jdkOs}/${p.jdkArch}/jdk/hotspot/normal/eclipse`;
|
|
246
|
+
log(`Installing Temurin JDK ${feature} (set JDK_VERSION to override)...`);
|
|
247
|
+
try {
|
|
248
|
+
await download(url, archive);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
throw new Error(`${err.message}\n (override with JDK_VERSION=<feature>)`);
|
|
251
|
+
}
|
|
252
|
+
extract(archive, tmp);
|
|
253
|
+
fs.unlinkSync(archive);
|
|
254
|
+
const home = findJavaHome(tmp, p.exeSuffix);
|
|
255
|
+
if (!home) throw new Error('could not locate java in extracted JDK');
|
|
256
|
+
// Path relative to tmp stays valid after tmp is renamed to finalDir.
|
|
257
|
+
fs.writeFileSync(path.join(tmp, '.javahome'), path.relative(tmp, home));
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
let javaHome = null;
|
|
261
|
+
try {
|
|
262
|
+
javaHome = path.join(finalDir, fs.readFileSync(marker, 'utf8').trim());
|
|
263
|
+
} catch {
|
|
264
|
+
javaHome = null;
|
|
265
|
+
}
|
|
266
|
+
if (
|
|
267
|
+
!javaHome ||
|
|
268
|
+
!fs.existsSync(path.join(javaHome, 'bin', `java${p.exeSuffix}`))
|
|
269
|
+
) {
|
|
270
|
+
javaHome = findJavaHome(finalDir, p.exeSuffix);
|
|
271
|
+
}
|
|
272
|
+
if (!javaHome) {
|
|
273
|
+
throw new Error(`JDK cache looks corrupt; remove ${finalDir} and retry`);
|
|
274
|
+
}
|
|
275
|
+
return javaHome;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --- git (Linux only) ----------------------------------------------------
|
|
279
|
+
|
|
280
|
+
function commandWorks(cmd, args) {
|
|
281
|
+
const r = spawnSync(cmd, args, { stdio: 'ignore' });
|
|
282
|
+
return !r.error && r.status === 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ENOENT sets r.error; any exit code otherwise means the binary exists.
|
|
286
|
+
function binExists(cmd) {
|
|
287
|
+
return !spawnSync(cmd, ['--version'], { stdio: 'ignore' }).error;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// On Linux, install git via the system package manager if it is missing.
|
|
291
|
+
// Skipped when git is already on PATH; a no-op on macOS/Windows.
|
|
292
|
+
function ensureGit() {
|
|
293
|
+
if (process.platform !== 'linux') return;
|
|
294
|
+
if (commandWorks('git', ['--version'])) return;
|
|
295
|
+
|
|
296
|
+
const isRoot =
|
|
297
|
+
typeof process.getuid === 'function' && process.getuid() === 0;
|
|
298
|
+
const sudo = isRoot ? [] : binExists('sudo') ? ['sudo'] : null;
|
|
299
|
+
if (sudo === null) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
'git is missing and cannot be installed: not running as root and ' +
|
|
302
|
+
'`sudo` is unavailable.\n' +
|
|
303
|
+
' Install git manually (e.g. `apt-get install git`) and re-run.'
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// First match wins; `soft` lists step indexes allowed to fail (e.g.
|
|
308
|
+
// `apt-get update`, which is non-fatal if package lists already exist).
|
|
309
|
+
const managers = [
|
|
310
|
+
{
|
|
311
|
+
bin: 'apt-get',
|
|
312
|
+
steps: [
|
|
313
|
+
['apt-get', 'update', '-y'],
|
|
314
|
+
['apt-get', 'install', '-y', 'git'],
|
|
315
|
+
],
|
|
316
|
+
soft: [0],
|
|
317
|
+
},
|
|
318
|
+
{ bin: 'dnf', steps: [['dnf', 'install', '-y', 'git']] },
|
|
319
|
+
{ bin: 'yum', steps: [['yum', 'install', '-y', 'git']] },
|
|
320
|
+
{
|
|
321
|
+
bin: 'zypper',
|
|
322
|
+
steps: [['zypper', '--non-interactive', 'install', 'git']],
|
|
323
|
+
},
|
|
324
|
+
{ bin: 'pacman', steps: [['pacman', '-S', '--noconfirm', 'git']] },
|
|
325
|
+
{ bin: 'apk', steps: [['apk', 'add', '--no-cache', 'git']] },
|
|
326
|
+
];
|
|
327
|
+
const pm = managers.find((m) => binExists(m.bin));
|
|
328
|
+
if (!pm) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
'git is missing and no supported package manager ' +
|
|
331
|
+
'(apt-get/dnf/yum/zypper/pacman/apk) was found.\n' +
|
|
332
|
+
' Install git manually and re-run.'
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
log(`Installing git via ${pm.bin}${sudo.length ? ' (sudo)' : ''}...`);
|
|
337
|
+
const env = { ...process.env, DEBIAN_FRONTEND: 'noninteractive' };
|
|
338
|
+
pm.steps.forEach((step, i) => {
|
|
339
|
+
const argv = [...sudo, ...step];
|
|
340
|
+
const r = spawnSync(argv[0], argv.slice(1), { stdio: 'inherit', env });
|
|
341
|
+
const ok = !r.error && r.status === 0;
|
|
342
|
+
if (!ok && !(pm.soft && pm.soft.includes(i))) {
|
|
343
|
+
const why = r.error ? r.error.code : `exit ${r.status}`;
|
|
344
|
+
throw new Error(`git install failed: \`${argv.join(' ')}\` (${why}).`);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
if (!commandWorks('git', ['--version'])) {
|
|
349
|
+
throw new Error('git still not available after the install attempt.');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// --- bb.edn bootstrap ----------------------------------------------------
|
|
354
|
+
|
|
355
|
+
// Env augmented so spawned processes find the cached JDK and bb; nothing system-wide.
|
|
356
|
+
function bbEnv(javaHome, bbPath, extraEnv = {}) {
|
|
357
|
+
const env = { ...process.env, ...extraEnv };
|
|
358
|
+
const pathEntries = [];
|
|
359
|
+
if (javaHome) {
|
|
360
|
+
env.JAVA_HOME = javaHome;
|
|
361
|
+
pathEntries.push(path.join(javaHome, 'bin'));
|
|
362
|
+
}
|
|
363
|
+
if (bbPath) {
|
|
364
|
+
pathEntries.push(path.dirname(bbPath));
|
|
365
|
+
}
|
|
366
|
+
if (pathEntries.length) {
|
|
367
|
+
env.PATH = pathEntries.join(path.delimiter) + path.delimiter + (env.PATH || '');
|
|
368
|
+
}
|
|
369
|
+
return env;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function ghFetch(url, accept) {
|
|
373
|
+
const headers = {
|
|
374
|
+
'user-agent': 'bc-pkg',
|
|
375
|
+
accept: accept || 'application/vnd.github+json',
|
|
376
|
+
'x-github-api-version': '2022-11-28',
|
|
377
|
+
};
|
|
378
|
+
if (process.env.GITHUB_TOKEN) {
|
|
379
|
+
headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
380
|
+
}
|
|
381
|
+
return fetch(url, { headers, redirect: 'follow' });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Reads/edits the EDN with borkdude/rewrite-edn so comments & formatting of
|
|
385
|
+
// untouched nodes survive. Params are passed via env to avoid quoting issues.
|
|
386
|
+
const REWRITE_SCRIPT = `(require '[babashka.deps :as deps])
|
|
387
|
+
(deps/add-deps {:deps {'borkdude/rewrite-edn {:mvn/version (System/getenv "BBEDN_REWRITE_VERSION")}}})
|
|
388
|
+
(require '[borkdude.rewrite-edn :as r])
|
|
389
|
+
|
|
390
|
+
(defn fail! [msg]
|
|
391
|
+
(binding [*out* *err*]
|
|
392
|
+
(println msg))
|
|
393
|
+
(System/exit 1))
|
|
394
|
+
|
|
395
|
+
(defn local-root? [coord]
|
|
396
|
+
(and (map? coord) (contains? coord :local/root)))
|
|
397
|
+
|
|
398
|
+
;; Drop every entry whose effective coord (sexpr respects #_ discard) is a
|
|
399
|
+
;; map containing :local/root. Returns the (possibly unchanged) map node.
|
|
400
|
+
(defn strip-local-root [m-node]
|
|
401
|
+
(if (nil? m-node)
|
|
402
|
+
m-node
|
|
403
|
+
(let [m (r/sexpr m-node)]
|
|
404
|
+
(reduce (fn [acc k]
|
|
405
|
+
(if (local-root? (get m k)) (r/dissoc acc k) acc))
|
|
406
|
+
m-node
|
|
407
|
+
(keys m)))))
|
|
408
|
+
|
|
409
|
+
(let [in (System/getenv "BBEDN_IN")
|
|
410
|
+
out (System/getenv "BBEDN_OUT")
|
|
411
|
+
owner (System/getenv "BBEDN_OWNER")
|
|
412
|
+
proj (System/getenv "BBEDN_PROJECT")
|
|
413
|
+
sha (System/getenv "BBEDN_SHA")
|
|
414
|
+
repo (System/getenv "BBEDN_REPO")
|
|
415
|
+
dep (symbol (str "io.github." owner) proj)
|
|
416
|
+
nodes (r/parse-string (slurp in))
|
|
417
|
+
data (r/sexpr nodes)
|
|
418
|
+
existing-repo (if (and (map? data) (contains? data :repo))
|
|
419
|
+
(:repo data)
|
|
420
|
+
::missing)
|
|
421
|
+
_validate-map (when-not (map? data)
|
|
422
|
+
(fail! "Downloaded bb.edn must contain a top-level EDN map"))
|
|
423
|
+
_validate-repo (when (and (not= ::missing existing-repo)
|
|
424
|
+
(not= existing-repo repo))
|
|
425
|
+
(fail! (str "Downloaded bb.edn :repo " (pr-str existing-repo)
|
|
426
|
+
" does not match CLI repo " (pr-str repo))))
|
|
427
|
+
nodes (if (= ::missing existing-repo) (r/assoc nodes :repo repo) nodes)
|
|
428
|
+
;; 1. strip :local/root from top-level :deps
|
|
429
|
+
nodes (if (r/get nodes :deps)
|
|
430
|
+
(r/update nodes :deps strip-local-root)
|
|
431
|
+
nodes)
|
|
432
|
+
;; 2. strip :local/root from each task's :extra-deps
|
|
433
|
+
tasks (some-> (r/get nodes :tasks) r/sexpr)
|
|
434
|
+
nodes (reduce (fn [acc tk]
|
|
435
|
+
(let [tv (get tasks tk)]
|
|
436
|
+
(if (and (map? tv) (map? (:extra-deps tv)))
|
|
437
|
+
(r/update-in acc [:tasks tk :extra-deps] strip-local-root)
|
|
438
|
+
acc)))
|
|
439
|
+
nodes
|
|
440
|
+
(keys tasks))
|
|
441
|
+
;; 3. ensure :deps exists, then inject the repo as a git dep
|
|
442
|
+
nodes (if (nil? (r/get nodes :deps)) (r/assoc nodes :deps {}) nodes)
|
|
443
|
+
nodes (r/assoc-in nodes [:deps dep] {:git/sha sha})]
|
|
444
|
+
(spit out (str nodes)))
|
|
445
|
+
`;
|
|
446
|
+
|
|
447
|
+
const VALIDATE_REPO_SCRIPT = `(require '[clojure.edn :as edn])
|
|
448
|
+
|
|
449
|
+
(defn fail! [msg]
|
|
450
|
+
(binding [*out* *err*]
|
|
451
|
+
(println msg))
|
|
452
|
+
(System/exit 1))
|
|
453
|
+
|
|
454
|
+
(let [target (System/getenv "BBEDN_TARGET")
|
|
455
|
+
cli-repo (System/getenv "BBEDN_REPO")
|
|
456
|
+
data (try
|
|
457
|
+
(edn/read-string (slurp target))
|
|
458
|
+
(catch Exception e
|
|
459
|
+
(fail! (str "Invalid bb.edn: " (.getMessage e)))))
|
|
460
|
+
has-repo? (and (map? data) (contains? data :repo))
|
|
461
|
+
file-repo (when has-repo? (:repo data))]
|
|
462
|
+
(when-not (map? data)
|
|
463
|
+
(fail! "Invalid bb.edn: expected top-level EDN map"))
|
|
464
|
+
(when-not has-repo?
|
|
465
|
+
(fail! (str "bb.edn exists but does not contain :repo; omit the CLI repo or add :repo "
|
|
466
|
+
(pr-str cli-repo))))
|
|
467
|
+
(when-not (= file-repo cli-repo)
|
|
468
|
+
(fail! (str "bb.edn :repo " (pr-str file-repo)
|
|
469
|
+
" does not match CLI repo " (pr-str cli-repo)))))
|
|
470
|
+
`;
|
|
471
|
+
|
|
472
|
+
// Regex for the "owner/project" slug shape. Anchored, no slashes/spaces/@
|
|
473
|
+
// inside either segment. When present as the first argument, it is consumed as
|
|
474
|
+
// repo identity and never forwarded to bb.
|
|
475
|
+
const REPO_SLUG_RE = /^([^/\s@]+)\/([^/\s@]+)$/;
|
|
476
|
+
|
|
477
|
+
function validateBbEdnRepo(bbPath, javaHome, target, repo) {
|
|
478
|
+
const targetPath = path.resolve(target);
|
|
479
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'bb-edn-validate-'));
|
|
480
|
+
try {
|
|
481
|
+
const script = path.join(tmp, 'validate-repo.clj');
|
|
482
|
+
fs.writeFileSync(script, VALIDATE_REPO_SCRIPT);
|
|
483
|
+
const r = spawnSync(bbPath, [script], {
|
|
484
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
485
|
+
encoding: 'utf8',
|
|
486
|
+
cwd: tmp,
|
|
487
|
+
env: bbEnv(javaHome, bbPath, {
|
|
488
|
+
BBEDN_TARGET: targetPath,
|
|
489
|
+
BBEDN_REPO: repo,
|
|
490
|
+
}),
|
|
491
|
+
});
|
|
492
|
+
if (r.error || r.status !== 0) {
|
|
493
|
+
const details = [r.stderr, r.stdout]
|
|
494
|
+
.filter(Boolean)
|
|
495
|
+
.map((s) => s.trim())
|
|
496
|
+
.filter(Boolean)
|
|
497
|
+
.join('\n');
|
|
498
|
+
const why = r.error ? r.error.message : `exit ${r.status}`;
|
|
499
|
+
throw new Error(details || `bb.edn repo validation failed (${why})`);
|
|
500
|
+
}
|
|
501
|
+
} finally {
|
|
502
|
+
rmrf(tmp);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// If bb.edn exists and a CLI repo was provided, validate its top-level :repo.
|
|
507
|
+
// Otherwise, when cwd has no bb.edn and `repo` is "owner/project", fetch that
|
|
508
|
+
// repo's bb.edn (pinned to its default-branch HEAD), inject top-level :repo,
|
|
509
|
+
// and add the repo itself as an io.github git dep.
|
|
510
|
+
async function ensureBbEdn(bbPath, javaHome, repo) {
|
|
511
|
+
let owner = null;
|
|
512
|
+
let project = null;
|
|
513
|
+
let slug = null;
|
|
514
|
+
if (repo) {
|
|
515
|
+
const m = repo.match(REPO_SLUG_RE);
|
|
516
|
+
if (!m) {
|
|
517
|
+
throw new Error(`repo must be "owner/project" (got "${repo}")`);
|
|
518
|
+
}
|
|
519
|
+
owner = m[1];
|
|
520
|
+
project = m[2];
|
|
521
|
+
slug = `${owner}/${project}`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const target = path.join(process.cwd(), 'bb.edn');
|
|
525
|
+
if (fs.existsSync(target)) {
|
|
526
|
+
if (repo) validateBbEdnRepo(bbPath, javaHome, target, slug);
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
if (!repo) return null; // step disabled — proceed straight to bb
|
|
530
|
+
|
|
531
|
+
const api = `https://api.github.com/repos/${owner}/${project}`;
|
|
532
|
+
|
|
533
|
+
const cr = await ghFetch(`${api}/commits?per_page=1`);
|
|
534
|
+
if (cr.status === 404) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
`${slug} not found or not accessible ` +
|
|
537
|
+
`(set GITHUB_TOKEN for private repos)`
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
if (!cr.ok) {
|
|
541
|
+
throw new Error(`GitHub API error ${cr.status} resolving ${slug}`);
|
|
542
|
+
}
|
|
543
|
+
const commits = await cr.json();
|
|
544
|
+
const sha = Array.isArray(commits) && commits[0] && commits[0].sha;
|
|
545
|
+
if (!sha) throw new Error(`${slug} has no commits`);
|
|
546
|
+
|
|
547
|
+
const fr = await ghFetch(
|
|
548
|
+
`${api}/contents/bb.edn?ref=${sha}`,
|
|
549
|
+
'application/vnd.github.raw'
|
|
550
|
+
);
|
|
551
|
+
if (fr.status === 404) {
|
|
552
|
+
throw new Error(`${slug} (at ${sha.slice(0, 7)}) has no bb.edn`);
|
|
553
|
+
}
|
|
554
|
+
if (!fr.ok) {
|
|
555
|
+
throw new Error(`GitHub API error ${fr.status} fetching bb.edn from ${slug}`);
|
|
556
|
+
}
|
|
557
|
+
const ednText = await fr.text();
|
|
558
|
+
|
|
559
|
+
log(`Bootstrapping bb.edn from ${slug}@${sha.slice(0, 7)}...`);
|
|
560
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'bb-edn-'));
|
|
561
|
+
try {
|
|
562
|
+
const inFile = path.join(tmp, 'in.edn');
|
|
563
|
+
const script = path.join(tmp, 'rewrite.clj');
|
|
564
|
+
fs.writeFileSync(inFile, ednText);
|
|
565
|
+
fs.writeFileSync(script, REWRITE_SCRIPT);
|
|
566
|
+
const env = bbEnv(javaHome, bbPath);
|
|
567
|
+
Object.assign(env, {
|
|
568
|
+
BBEDN_IN: inFile,
|
|
569
|
+
BBEDN_OUT: target,
|
|
570
|
+
BBEDN_OWNER: owner,
|
|
571
|
+
BBEDN_PROJECT: project,
|
|
572
|
+
BBEDN_REPO: slug,
|
|
573
|
+
BBEDN_SHA: sha,
|
|
574
|
+
BBEDN_REWRITE_VERSION: REWRITE_EDN_VERSION,
|
|
575
|
+
});
|
|
576
|
+
const r = spawnSync(bbPath, [script], {
|
|
577
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
578
|
+
encoding: 'utf8',
|
|
579
|
+
cwd: tmp,
|
|
580
|
+
env,
|
|
581
|
+
});
|
|
582
|
+
if (r.error || r.status !== 0) {
|
|
583
|
+
const details = [r.stderr, r.stdout]
|
|
584
|
+
.filter(Boolean)
|
|
585
|
+
.map((s) => s.trim())
|
|
586
|
+
.filter(Boolean)
|
|
587
|
+
.join('\n');
|
|
588
|
+
const why = r.error ? r.error.message : `exit ${r.status}`;
|
|
589
|
+
throw new Error(
|
|
590
|
+
details || `failed to write bb.edn via rewrite-edn (${why})`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
if (!fs.existsSync(target)) {
|
|
594
|
+
throw new Error('rewrite-edn step did not produce a bb.edn');
|
|
595
|
+
}
|
|
596
|
+
return { owner, project, sha };
|
|
597
|
+
} finally {
|
|
598
|
+
rmrf(tmp);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// --- Run bb --------------------------------------------------------------
|
|
603
|
+
|
|
604
|
+
function runBb(bbPath, args, javaHome, extraEnv = {}) {
|
|
605
|
+
const env = bbEnv(javaHome, bbPath, extraEnv);
|
|
606
|
+
|
|
607
|
+
const child = spawn(bbPath, args, {
|
|
608
|
+
stdio: 'inherit',
|
|
609
|
+
cwd: process.cwd(),
|
|
610
|
+
env,
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const forward = (sig) => {
|
|
614
|
+
try {
|
|
615
|
+
child.kill(sig);
|
|
616
|
+
} catch {
|
|
617
|
+
/* child already gone */
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
process.on('SIGINT', forward);
|
|
621
|
+
process.on('SIGTERM', forward);
|
|
622
|
+
|
|
623
|
+
return new Promise((resolve) => {
|
|
624
|
+
child.on('error', (err) => {
|
|
625
|
+
log(`failed to start bb: ${err.message}`);
|
|
626
|
+
resolve(127);
|
|
627
|
+
});
|
|
628
|
+
child.on('exit', (code, signal) => {
|
|
629
|
+
resolve(signal ? 1 : code == null ? 1 : code);
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function main(args) {
|
|
635
|
+
const p = resolvePlatform();
|
|
636
|
+
// Consume the first positional argument as repo identity whenever it has
|
|
637
|
+
// the shape "owner/project". It is never forwarded to bb: with no bb.edn it
|
|
638
|
+
// bootstraps one, and with an existing bb.edn it validates top-level :repo.
|
|
639
|
+
let repo = null;
|
|
640
|
+
if (args.length && REPO_SLUG_RE.test(args[0])) {
|
|
641
|
+
repo = args[0];
|
|
642
|
+
args = args.slice(1);
|
|
643
|
+
}
|
|
644
|
+
const bbPath = await ensureBabashka(p);
|
|
645
|
+
const javaHome = await ensureJdk(p);
|
|
646
|
+
ensureGit();
|
|
647
|
+
const bootstrapped = await ensureBbEdn(bbPath, javaHome, repo);
|
|
648
|
+
const extraEnv = bootstrapped ? { BB_BOOTSTRAP_SHA: bootstrapped.sha } : {};
|
|
649
|
+
const code = await runBb(bbPath, args, javaHome, extraEnv);
|
|
650
|
+
process.exit(code);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (require.main === module) {
|
|
654
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
655
|
+
log(err && err.message ? err.message : String(err));
|
|
656
|
+
process.exit(1);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Exported for tests / inspection.
|
|
661
|
+
module.exports = {
|
|
662
|
+
resolvePlatform,
|
|
663
|
+
cacheRoot,
|
|
664
|
+
findJavaHome,
|
|
665
|
+
ensureGit,
|
|
666
|
+
validateBbEdnRepo,
|
|
667
|
+
ensureBbEdn,
|
|
668
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bc-pkg",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bootstrap and run babashka (bb): installs babashka and a Temurin JDK on first use if missing, then forwards all arguments to bb.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"bc-pkg": "bin/bc-pkg.js"
|
|
7
|
+
},
|
|
8
|
+
"files": ["bin/", "README.md"],
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["babashka", "bb", "clojure", "jdk", "cli"],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "commonjs"
|
|
15
|
+
}
|