bc-pkg 1.0.0 → 1.0.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 -139
- package/bin/bc-pkg.js +445 -421
- package/package.json +13 -13
package/README.md
CHANGED
|
@@ -1,163 +1,65 @@
|
|
|
1
1
|
# bc-pkg
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
`bc-pkg` creates or reuses a BigConfig CLI in the current directory, then
|
|
4
|
+
forwards your command to that CLI.
|
|
5
|
+
|
|
6
|
+
The npm package and the PyPI package expose the same behavior. The target
|
|
7
|
+
package can be implemented in Clojure, TypeScript, or Python; `bc-pkg` infers
|
|
8
|
+
that language from the pinned GitHub content.
|
|
7
9
|
|
|
8
10
|
## Usage
|
|
9
11
|
|
|
10
12
|
```sh
|
|
11
|
-
npx bc-pkg@
|
|
12
|
-
npx bc-pkg
|
|
13
|
-
npx bc-pkg@latest <owner>/<project> <args...> # bootstrap/validate bb.edn, then -> bb <args...>
|
|
13
|
+
npx bc-pkg <owner/repo@ref> package validate
|
|
14
|
+
npx bc-pkg package validate
|
|
14
15
|
```
|
|
15
16
|
|
|
16
|
-
|
|
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.
|
|
17
|
+
`ref` can be a branch name or a full 40-character commit SHA:
|
|
73
18
|
|
|
74
19
|
```sh
|
|
75
|
-
npx bc-pkg
|
|
20
|
+
npx bc-pkg bigconfig-ai/once@typescript package validate
|
|
21
|
+
npx bc-pkg bigconfig-ai/once@2f4e8c0d0b4c4b8f0c3a9f6e2a1b5c7d8e9f0123 package validate
|
|
76
22
|
```
|
|
77
23
|
|
|
78
|
-
|
|
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` |
|
|
24
|
+
On the first run, `bc-pkg` resolves the ref to a full SHA and pins it. Later
|
|
25
|
+
runs omit `<owner/repo@ref>` and keep using the pinned SHA.
|
|
97
26
|
|
|
98
|
-
##
|
|
27
|
+
## What is created
|
|
99
28
|
|
|
100
|
-
|
|
29
|
+
The launcher copies the target package's root `run` file into the current
|
|
30
|
+
directory and writes language-native metadata:
|
|
101
31
|
|
|
102
|
-
|
|
32
|
+
| Target language | Manifest | Runtime command |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| Clojure | `deps.edn` | `bb run ...` |
|
|
35
|
+
| TypeScript | `package.json` | `node run ...` |
|
|
36
|
+
| Python | `pyproject.toml` | `uv run python run ...` |
|
|
103
37
|
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
38
|
+
For Clojure targets, a small `bb.edn` runtime dependency file is also written so
|
|
39
|
+
Babashka can load the pinned Git dependency.
|
|
110
40
|
|
|
111
|
-
|
|
41
|
+
If the directory is already initialized for a different repo/ref/SHA, `bc-pkg`
|
|
42
|
+
exits with an error instead of updating it implicitly.
|
|
112
43
|
|
|
113
|
-
|
|
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.
|
|
44
|
+
## Requirements
|
|
119
45
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
46
|
+
- npm launcher: Node.js >= 18.
|
|
47
|
+
- TypeScript target packages: Node.js and npm must already be installed.
|
|
48
|
+
- Python target packages: Python and `uv` must already be installed.
|
|
49
|
+
- Clojure target packages: `bc-pkg` downloads pinned Babashka and Temurin JDK
|
|
50
|
+
versions into a shared user cache and checks/installs `git` on Linux.
|
|
124
51
|
|
|
125
|
-
|
|
126
|
-
mkdir empty && cd empty
|
|
127
|
-
npx bc-pkg@latest bigconfig-ai/bc-pkg shell
|
|
128
|
-
```
|
|
52
|
+
## Environment
|
|
129
53
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
bb shell --skip-build # reuse the existing image
|
|
136
|
-
```
|
|
54
|
+
| Variable | Effect |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `GITHUB_TOKEN` | Used for private GitHub repos or higher API rate limits. |
|
|
57
|
+
| `BB_VERSION` | Override the Babashka version for Clojure targets. |
|
|
58
|
+
| `JDK_VERSION` | Override the Temurin JDK feature version for Clojure targets. |
|
|
137
59
|
|
|
138
|
-
|
|
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.
|
|
60
|
+
## Cache
|
|
160
61
|
|
|
161
|
-
|
|
62
|
+
Babashka and JDK downloads are shared across projects under:
|
|
162
63
|
|
|
163
|
-
|
|
64
|
+
- macOS/Linux: `$XDG_CACHE_HOME/bc-pkg` or `~/.cache/bc-pkg`
|
|
65
|
+
- Windows: `%LOCALAPPDATA%/bc-pkg`
|
package/bin/bc-pkg.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
// bc-pkg —
|
|
5
|
-
//
|
|
4
|
+
// bc-pkg — creates/reuses a BigConfig CLI in the current directory and runs it.
|
|
5
|
+
// The target package can be implemented in Clojure, TypeScript, or Python. The
|
|
6
|
+
// target language is inferred from the package's pinned GitHub content.
|
|
6
7
|
|
|
7
8
|
const fs = require('fs');
|
|
8
9
|
const os = require('os');
|
|
@@ -11,24 +12,403 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
11
12
|
const { Readable } = require('stream');
|
|
12
13
|
const { pipeline } = require('stream/promises');
|
|
13
14
|
|
|
14
|
-
// Pinned, known-good versions. Overridable via env.
|
|
15
15
|
const DEFAULT_BB_VERSION = process.env.BB_VERSION || '1.12.196';
|
|
16
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
17
|
const TAG = '[bc-pkg]';
|
|
18
|
+
const FULL_SHA_RE = /^[0-9a-fA-F]{40}$/;
|
|
19
|
+
const SPEC_RE = /^([^/\s@]+)\/([^/\s@]+)@([^\s]+)$/;
|
|
20
20
|
|
|
21
21
|
function log(msg) {
|
|
22
|
-
// stderr so stdout stays clean for bb's own output.
|
|
23
22
|
process.stderr.write(`${TAG} ${msg}\n`);
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
function fail(msg) {
|
|
26
|
+
throw new Error(msg);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function usage() {
|
|
30
|
+
return `Usage:\n bc-pkg <owner/repo@ref> <args...>\n bc-pkg <args...>\n\nExamples:\n npx bc-pkg bigconfig-ai/once@typescript package validate\n npx bc-pkg package validate`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- generic process helpers -------------------------------------------
|
|
34
|
+
|
|
35
|
+
function commandWorks(cmd, args = ['--version']) {
|
|
36
|
+
const r = spawnSync(cmd, args, { stdio: 'ignore' });
|
|
37
|
+
return !r.error && r.status === 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function binExists(cmd) {
|
|
41
|
+
return !spawnSync(cmd, ['--version'], { stdio: 'ignore' }).error;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runCommand(cmd, args, options = {}) {
|
|
45
|
+
const child = spawn(cmd, args, {
|
|
46
|
+
stdio: 'inherit',
|
|
47
|
+
cwd: process.cwd(),
|
|
48
|
+
env: process.env,
|
|
49
|
+
...options,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const forward = (sig) => {
|
|
53
|
+
try {
|
|
54
|
+
child.kill(sig);
|
|
55
|
+
} catch {
|
|
56
|
+
// child already gone
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
process.on('SIGINT', forward);
|
|
60
|
+
process.on('SIGTERM', forward);
|
|
61
|
+
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
child.on('error', (err) => {
|
|
64
|
+
log(`failed to start ${cmd}: ${err.message}`);
|
|
65
|
+
resolve(127);
|
|
66
|
+
});
|
|
67
|
+
child.on('exit', (code, signal) => {
|
|
68
|
+
resolve(signal ? 1 : code == null ? 1 : code);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function whichPython() {
|
|
74
|
+
if (commandWorks('python3')) return 'python3';
|
|
75
|
+
if (commandWorks('python')) return 'python';
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function requireCommand(cmd, installHint) {
|
|
80
|
+
if (!commandWorks(cmd)) {
|
|
81
|
+
fail(`${cmd} is required but was not found on PATH.${installHint ? `\n ${installHint}` : ''}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- GitHub --------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
function parseSpec(arg) {
|
|
88
|
+
const m = arg && arg.match(SPEC_RE);
|
|
89
|
+
if (!m) return null;
|
|
90
|
+
return { owner: m[1], repo: m[2], ref: m[3], slug: `${m[1]}/${m[2]}` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ghHeaders(accept) {
|
|
94
|
+
const headers = {
|
|
95
|
+
'user-agent': 'bc-pkg',
|
|
96
|
+
accept: accept || 'application/vnd.github+json',
|
|
97
|
+
'x-github-api-version': '2022-11-28',
|
|
98
|
+
};
|
|
99
|
+
if (process.env.GITHUB_TOKEN) {
|
|
100
|
+
headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
101
|
+
}
|
|
102
|
+
return headers;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function ghFetch(url, accept) {
|
|
106
|
+
return fetch(url, { headers: ghHeaders(accept), redirect: 'follow' });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function resolveRef(spec) {
|
|
110
|
+
if (FULL_SHA_RE.test(spec.ref)) {
|
|
111
|
+
return spec.ref.toLowerCase();
|
|
112
|
+
}
|
|
113
|
+
const url = `https://api.github.com/repos/${spec.owner}/${spec.repo}/commits/${encodeURIComponent(spec.ref)}`;
|
|
114
|
+
const res = await ghFetch(url);
|
|
115
|
+
if (res.status === 404) {
|
|
116
|
+
fail(`${spec.slug}@${spec.ref} not found or not accessible (set GITHUB_TOKEN for private repos)`);
|
|
117
|
+
}
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
fail(`GitHub API error ${res.status} resolving ${spec.slug}@${spec.ref}`);
|
|
120
|
+
}
|
|
121
|
+
const data = await res.json();
|
|
122
|
+
if (!data || !data.sha) fail(`${spec.slug}@${spec.ref} did not resolve to a commit`);
|
|
123
|
+
return String(data.sha).toLowerCase();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function fetchFile(spec, sha, filePath, { required = false } = {}) {
|
|
127
|
+
const url = `https://api.github.com/repos/${spec.owner}/${spec.repo}/contents/${filePath}?ref=${sha}`;
|
|
128
|
+
const res = await ghFetch(url, 'application/vnd.github.raw');
|
|
129
|
+
if (res.status === 404) {
|
|
130
|
+
if (required) fail(`${spec.slug}@${sha.slice(0, 7)} has no ${filePath}`);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
fail(`GitHub API error ${res.status} fetching ${filePath} from ${spec.slug}@${sha.slice(0, 7)}`);
|
|
135
|
+
}
|
|
136
|
+
return await res.text();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseJson(text, label) {
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(text);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
fail(`Invalid JSON in ${label}: ${err.message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parsePyProjectName(text) {
|
|
148
|
+
const project = sectionText(text, 'project');
|
|
149
|
+
const m = project && project.match(/^\s*name\s*=\s*["']([^"']+)["']/m);
|
|
150
|
+
return m ? m[1] : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sectionText(text, name) {
|
|
154
|
+
const re = new RegExp(`^\\s*\\[${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]\\s*$`, 'm');
|
|
155
|
+
const m = re.exec(text);
|
|
156
|
+
if (!m) return null;
|
|
157
|
+
const start = m.index + m[0].length;
|
|
158
|
+
const rest = text.slice(start);
|
|
159
|
+
const next = /^\s*\[[^\]]+\]\s*$/m.exec(rest);
|
|
160
|
+
return next ? rest.slice(0, next.index) : rest;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function detectTarget(spec, sha) {
|
|
164
|
+
const [depsEdn, packageJsonText, pyprojectText] = await Promise.all([
|
|
165
|
+
fetchFile(spec, sha, 'deps.edn'),
|
|
166
|
+
fetchFile(spec, sha, 'package.json'),
|
|
167
|
+
fetchFile(spec, sha, 'pyproject.toml'),
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const found = [];
|
|
171
|
+
if (depsEdn != null) found.push('clojure');
|
|
172
|
+
if (packageJsonText != null) found.push('typescript');
|
|
173
|
+
if (pyprojectText != null) found.push('python');
|
|
174
|
+
if (found.length === 0) {
|
|
175
|
+
fail(`${spec.slug}@${sha.slice(0, 7)} has no deps.edn, package.json, or pyproject.toml`);
|
|
176
|
+
}
|
|
177
|
+
if (found.length > 1) {
|
|
178
|
+
fail(`${spec.slug}@${sha.slice(0, 7)} is ambiguous; found ${found.join(', ')} manifests`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (found[0] === 'typescript') {
|
|
182
|
+
const pkg = parseJson(packageJsonText, 'package.json');
|
|
183
|
+
if (!pkg.name) fail(`${spec.slug}@${sha.slice(0, 7)} package.json has no name`);
|
|
184
|
+
return { language: 'typescript', packageName: pkg.name };
|
|
185
|
+
}
|
|
186
|
+
if (found[0] === 'python') {
|
|
187
|
+
const packageName = parsePyProjectName(pyprojectText);
|
|
188
|
+
if (!packageName) fail(`${spec.slug}@${sha.slice(0, 7)} pyproject.toml has no [project].name`);
|
|
189
|
+
return { language: 'python', packageName };
|
|
190
|
+
}
|
|
191
|
+
return { language: 'clojure', packageName: `io.github.${spec.owner}/${spec.repo}` };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- native metadata -----------------------------------------------------
|
|
195
|
+
|
|
196
|
+
function metadataFromPackageJson(file) {
|
|
197
|
+
if (!fs.existsSync(file)) return null;
|
|
198
|
+
const pkg = parseJson(fs.readFileSync(file, 'utf8'), file);
|
|
199
|
+
if (!pkg.bigconfig) return null;
|
|
200
|
+
return { ...pkg.bigconfig, language: pkg.bigconfig.language || 'typescript', manifest: file };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function metadataFromPyproject(file) {
|
|
204
|
+
if (!fs.existsSync(file)) return null;
|
|
205
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
206
|
+
const sec = sectionText(text, 'tool.bigconfig');
|
|
207
|
+
if (!sec) return null;
|
|
208
|
+
const get = (key) => {
|
|
209
|
+
const m = sec.match(new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=\\s*["']([^"']+)["']`, 'm'));
|
|
210
|
+
return m ? m[1] : undefined;
|
|
211
|
+
};
|
|
212
|
+
return {
|
|
213
|
+
repo: get('repo'),
|
|
214
|
+
ref: get('ref'),
|
|
215
|
+
sha: get('sha'),
|
|
216
|
+
language: get('language') || 'python',
|
|
217
|
+
run: get('run') || 'run',
|
|
218
|
+
packageName: get('package-name'),
|
|
219
|
+
manifest: file,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function metadataFromDepsEdn(file) {
|
|
224
|
+
if (!fs.existsSync(file)) return null;
|
|
225
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
226
|
+
if (!text.includes(':bigconfig/repo')) return null;
|
|
227
|
+
const get = (key) => {
|
|
228
|
+
const m = text.match(new RegExp(`:${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s+"([^"]+)"`));
|
|
229
|
+
return m ? m[1] : undefined;
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
repo: get('bigconfig/repo'),
|
|
233
|
+
ref: get('bigconfig/ref'),
|
|
234
|
+
sha: get('bigconfig/sha'),
|
|
235
|
+
language: get('bigconfig/language') || 'clojure',
|
|
236
|
+
run: get('bigconfig/run') || 'run',
|
|
237
|
+
manifest: file,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function readMetadata(cwd = process.cwd()) {
|
|
242
|
+
const metas = [
|
|
243
|
+
metadataFromDepsEdn(path.join(cwd, 'deps.edn')),
|
|
244
|
+
metadataFromPackageJson(path.join(cwd, 'package.json')),
|
|
245
|
+
metadataFromPyproject(path.join(cwd, 'pyproject.toml')),
|
|
246
|
+
].filter(Boolean);
|
|
247
|
+
if (metas.length > 1) {
|
|
248
|
+
fail('Multiple BigConfig metadata files found; keep only one of deps.edn, package.json, or pyproject.toml initialized for bc-pkg.');
|
|
249
|
+
}
|
|
250
|
+
if (metas.length === 0) return null;
|
|
251
|
+
const meta = metas[0];
|
|
252
|
+
if (!meta.repo || !meta.ref || !meta.sha || !meta.language) {
|
|
253
|
+
fail(`Incomplete BigConfig metadata in ${meta.manifest}`);
|
|
254
|
+
}
|
|
255
|
+
return meta;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function validateExistingMetadata(meta, spec, sha) {
|
|
259
|
+
const expectedRepo = spec.slug;
|
|
260
|
+
const problems = [];
|
|
261
|
+
if (meta.repo !== expectedRepo) problems.push(`repo ${JSON.stringify(meta.repo)} != ${JSON.stringify(expectedRepo)}`);
|
|
262
|
+
if (meta.ref !== spec.ref) problems.push(`ref ${JSON.stringify(meta.ref)} != ${JSON.stringify(spec.ref)}`);
|
|
263
|
+
if (String(meta.sha).toLowerCase() !== sha.toLowerCase()) problems.push(`sha ${meta.sha} != ${sha}`);
|
|
264
|
+
if (problems.length) {
|
|
265
|
+
fail(`Current directory is already initialized for a different BigConfig package:\n ${problems.join('\n ')}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function quoteToml(s) {
|
|
270
|
+
return JSON.stringify(String(s));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function writeRunFile(text) {
|
|
274
|
+
const target = path.join(process.cwd(), 'run');
|
|
275
|
+
fs.writeFileSync(target, text);
|
|
276
|
+
if (process.platform !== 'win32') fs.chmodSync(target, 0o755);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function clojureCoord(spec) {
|
|
280
|
+
return `io.github.${spec.owner}/${spec.repo}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function writeClojureManifest(spec, sha, target) {
|
|
284
|
+
const coord = clojureCoord(spec);
|
|
285
|
+
const gitUrl = `https://github.com/${spec.owner}/${spec.repo}.git`;
|
|
286
|
+
const deps = `{:deps {${coord} {:git/url "${gitUrl}"\n :git/sha "${sha}"}}\n :bigconfig/repo "${spec.slug}"\n :bigconfig/ref "${spec.ref}"\n :bigconfig/sha "${sha}"\n :bigconfig/language "clojure"\n :bigconfig/run "run"}\n`;
|
|
287
|
+
fs.writeFileSync(path.join(process.cwd(), 'deps.edn'), deps);
|
|
288
|
+
|
|
289
|
+
// Babashka script execution reads bb.edn, not deps.edn. Metadata remains in
|
|
290
|
+
// deps.edn per the launcher contract; bb.edn is the runtime dependency file.
|
|
291
|
+
const bb = `{:deps {${coord} {:git/url "${gitUrl}"\n :git/sha "${sha}"}}}\n`;
|
|
292
|
+
fs.writeFileSync(path.join(process.cwd(), 'bb.edn'), bb);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function writeTypeScriptManifest(spec, sha, target) {
|
|
296
|
+
const file = path.join(process.cwd(), 'package.json');
|
|
297
|
+
let pkg = {};
|
|
298
|
+
if (fs.existsSync(file)) {
|
|
299
|
+
pkg = parseJson(fs.readFileSync(file, 'utf8'), file);
|
|
300
|
+
if (pkg.bigconfig) validateExistingMetadata(pkg.bigconfig, spec, sha);
|
|
301
|
+
}
|
|
302
|
+
pkg.type = pkg.type || 'module';
|
|
303
|
+
pkg.scripts = { ...(pkg.scripts || {}), run: 'node run' };
|
|
304
|
+
pkg.dependencies = { ...(pkg.dependencies || {}) };
|
|
305
|
+
pkg.dependencies[target.packageName] = `github:${spec.owner}/${spec.repo}#${sha}`;
|
|
306
|
+
pkg.bigconfig = {
|
|
307
|
+
repo: spec.slug,
|
|
308
|
+
ref: spec.ref,
|
|
309
|
+
sha,
|
|
310
|
+
language: 'typescript',
|
|
311
|
+
run: 'run',
|
|
312
|
+
packageName: target.packageName,
|
|
313
|
+
};
|
|
314
|
+
fs.writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function writePythonManifest(spec, sha, target) {
|
|
318
|
+
const file = path.join(process.cwd(), 'pyproject.toml');
|
|
319
|
+
if (fs.existsSync(file)) {
|
|
320
|
+
const existing = metadataFromPyproject(file);
|
|
321
|
+
if (!existing) {
|
|
322
|
+
fail('pyproject.toml already exists and is not initialized for bc-pkg; refusing to rewrite it.');
|
|
323
|
+
}
|
|
324
|
+
validateExistingMetadata(existing, spec, sha);
|
|
325
|
+
}
|
|
326
|
+
const dep = `${target.packageName} @ git+https://github.com/${spec.owner}/${spec.repo}.git@${sha}`;
|
|
327
|
+
const text = `[project]\nname = "bigconfig-cli"\nversion = "0.1.0"\nrequires-python = ">=3.12"\ndependencies = [\n ${quoteToml(dep)},\n]\n\n[tool.bigconfig]\nrepo = ${quoteToml(spec.slug)}\nref = ${quoteToml(spec.ref)}\nsha = ${quoteToml(sha)}\nlanguage = "python"\nrun = "run"\npackage-name = ${quoteToml(target.packageName)}\n`;
|
|
328
|
+
fs.writeFileSync(file, text);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function writeNativeManifest(spec, sha, target) {
|
|
332
|
+
if (target.language === 'clojure') return writeClojureManifest(spec, sha, target);
|
|
333
|
+
if (target.language === 'typescript') return writeTypeScriptManifest(spec, sha, target);
|
|
334
|
+
if (target.language === 'python') return writePythonManifest(spec, sha, target);
|
|
335
|
+
fail(`Unsupported language: ${target.language}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// --- target dependency setup and execution -------------------------------
|
|
339
|
+
|
|
340
|
+
async function ensureTargetDeps(meta) {
|
|
341
|
+
if (meta.language === 'typescript') {
|
|
342
|
+
requireCommand('node', 'Install Node.js and try again.');
|
|
343
|
+
requireCommand('npm', 'Install npm and try again.');
|
|
344
|
+
if (!fs.existsSync(path.join(process.cwd(), 'node_modules'))) {
|
|
345
|
+
log('Installing TypeScript target dependencies with npm install...');
|
|
346
|
+
const code = await runCommand('npm', ['install']);
|
|
347
|
+
if (code !== 0) process.exit(code);
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (meta.language === 'python') {
|
|
352
|
+
const py = whichPython();
|
|
353
|
+
if (!py) fail('python3 or python is required but was not found on PATH.');
|
|
354
|
+
requireCommand('uv', 'Install uv and try again.');
|
|
355
|
+
if (!fs.existsSync(path.join(process.cwd(), '.venv'))) {
|
|
356
|
+
log('Installing Python target dependencies with uv sync...');
|
|
357
|
+
const code = await runCommand('uv', ['sync']);
|
|
358
|
+
if (code !== 0) process.exit(code);
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function runTarget(meta, args) {
|
|
365
|
+
if (meta.language === 'typescript') {
|
|
366
|
+
await ensureTargetDeps(meta);
|
|
367
|
+
return await runCommand('node', ['run', ...args]);
|
|
368
|
+
}
|
|
369
|
+
if (meta.language === 'python') {
|
|
370
|
+
await ensureTargetDeps(meta);
|
|
371
|
+
return await runCommand('uv', ['run', 'python', meta.run || 'run', ...args]);
|
|
372
|
+
}
|
|
373
|
+
if (meta.language === 'clojure') {
|
|
374
|
+
const p = resolvePlatform();
|
|
375
|
+
const bbPath = await ensureBabashka(p);
|
|
376
|
+
const javaHome = await ensureJdk(p);
|
|
377
|
+
ensureGit();
|
|
378
|
+
return await runBb(bbPath, ['run', ...args], javaHome);
|
|
379
|
+
}
|
|
380
|
+
fail(`Unsupported language: ${meta.language}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function initialize(spec, sha) {
|
|
384
|
+
const target = await detectTarget(spec, sha);
|
|
385
|
+
const runText = await fetchFile(spec, sha, 'run', { required: true });
|
|
386
|
+
writeRunFile(runText);
|
|
387
|
+
writeNativeManifest(spec, sha, target);
|
|
388
|
+
return {
|
|
389
|
+
repo: spec.slug,
|
|
390
|
+
ref: spec.ref,
|
|
391
|
+
sha,
|
|
392
|
+
language: target.language,
|
|
393
|
+
run: 'run',
|
|
394
|
+
packageName: target.packageName,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function restoreRunIfMissing(meta) {
|
|
399
|
+
const runPath = path.join(process.cwd(), meta.run || 'run');
|
|
400
|
+
if (fs.existsSync(runPath)) return;
|
|
401
|
+
const [owner, repo] = meta.repo.split('/');
|
|
402
|
+
const spec = { owner, repo, slug: meta.repo, ref: meta.ref };
|
|
403
|
+
const runText = await fetchFile(spec, meta.sha, 'run', { required: true });
|
|
404
|
+
writeRunFile(runText);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// --- Babashka/JDK bootstrap for Clojure targets --------------------------
|
|
27
408
|
|
|
28
|
-
// Maps the host OS/arch to babashka release asset + Adoptium API parameters.
|
|
29
409
|
function resolvePlatform() {
|
|
30
|
-
const plat = process.platform;
|
|
31
|
-
const arch = process.arch;
|
|
410
|
+
const plat = process.platform;
|
|
411
|
+
const arch = process.arch;
|
|
32
412
|
const exeSuffix = plat === 'win32' ? '.exe' : '';
|
|
33
413
|
|
|
34
414
|
let bbOs;
|
|
@@ -47,7 +427,7 @@ function resolvePlatform() {
|
|
|
47
427
|
jdkOs = 'windows';
|
|
48
428
|
archiveExt = 'zip';
|
|
49
429
|
} else {
|
|
50
|
-
|
|
430
|
+
fail(`Unsupported OS: ${plat}`);
|
|
51
431
|
}
|
|
52
432
|
|
|
53
433
|
let bbArch;
|
|
@@ -59,17 +439,11 @@ function resolvePlatform() {
|
|
|
59
439
|
bbArch = 'amd64';
|
|
60
440
|
jdkArch = 'x64';
|
|
61
441
|
} else {
|
|
62
|
-
|
|
442
|
+
fail(`Unsupported CPU architecture: ${arch}`);
|
|
63
443
|
}
|
|
64
444
|
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
}
|
|
445
|
+
const bbArchToken = plat === 'linux' && arch === 'arm64' ? 'aarch64-static' : bbArch;
|
|
446
|
+
if (plat === 'win32' && arch === 'arm64') fail('babashka has no prebuilt Windows arm64 binary');
|
|
73
447
|
|
|
74
448
|
return {
|
|
75
449
|
exeSuffix,
|
|
@@ -84,22 +458,16 @@ function resolvePlatform() {
|
|
|
84
458
|
}
|
|
85
459
|
|
|
86
460
|
function cacheRoot() {
|
|
87
|
-
const base =
|
|
88
|
-
process.
|
|
89
|
-
|
|
90
|
-
: process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
461
|
+
const base = process.platform === 'win32'
|
|
462
|
+
? process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
|
|
463
|
+
: process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
91
464
|
return path.join(base, 'bc-pkg');
|
|
92
465
|
}
|
|
93
466
|
|
|
94
|
-
// --- Filesystem helpers --------------------------------------------------
|
|
95
|
-
|
|
96
467
|
function rmrf(target) {
|
|
97
468
|
fs.rmSync(target, { recursive: true, force: true });
|
|
98
469
|
}
|
|
99
470
|
|
|
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
471
|
async function installOnce(finalDir, install) {
|
|
104
472
|
if (fs.existsSync(finalDir)) return;
|
|
105
473
|
const tmp = `${finalDir}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -107,7 +475,7 @@ async function installOnce(finalDir, install) {
|
|
|
107
475
|
try {
|
|
108
476
|
await install(tmp);
|
|
109
477
|
if (fs.existsSync(finalDir)) {
|
|
110
|
-
rmrf(tmp);
|
|
478
|
+
rmrf(tmp);
|
|
111
479
|
return;
|
|
112
480
|
}
|
|
113
481
|
fs.mkdirSync(path.dirname(finalDir), { recursive: true });
|
|
@@ -127,52 +495,28 @@ async function installOnce(finalDir, install) {
|
|
|
127
495
|
}
|
|
128
496
|
|
|
129
497
|
async function download(url, destFile) {
|
|
130
|
-
const res = await fetch(url, {
|
|
131
|
-
redirect: 'follow',
|
|
132
|
-
headers: { 'user-agent': 'bc-pkg' },
|
|
133
|
-
});
|
|
498
|
+
const res = await fetch(url, { redirect: 'follow', headers: { 'user-agent': 'bc-pkg' } });
|
|
134
499
|
if (!res.ok || !res.body) {
|
|
135
|
-
|
|
136
|
-
`Download failed (HTTP ${res.status} ${res.statusText})\n ${url}`
|
|
137
|
-
);
|
|
500
|
+
fail(`Download failed (HTTP ${res.status} ${res.statusText})\n ${url}`);
|
|
138
501
|
}
|
|
139
502
|
await fs.promises.mkdir(path.dirname(destFile), { recursive: true });
|
|
140
503
|
await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(destFile));
|
|
141
504
|
}
|
|
142
505
|
|
|
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
506
|
function extract(archive, destDir) {
|
|
147
507
|
fs.mkdirSync(destDir, { recursive: true });
|
|
148
|
-
let r = spawnSync('tar', ['-xf', archive, '-C', destDir], {
|
|
149
|
-
stdio: ['ignore', 'inherit', 'inherit'],
|
|
150
|
-
});
|
|
508
|
+
let r = spawnSync('tar', ['-xf', archive, '-C', destDir], { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
151
509
|
if (r.error && r.error.code === 'ENOENT') {
|
|
152
510
|
if (process.platform === 'win32' && archive.endsWith('.zip')) {
|
|
153
|
-
r = spawnSync(
|
|
154
|
-
|
|
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
|
-
}
|
|
511
|
+
r = spawnSync('powershell', ['-NoProfile', '-Command', `Expand-Archive -LiteralPath '${archive}' -DestinationPath '${destDir}' -Force`], { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
512
|
+
if (r.status !== 0) fail(`Failed to extract ${archive} (PowerShell fallback)`);
|
|
165
513
|
return;
|
|
166
514
|
}
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
if (r.status !== 0) {
|
|
170
|
-
throw new Error(`Failed to extract ${archive} (tar exit ${r.status})`);
|
|
515
|
+
fail(`'tar' not found on PATH; cannot extract ${archive}`);
|
|
171
516
|
}
|
|
517
|
+
if (r.status !== 0) fail(`Failed to extract ${archive} (tar exit ${r.status})`);
|
|
172
518
|
}
|
|
173
519
|
|
|
174
|
-
// Locates JAVA_HOME inside an extracted JDK. Layout differs per OS
|
|
175
|
-
// (linux: <root>/bin/java, macOS: <root>/Contents/Home/bin/java).
|
|
176
520
|
function findJavaHome(root, exeSuffix) {
|
|
177
521
|
const javaRel = path.join('bin', `java${exeSuffix}`);
|
|
178
522
|
const stack = [root];
|
|
@@ -185,15 +529,11 @@ function findJavaHome(root, exeSuffix) {
|
|
|
185
529
|
} catch {
|
|
186
530
|
continue;
|
|
187
531
|
}
|
|
188
|
-
for (const e of entries)
|
|
189
|
-
if (e.isDirectory()) stack.push(path.join(dir, e.name));
|
|
190
|
-
}
|
|
532
|
+
for (const e of entries) if (e.isDirectory()) stack.push(path.join(dir, e.name));
|
|
191
533
|
}
|
|
192
534
|
return null;
|
|
193
535
|
}
|
|
194
536
|
|
|
195
|
-
// --- Bootstrap steps -----------------------------------------------------
|
|
196
|
-
|
|
197
537
|
async function ensureBabashka(p) {
|
|
198
538
|
const version = DEFAULT_BB_VERSION;
|
|
199
539
|
const finalDir = path.join(cacheRoot(), 'bb', version);
|
|
@@ -212,23 +552,13 @@ async function ensureBabashka(p) {
|
|
|
212
552
|
extract(archive, tmp);
|
|
213
553
|
fs.unlinkSync(archive);
|
|
214
554
|
const exe = path.join(tmp, `bb${p.exeSuffix}`);
|
|
215
|
-
if (!fs.existsSync(exe))
|
|
216
|
-
throw new Error('babashka binary not found after extraction');
|
|
217
|
-
}
|
|
555
|
+
if (!fs.existsSync(exe)) fail('babashka binary not found after extraction');
|
|
218
556
|
if (process.platform !== 'win32') fs.chmodSync(exe, 0o755);
|
|
219
557
|
});
|
|
220
558
|
|
|
221
|
-
if (!fs.existsSync(bbPath)) {
|
|
222
|
-
throw new Error(
|
|
223
|
-
`babashka cache looks corrupt; remove ${finalDir} and retry`
|
|
224
|
-
);
|
|
225
|
-
}
|
|
559
|
+
if (!fs.existsSync(bbPath)) fail(`babashka cache looks corrupt; remove ${finalDir} and retry`);
|
|
226
560
|
if (process.platform !== 'win32') {
|
|
227
|
-
try {
|
|
228
|
-
fs.chmodSync(bbPath, 0o755);
|
|
229
|
-
} catch {
|
|
230
|
-
/* already executable */
|
|
231
|
-
}
|
|
561
|
+
try { fs.chmodSync(bbPath, 0o755); } catch {}
|
|
232
562
|
}
|
|
233
563
|
return bbPath;
|
|
234
564
|
}
|
|
@@ -240,9 +570,7 @@ async function ensureJdk(p) {
|
|
|
240
570
|
|
|
241
571
|
await installOnce(finalDir, async (tmp) => {
|
|
242
572
|
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`;
|
|
573
|
+
const url = `https://api.adoptium.net/v3/binary/latest/${feature}/ga/${p.jdkOs}/${p.jdkArch}/jdk/hotspot/normal/eclipse`;
|
|
246
574
|
log(`Installing Temurin JDK ${feature} (set JDK_VERSION to override)...`);
|
|
247
575
|
try {
|
|
248
576
|
await download(url, archive);
|
|
@@ -252,8 +580,7 @@ async function ensureJdk(p) {
|
|
|
252
580
|
extract(archive, tmp);
|
|
253
581
|
fs.unlinkSync(archive);
|
|
254
582
|
const home = findJavaHome(tmp, p.exeSuffix);
|
|
255
|
-
if (!home)
|
|
256
|
-
// Path relative to tmp stays valid after tmp is renamed to finalDir.
|
|
583
|
+
if (!home) fail('could not locate java in extracted JDK');
|
|
257
584
|
fs.writeFileSync(path.join(tmp, '.javahome'), path.relative(tmp, home));
|
|
258
585
|
});
|
|
259
586
|
|
|
@@ -263,75 +590,33 @@ async function ensureJdk(p) {
|
|
|
263
590
|
} catch {
|
|
264
591
|
javaHome = null;
|
|
265
592
|
}
|
|
266
|
-
if (
|
|
267
|
-
!javaHome ||
|
|
268
|
-
!fs.existsSync(path.join(javaHome, 'bin', `java${p.exeSuffix}`))
|
|
269
|
-
) {
|
|
593
|
+
if (!javaHome || !fs.existsSync(path.join(javaHome, 'bin', `java${p.exeSuffix}`))) {
|
|
270
594
|
javaHome = findJavaHome(finalDir, p.exeSuffix);
|
|
271
595
|
}
|
|
272
|
-
if (!javaHome) {
|
|
273
|
-
throw new Error(`JDK cache looks corrupt; remove ${finalDir} and retry`);
|
|
274
|
-
}
|
|
596
|
+
if (!javaHome) fail(`JDK cache looks corrupt; remove ${finalDir} and retry`);
|
|
275
597
|
return javaHome;
|
|
276
598
|
}
|
|
277
599
|
|
|
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
600
|
function ensureGit() {
|
|
293
601
|
if (process.platform !== 'linux') return;
|
|
294
602
|
if (commandWorks('git', ['--version'])) return;
|
|
295
603
|
|
|
296
|
-
const isRoot =
|
|
297
|
-
typeof process.getuid === 'function' && process.getuid() === 0;
|
|
604
|
+
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
298
605
|
const sudo = isRoot ? [] : binExists('sudo') ? ['sudo'] : null;
|
|
299
606
|
if (sudo === null) {
|
|
300
|
-
|
|
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
|
-
);
|
|
607
|
+
fail('git is missing and cannot be installed: not running as root and `sudo` is unavailable.\n Install git manually and re-run.');
|
|
305
608
|
}
|
|
306
609
|
|
|
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
610
|
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
|
-
},
|
|
611
|
+
{ bin: 'apt-get', steps: [['apt-get', 'update', '-y'], ['apt-get', 'install', '-y', 'git']], soft: [0] },
|
|
318
612
|
{ bin: 'dnf', steps: [['dnf', 'install', '-y', 'git']] },
|
|
319
613
|
{ bin: 'yum', steps: [['yum', 'install', '-y', 'git']] },
|
|
320
|
-
{
|
|
321
|
-
bin: 'zypper',
|
|
322
|
-
steps: [['zypper', '--non-interactive', 'install', 'git']],
|
|
323
|
-
},
|
|
614
|
+
{ bin: 'zypper', steps: [['zypper', '--non-interactive', 'install', 'git']] },
|
|
324
615
|
{ bin: 'pacman', steps: [['pacman', '-S', '--noconfirm', 'git']] },
|
|
325
616
|
{ bin: 'apk', steps: [['apk', 'add', '--no-cache', 'git']] },
|
|
326
617
|
];
|
|
327
618
|
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
|
-
}
|
|
619
|
+
if (!pm) fail('git is missing and no supported package manager was found. Install git manually and re-run.');
|
|
335
620
|
|
|
336
621
|
log(`Installing git via ${pm.bin}${sudo.length ? ' (sudo)' : ''}...`);
|
|
337
622
|
const env = { ...process.env, DEBIAN_FRONTEND: 'noninteractive' };
|
|
@@ -341,18 +626,12 @@ function ensureGit() {
|
|
|
341
626
|
const ok = !r.error && r.status === 0;
|
|
342
627
|
if (!ok && !(pm.soft && pm.soft.includes(i))) {
|
|
343
628
|
const why = r.error ? r.error.code : `exit ${r.status}`;
|
|
344
|
-
|
|
629
|
+
fail(`git install failed: \`${argv.join(' ')}\` (${why}).`);
|
|
345
630
|
}
|
|
346
631
|
});
|
|
347
|
-
|
|
348
|
-
if (!commandWorks('git', ['--version'])) {
|
|
349
|
-
throw new Error('git still not available after the install attempt.');
|
|
350
|
-
}
|
|
632
|
+
if (!commandWorks('git', ['--version'])) fail('git still not available after the install attempt.');
|
|
351
633
|
}
|
|
352
634
|
|
|
353
|
-
// --- bb.edn bootstrap ----------------------------------------------------
|
|
354
|
-
|
|
355
|
-
// Env augmented so spawned processes find the cached JDK and bb; nothing system-wide.
|
|
356
635
|
function bbEnv(javaHome, bbPath, extraEnv = {}) {
|
|
357
636
|
const env = { ...process.env, ...extraEnv };
|
|
358
637
|
const pathEntries = [];
|
|
@@ -360,293 +639,36 @@ function bbEnv(javaHome, bbPath, extraEnv = {}) {
|
|
|
360
639
|
env.JAVA_HOME = javaHome;
|
|
361
640
|
pathEntries.push(path.join(javaHome, 'bin'));
|
|
362
641
|
}
|
|
363
|
-
if (bbPath)
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
if (pathEntries.length) {
|
|
367
|
-
env.PATH = pathEntries.join(path.delimiter) + path.delimiter + (env.PATH || '');
|
|
368
|
-
}
|
|
642
|
+
if (bbPath) pathEntries.push(path.dirname(bbPath));
|
|
643
|
+
if (pathEntries.length) env.PATH = pathEntries.join(path.delimiter) + path.delimiter + (env.PATH || '');
|
|
369
644
|
return env;
|
|
370
645
|
}
|
|
371
646
|
|
|
372
|
-
function
|
|
373
|
-
|
|
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
|
-
}
|
|
647
|
+
function runBb(bbPath, args, javaHome, extraEnv = {}) {
|
|
648
|
+
return runCommand(bbPath, args, { env: bbEnv(javaHome, bbPath, extraEnv) });
|
|
504
649
|
}
|
|
505
650
|
|
|
506
|
-
//
|
|
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`);
|
|
651
|
+
// --- main ----------------------------------------------------------------
|
|
546
652
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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();
|
|
653
|
+
async function main(argv) {
|
|
654
|
+
let args = [...argv];
|
|
655
|
+
let spec = args.length ? parseSpec(args[0]) : null;
|
|
656
|
+
if (spec) args = args.slice(1);
|
|
558
657
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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');
|
|
658
|
+
let meta = readMetadata();
|
|
659
|
+
if (spec) {
|
|
660
|
+
const sha = await resolveRef(spec);
|
|
661
|
+
if (meta) {
|
|
662
|
+
validateExistingMetadata(meta, spec, sha);
|
|
663
|
+
} else {
|
|
664
|
+
meta = await initialize(spec, sha);
|
|
595
665
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
rmrf(tmp);
|
|
666
|
+
} else if (!meta) {
|
|
667
|
+
fail(`No BigConfig CLI is initialized in this directory.\n\n${usage()}`);
|
|
599
668
|
}
|
|
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
669
|
|
|
634
|
-
|
|
635
|
-
const
|
|
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);
|
|
670
|
+
await restoreRunIfMissing(meta);
|
|
671
|
+
const code = await runTarget(meta, args);
|
|
650
672
|
process.exit(code);
|
|
651
673
|
}
|
|
652
674
|
|
|
@@ -657,12 +679,14 @@ if (require.main === module) {
|
|
|
657
679
|
});
|
|
658
680
|
}
|
|
659
681
|
|
|
660
|
-
// Exported for tests / inspection.
|
|
661
682
|
module.exports = {
|
|
683
|
+
parseSpec,
|
|
684
|
+
resolveRef,
|
|
685
|
+
detectTarget,
|
|
686
|
+
readMetadata,
|
|
687
|
+
validateExistingMetadata,
|
|
662
688
|
resolvePlatform,
|
|
663
689
|
cacheRoot,
|
|
664
690
|
findJavaHome,
|
|
665
691
|
ensureGit,
|
|
666
|
-
validateBbEdnRepo,
|
|
667
|
-
ensureBbEdn,
|
|
668
692
|
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
"name": "bc-pkg",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Create and run a BigConfig CLI from a language-specific GitHub package.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"bc-pkg": "bin/bc-pkg.js"
|
|
7
|
+
},
|
|
8
|
+
"files": ["bin/", "README.md"],
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["bigconfig", "cli", "launcher", "clojure", "typescript", "python"],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "commonjs"
|
|
15
15
|
}
|