bc-pkg 1.0.0 → 1.0.2
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 +66 -134
- package/bin/bc-pkg.js +714 -422
- package/package.json +13 -13
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,659 @@ 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 <local-path> <args...>\n bc-pkg <args...>\n\nExamples:\n npx bc-pkg bigconfig-ai/once@typescript package validate\n npx bc-pkg ../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
|
+
// A normal local path: leading slash, dot-slash, or tilde. Bare names with no
|
|
94
|
+
// path prefix are treated as forwarded args, not local targets.
|
|
95
|
+
function isLocalSpec(arg) {
|
|
96
|
+
if (!arg) return false;
|
|
97
|
+
if (arg === '.' || arg === '..') return true;
|
|
98
|
+
if (arg.startsWith('/') || arg.startsWith('~') || arg.startsWith('./') || arg.startsWith('../')) return true;
|
|
99
|
+
if (process.platform === 'win32') {
|
|
100
|
+
if (arg.startsWith('.\\') || arg.startsWith('..\\') || arg.startsWith('\\')) return true;
|
|
101
|
+
if (/^[A-Za-z]:[\\/]/.test(arg)) return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseLocalSpec(arg) {
|
|
107
|
+
let p = arg;
|
|
108
|
+
if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) {
|
|
109
|
+
p = path.join(os.homedir(), p.slice(1));
|
|
110
|
+
}
|
|
111
|
+
const abs = path.resolve(process.cwd(), p);
|
|
112
|
+
let resolved;
|
|
113
|
+
try {
|
|
114
|
+
resolved = fs.realpathSync(abs);
|
|
115
|
+
} catch {
|
|
116
|
+
fail(`local path ${JSON.stringify(arg)} is not a directory (${abs})`);
|
|
117
|
+
}
|
|
118
|
+
if (!fs.statSync(resolved).isDirectory()) {
|
|
119
|
+
fail(`local path ${JSON.stringify(arg)} is not a directory (${resolved})`);
|
|
120
|
+
}
|
|
121
|
+
return { path: resolved, name: path.basename(resolved) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ghHeaders(accept) {
|
|
125
|
+
const headers = {
|
|
126
|
+
'user-agent': 'bc-pkg',
|
|
127
|
+
accept: accept || 'application/vnd.github+json',
|
|
128
|
+
'x-github-api-version': '2022-11-28',
|
|
129
|
+
};
|
|
130
|
+
if (process.env.GITHUB_TOKEN) {
|
|
131
|
+
headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
132
|
+
}
|
|
133
|
+
return headers;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function ghFetch(url, accept) {
|
|
137
|
+
return fetch(url, { headers: ghHeaders(accept), redirect: 'follow' });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function resolveRef(spec) {
|
|
141
|
+
if (FULL_SHA_RE.test(spec.ref)) {
|
|
142
|
+
return spec.ref.toLowerCase();
|
|
143
|
+
}
|
|
144
|
+
const url = `https://api.github.com/repos/${spec.owner}/${spec.repo}/commits/${encodeURIComponent(spec.ref)}`;
|
|
145
|
+
const res = await ghFetch(url);
|
|
146
|
+
if (res.status === 404) {
|
|
147
|
+
fail(`${spec.slug}@${spec.ref} not found or not accessible (set GITHUB_TOKEN for private repos)`);
|
|
148
|
+
}
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
fail(`GitHub API error ${res.status} resolving ${spec.slug}@${spec.ref}`);
|
|
151
|
+
}
|
|
152
|
+
const data = await res.json();
|
|
153
|
+
if (!data || !data.sha) fail(`${spec.slug}@${spec.ref} did not resolve to a commit`);
|
|
154
|
+
return String(data.sha).toLowerCase();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function fetchFile(spec, sha, filePath, { required = false } = {}) {
|
|
158
|
+
const url = `https://api.github.com/repos/${spec.owner}/${spec.repo}/contents/${filePath}?ref=${sha}`;
|
|
159
|
+
const res = await ghFetch(url, 'application/vnd.github.raw');
|
|
160
|
+
if (res.status === 404) {
|
|
161
|
+
if (required) fail(`${spec.slug}@${sha.slice(0, 7)} has no ${filePath}`);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
fail(`GitHub API error ${res.status} fetching ${filePath} from ${spec.slug}@${sha.slice(0, 7)}`);
|
|
166
|
+
}
|
|
167
|
+
return await res.text();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseJson(text, label) {
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(text);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
fail(`Invalid JSON in ${label}: ${err.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parsePyProjectName(text) {
|
|
179
|
+
const project = sectionText(text, 'project');
|
|
180
|
+
const m = project && project.match(/^\s*name\s*=\s*["']([^"']+)["']/m);
|
|
181
|
+
return m ? m[1] : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function sectionText(text, name) {
|
|
185
|
+
const re = new RegExp(`^\\s*\\[${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]\\s*$`, 'm');
|
|
186
|
+
const m = re.exec(text);
|
|
187
|
+
if (!m) return null;
|
|
188
|
+
const start = m.index + m[0].length;
|
|
189
|
+
const rest = text.slice(start);
|
|
190
|
+
const next = /^\s*\[[^\]]+\]\s*$/m.exec(rest);
|
|
191
|
+
return next ? rest.slice(0, next.index) : rest;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function detectTargetFromManifests(label, depsEdn, packageJsonText, pyprojectText, clojureCoord) {
|
|
195
|
+
const found = [];
|
|
196
|
+
if (depsEdn != null) found.push('clojure');
|
|
197
|
+
if (packageJsonText != null) found.push('typescript');
|
|
198
|
+
if (pyprojectText != null) found.push('python');
|
|
199
|
+
if (found.length === 0) {
|
|
200
|
+
fail(`${label} has no deps.edn, package.json, or pyproject.toml`);
|
|
201
|
+
}
|
|
202
|
+
if (found.length > 1) {
|
|
203
|
+
fail(`${label} is ambiguous; found ${found.join(', ')} manifests`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (found[0] === 'typescript') {
|
|
207
|
+
const pkg = parseJson(packageJsonText, 'package.json');
|
|
208
|
+
if (!pkg.name) fail(`${label} package.json has no name`);
|
|
209
|
+
return { language: 'typescript', packageName: pkg.name };
|
|
210
|
+
}
|
|
211
|
+
if (found[0] === 'python') {
|
|
212
|
+
const packageName = parsePyProjectName(pyprojectText);
|
|
213
|
+
if (!packageName) fail(`${label} pyproject.toml has no [project].name`);
|
|
214
|
+
return { language: 'python', packageName };
|
|
215
|
+
}
|
|
216
|
+
return { language: 'clojure', packageName: clojureCoord };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function detectTarget(spec, sha) {
|
|
220
|
+
const [depsEdn, packageJsonText, pyprojectText] = await Promise.all([
|
|
221
|
+
fetchFile(spec, sha, 'deps.edn'),
|
|
222
|
+
fetchFile(spec, sha, 'package.json'),
|
|
223
|
+
fetchFile(spec, sha, 'pyproject.toml'),
|
|
224
|
+
]);
|
|
225
|
+
return detectTargetFromManifests(
|
|
226
|
+
`${spec.slug}@${sha.slice(0, 7)}`,
|
|
227
|
+
depsEdn,
|
|
228
|
+
packageJsonText,
|
|
229
|
+
pyprojectText,
|
|
230
|
+
`io.github.${spec.owner}/${spec.repo}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function readLocalFile(local, filePath, { required = false } = {}) {
|
|
235
|
+
const f = path.join(local.path, filePath);
|
|
236
|
+
if (!fs.existsSync(f) || !fs.statSync(f).isFile()) {
|
|
237
|
+
if (required) fail(`${local.path} has no ${filePath}`);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return fs.readFileSync(f, 'utf8');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function detectTargetLocal(local) {
|
|
244
|
+
return detectTargetFromManifests(
|
|
245
|
+
local.path,
|
|
246
|
+
readLocalFile(local, 'deps.edn'),
|
|
247
|
+
readLocalFile(local, 'package.json'),
|
|
248
|
+
readLocalFile(local, 'pyproject.toml'),
|
|
249
|
+
`local/${local.name}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// --- native metadata -----------------------------------------------------
|
|
254
|
+
|
|
255
|
+
function metadataFromPackageJson(file) {
|
|
256
|
+
if (!fs.existsSync(file)) return null;
|
|
257
|
+
const pkg = parseJson(fs.readFileSync(file, 'utf8'), file);
|
|
258
|
+
if (!pkg.bigconfig) return null;
|
|
259
|
+
return { ...pkg.bigconfig, language: pkg.bigconfig.language || 'typescript', manifest: file };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function metadataFromPyproject(file) {
|
|
263
|
+
if (!fs.existsSync(file)) return null;
|
|
264
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
265
|
+
const sec = sectionText(text, 'tool.bigconfig');
|
|
266
|
+
if (!sec) return null;
|
|
267
|
+
const get = (key) => {
|
|
268
|
+
const m = sec.match(new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=\\s*["']([^"']+)["']`, 'm'));
|
|
269
|
+
return m ? m[1] : undefined;
|
|
270
|
+
};
|
|
271
|
+
return {
|
|
272
|
+
repo: get('repo'),
|
|
273
|
+
ref: get('ref'),
|
|
274
|
+
sha: get('sha'),
|
|
275
|
+
language: get('language') || 'python',
|
|
276
|
+
run: get('run') || 'run',
|
|
277
|
+
packageName: get('package-name'),
|
|
278
|
+
local: /^\s*local\s*=\s*true\s*$/m.test(sec),
|
|
279
|
+
path: get('path'),
|
|
280
|
+
manifest: file,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function metadataFromDepsEdn(file) {
|
|
285
|
+
if (!fs.existsSync(file)) return null;
|
|
286
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
287
|
+
if (!text.includes(':bigconfig/repo') && !text.includes(':bigconfig/path')) return null;
|
|
288
|
+
const get = (key) => {
|
|
289
|
+
const m = text.match(new RegExp(`:${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s+"([^"]+)"`));
|
|
290
|
+
return m ? m[1] : undefined;
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
repo: get('bigconfig/repo'),
|
|
294
|
+
ref: get('bigconfig/ref'),
|
|
295
|
+
sha: get('bigconfig/sha'),
|
|
296
|
+
language: get('bigconfig/language') || 'clojure',
|
|
297
|
+
run: get('bigconfig/run') || 'run',
|
|
298
|
+
local: get('bigconfig/local') === 'true',
|
|
299
|
+
path: get('bigconfig/path'),
|
|
300
|
+
manifest: file,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function readMetadata(cwd = process.cwd()) {
|
|
305
|
+
const metas = [
|
|
306
|
+
metadataFromDepsEdn(path.join(cwd, 'deps.edn')),
|
|
307
|
+
metadataFromPackageJson(path.join(cwd, 'package.json')),
|
|
308
|
+
metadataFromPyproject(path.join(cwd, 'pyproject.toml')),
|
|
309
|
+
].filter(Boolean);
|
|
310
|
+
if (metas.length > 1) {
|
|
311
|
+
fail('Multiple BigConfig metadata files found; keep only one of deps.edn, package.json, or pyproject.toml initialized for bc-pkg.');
|
|
312
|
+
}
|
|
313
|
+
if (metas.length === 0) return null;
|
|
314
|
+
const meta = metas[0];
|
|
315
|
+
if (meta.local) {
|
|
316
|
+
if (!meta.path || !meta.language) fail(`Incomplete BigConfig metadata in ${meta.manifest}`);
|
|
317
|
+
} else if (!meta.repo || !meta.ref || !meta.sha || !meta.language) {
|
|
318
|
+
fail(`Incomplete BigConfig metadata in ${meta.manifest}`);
|
|
319
|
+
}
|
|
320
|
+
return meta;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function validateExistingMetadata(meta, spec, sha) {
|
|
324
|
+
if (meta.local) {
|
|
325
|
+
fail('Current directory is initialized for a local BigConfig package; refusing to switch to a GitHub package.');
|
|
326
|
+
}
|
|
327
|
+
const expectedRepo = spec.slug;
|
|
328
|
+
const problems = [];
|
|
329
|
+
if (meta.repo !== expectedRepo) problems.push(`repo ${JSON.stringify(meta.repo)} != ${JSON.stringify(expectedRepo)}`);
|
|
330
|
+
if (meta.ref !== spec.ref) problems.push(`ref ${JSON.stringify(meta.ref)} != ${JSON.stringify(spec.ref)}`);
|
|
331
|
+
if (String(meta.sha).toLowerCase() !== sha.toLowerCase()) problems.push(`sha ${meta.sha} != ${sha}`);
|
|
332
|
+
if (problems.length) {
|
|
333
|
+
fail(`Current directory is already initialized for a different BigConfig package:\n ${problems.join('\n ')}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function validateExistingLocalMetadata(meta, local) {
|
|
338
|
+
if (!meta.local) {
|
|
339
|
+
fail('Current directory is initialized for a GitHub BigConfig package; refusing to switch to a local package.');
|
|
340
|
+
}
|
|
341
|
+
let existing = null;
|
|
342
|
+
if (meta.path) {
|
|
343
|
+
try {
|
|
344
|
+
existing = fs.realpathSync(meta.path);
|
|
345
|
+
} catch {
|
|
346
|
+
existing = path.resolve(meta.path);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (existing !== local.path) {
|
|
350
|
+
fail(`Current directory is already initialized for a different local BigConfig package:\n path ${JSON.stringify(meta.path)} != ${JSON.stringify(local.path)}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function quoteToml(s) {
|
|
355
|
+
return JSON.stringify(String(s));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function writeRunFile(text) {
|
|
359
|
+
const target = path.join(process.cwd(), 'run');
|
|
360
|
+
fs.writeFileSync(target, text);
|
|
361
|
+
if (process.platform !== 'win32') fs.chmodSync(target, 0o755);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Local targets symlink the run file so edits to the local package flow through;
|
|
365
|
+
// copy as a fallback where symlinks are unavailable.
|
|
366
|
+
function linkRunFile(source) {
|
|
367
|
+
const target = path.join(process.cwd(), 'run');
|
|
368
|
+
try {
|
|
369
|
+
fs.lstatSync(target);
|
|
370
|
+
fs.rmSync(target, { force: true });
|
|
371
|
+
} catch {
|
|
372
|
+
// nothing to remove
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
fs.symlinkSync(source, target);
|
|
376
|
+
} catch {
|
|
377
|
+
fs.copyFileSync(source, target);
|
|
378
|
+
if (process.platform !== 'win32') fs.chmodSync(target, 0o755);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function clojureCoord(spec) {
|
|
383
|
+
return `io.github.${spec.owner}/${spec.repo}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function writeClojureManifest(spec, sha, target) {
|
|
387
|
+
const coord = clojureCoord(spec);
|
|
388
|
+
const gitUrl = `https://github.com/${spec.owner}/${spec.repo}.git`;
|
|
389
|
+
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`;
|
|
390
|
+
fs.writeFileSync(path.join(process.cwd(), 'deps.edn'), deps);
|
|
391
|
+
|
|
392
|
+
// Babashka script execution reads bb.edn, not deps.edn. Metadata remains in
|
|
393
|
+
// deps.edn per the launcher contract; bb.edn is the runtime dependency file.
|
|
394
|
+
const bb = `{:deps {${coord} {:git/url "${gitUrl}"\n :git/sha "${sha}"}}}\n`;
|
|
395
|
+
fs.writeFileSync(path.join(process.cwd(), 'bb.edn'), bb);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function writeTypeScriptManifest(spec, sha, target) {
|
|
399
|
+
const file = path.join(process.cwd(), 'package.json');
|
|
400
|
+
let pkg = {};
|
|
401
|
+
if (fs.existsSync(file)) {
|
|
402
|
+
pkg = parseJson(fs.readFileSync(file, 'utf8'), file);
|
|
403
|
+
if (pkg.bigconfig) validateExistingMetadata(pkg.bigconfig, spec, sha);
|
|
404
|
+
}
|
|
405
|
+
pkg.type = pkg.type || 'module';
|
|
406
|
+
pkg.scripts = { ...(pkg.scripts || {}), run: 'node run' };
|
|
407
|
+
pkg.dependencies = { ...(pkg.dependencies || {}) };
|
|
408
|
+
pkg.dependencies[target.packageName] = `github:${spec.owner}/${spec.repo}#${sha}`;
|
|
409
|
+
pkg.bigconfig = {
|
|
410
|
+
repo: spec.slug,
|
|
411
|
+
ref: spec.ref,
|
|
412
|
+
sha,
|
|
413
|
+
language: 'typescript',
|
|
414
|
+
run: 'run',
|
|
415
|
+
packageName: target.packageName,
|
|
416
|
+
};
|
|
417
|
+
fs.writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function writePythonManifest(spec, sha, target) {
|
|
421
|
+
const file = path.join(process.cwd(), 'pyproject.toml');
|
|
422
|
+
if (fs.existsSync(file)) {
|
|
423
|
+
const existing = metadataFromPyproject(file);
|
|
424
|
+
if (!existing) {
|
|
425
|
+
fail('pyproject.toml already exists and is not initialized for bc-pkg; refusing to rewrite it.');
|
|
426
|
+
}
|
|
427
|
+
validateExistingMetadata(existing, spec, sha);
|
|
428
|
+
}
|
|
429
|
+
const dep = `${target.packageName} @ git+https://github.com/${spec.owner}/${spec.repo}.git@${sha}`;
|
|
430
|
+
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`;
|
|
431
|
+
fs.writeFileSync(file, text);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function writeNativeManifest(spec, sha, target) {
|
|
435
|
+
if (target.language === 'clojure') return writeClojureManifest(spec, sha, target);
|
|
436
|
+
if (target.language === 'typescript') return writeTypeScriptManifest(spec, sha, target);
|
|
437
|
+
if (target.language === 'python') return writePythonManifest(spec, sha, target);
|
|
438
|
+
fail(`Unsupported language: ${target.language}`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// --- local (live) manifests ----------------------------------------------
|
|
442
|
+
|
|
443
|
+
function writeClojureManifestLocal(local, target) {
|
|
444
|
+
const coord = target.packageName || `local/${local.name}`;
|
|
445
|
+
const p = local.path;
|
|
446
|
+
const deps = `{:deps {${coord} {:local/root "${p}"}}\n :bigconfig/local "true"\n :bigconfig/path "${p}"\n :bigconfig/language "clojure"\n :bigconfig/run "run"}\n`;
|
|
447
|
+
fs.writeFileSync(path.join(process.cwd(), 'deps.edn'), deps);
|
|
448
|
+
|
|
449
|
+
// Babashka reads bb.edn; metadata stays in deps.edn per the launcher contract.
|
|
450
|
+
const bb = `{:deps {${coord} {:local/root "${p}"}}}\n`;
|
|
451
|
+
fs.writeFileSync(path.join(process.cwd(), 'bb.edn'), bb);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function writeTypeScriptManifestLocal(local, target) {
|
|
455
|
+
const file = path.join(process.cwd(), 'package.json');
|
|
456
|
+
let pkg = {};
|
|
457
|
+
if (fs.existsSync(file)) {
|
|
458
|
+
pkg = parseJson(fs.readFileSync(file, 'utf8'), file);
|
|
459
|
+
if (pkg.bigconfig) validateExistingLocalMetadata(metadataFromPackageJson(file), local);
|
|
460
|
+
}
|
|
461
|
+
const packageName = target.packageName || local.name;
|
|
462
|
+
pkg.type = pkg.type || 'module';
|
|
463
|
+
pkg.scripts = { ...(pkg.scripts || {}), run: 'node run' };
|
|
464
|
+
pkg.dependencies = { ...(pkg.dependencies || {}) };
|
|
465
|
+
pkg.dependencies[packageName] = `file:${local.path}`;
|
|
466
|
+
pkg.bigconfig = {
|
|
467
|
+
local: true,
|
|
468
|
+
path: local.path,
|
|
469
|
+
language: 'typescript',
|
|
470
|
+
run: 'run',
|
|
471
|
+
packageName,
|
|
472
|
+
};
|
|
473
|
+
fs.writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function writePythonManifestLocal(local, target) {
|
|
477
|
+
const file = path.join(process.cwd(), 'pyproject.toml');
|
|
478
|
+
if (fs.existsSync(file)) {
|
|
479
|
+
const existing = metadataFromPyproject(file);
|
|
480
|
+
if (!existing) {
|
|
481
|
+
fail('pyproject.toml already exists and is not initialized for bc-pkg; refusing to rewrite it.');
|
|
482
|
+
}
|
|
483
|
+
validateExistingLocalMetadata(existing, local);
|
|
484
|
+
}
|
|
485
|
+
const packageName = target.packageName || local.name;
|
|
486
|
+
const text = `[project]\nname = "bigconfig-cli"\nversion = "0.1.0"\nrequires-python = ">=3.12"\ndependencies = [\n ${quoteToml(packageName)},\n]\n\n[tool.uv.sources]\n${quoteToml(packageName)} = { path = ${quoteToml(local.path)}, editable = true }\n\n[tool.bigconfig]\nlocal = true\npath = ${quoteToml(local.path)}\nlanguage = "python"\nrun = "run"\npackage-name = ${quoteToml(packageName)}\n`;
|
|
487
|
+
fs.writeFileSync(file, text);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function writeNativeManifestLocal(local, target) {
|
|
491
|
+
if (target.language === 'clojure') return writeClojureManifestLocal(local, target);
|
|
492
|
+
if (target.language === 'typescript') return writeTypeScriptManifestLocal(local, target);
|
|
493
|
+
if (target.language === 'python') return writePythonManifestLocal(local, target);
|
|
494
|
+
fail(`Unsupported language: ${target.language}`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// --- target dependency setup and execution -------------------------------
|
|
498
|
+
|
|
499
|
+
function pythonSitePackagesDirs() {
|
|
500
|
+
const venv = path.join(process.cwd(), '.venv');
|
|
501
|
+
const candidates = [path.join(venv, 'Lib', 'site-packages')];
|
|
502
|
+
for (const libName of ['lib', 'lib64']) {
|
|
503
|
+
const libDir = path.join(venv, libName);
|
|
504
|
+
let entries = [];
|
|
505
|
+
try {
|
|
506
|
+
entries = fs.readdirSync(libDir);
|
|
507
|
+
} catch {
|
|
508
|
+
entries = [];
|
|
509
|
+
}
|
|
510
|
+
for (const e of entries.sort()) {
|
|
511
|
+
if (e.startsWith('python')) candidates.push(path.join(libDir, e, 'site-packages'));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const seen = new Set();
|
|
515
|
+
const result = [];
|
|
516
|
+
for (const c of candidates) {
|
|
517
|
+
if (seen.has(c)) continue;
|
|
518
|
+
seen.add(c);
|
|
519
|
+
try {
|
|
520
|
+
if (fs.statSync(c).isDirectory()) result.push(c);
|
|
521
|
+
} catch {
|
|
522
|
+
// not present
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Python wheels install templates as top-level package data under
|
|
529
|
+
// site-packages/resources; BigConfig's renderer resolves them from ./resources.
|
|
530
|
+
// For editable local installs the source tree is used instead (resources live
|
|
531
|
+
// under <path>/src/resources or <path>/resources, never in site-packages).
|
|
532
|
+
function exposePythonResources(meta) {
|
|
533
|
+
const target = path.join(process.cwd(), 'resources');
|
|
534
|
+
try {
|
|
535
|
+
if (fs.statSync(target)) return;
|
|
536
|
+
} catch {
|
|
537
|
+
// not present; continue
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
if (fs.lstatSync(target)) fs.rmSync(target, { force: true, recursive: true });
|
|
541
|
+
} catch {
|
|
542
|
+
// nothing to remove (e.g. broken symlink already gone)
|
|
543
|
+
}
|
|
544
|
+
const candidates = [];
|
|
545
|
+
if (meta && meta.local && meta.path) {
|
|
546
|
+
candidates.push(path.join(meta.path, 'src', 'resources'), path.join(meta.path, 'resources'));
|
|
547
|
+
}
|
|
548
|
+
for (const site of pythonSitePackagesDirs()) candidates.push(path.join(site, 'resources'));
|
|
549
|
+
let source = null;
|
|
550
|
+
for (const c of candidates) {
|
|
551
|
+
try {
|
|
552
|
+
if (fs.statSync(c).isDirectory()) {
|
|
553
|
+
source = c;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
} catch {
|
|
557
|
+
// not a dir
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (!source) return;
|
|
561
|
+
try {
|
|
562
|
+
fs.symlinkSync(source, target, 'dir');
|
|
563
|
+
} catch {
|
|
564
|
+
fs.cpSync(source, target, { recursive: true });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function ensureTargetDeps(meta) {
|
|
569
|
+
if (meta.language === 'typescript') {
|
|
570
|
+
requireCommand('node', 'Install Node.js and try again.');
|
|
571
|
+
requireCommand('npm', 'Install npm and try again.');
|
|
572
|
+
if (!fs.existsSync(path.join(process.cwd(), 'node_modules'))) {
|
|
573
|
+
log('Installing TypeScript target dependencies with npm install...');
|
|
574
|
+
const code = await runCommand('npm', ['install']);
|
|
575
|
+
if (code !== 0) process.exit(code);
|
|
576
|
+
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (meta.language === 'python') {
|
|
580
|
+
const py = whichPython();
|
|
581
|
+
if (!py) fail('python3 or python is required but was not found on PATH.');
|
|
582
|
+
requireCommand('uv', 'Install uv and try again.');
|
|
583
|
+
if (!fs.existsSync(path.join(process.cwd(), '.venv'))) {
|
|
584
|
+
log('Installing Python target dependencies with uv sync...');
|
|
585
|
+
const code = await runCommand('uv', ['sync']);
|
|
586
|
+
if (code !== 0) process.exit(code);
|
|
587
|
+
}
|
|
588
|
+
exposePythonResources(meta);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function runTarget(meta, args) {
|
|
594
|
+
if (meta.language === 'typescript') {
|
|
595
|
+
await ensureTargetDeps(meta);
|
|
596
|
+
return await runCommand('node', ['run', ...args]);
|
|
597
|
+
}
|
|
598
|
+
if (meta.language === 'python') {
|
|
599
|
+
await ensureTargetDeps(meta);
|
|
600
|
+
return await runCommand('uv', ['run', 'python', meta.run || 'run', ...args]);
|
|
601
|
+
}
|
|
602
|
+
if (meta.language === 'clojure') {
|
|
603
|
+
const p = resolvePlatform();
|
|
604
|
+
const bbPath = await ensureBabashka(p);
|
|
605
|
+
const javaHome = await ensureJdk(p);
|
|
606
|
+
ensureGit();
|
|
607
|
+
return await runBb(bbPath, ['run', ...args], javaHome);
|
|
608
|
+
}
|
|
609
|
+
fail(`Unsupported language: ${meta.language}`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function initialize(spec, sha) {
|
|
613
|
+
const target = await detectTarget(spec, sha);
|
|
614
|
+
const runText = await fetchFile(spec, sha, 'run', { required: true });
|
|
615
|
+
writeRunFile(runText);
|
|
616
|
+
writeNativeManifest(spec, sha, target);
|
|
617
|
+
return {
|
|
618
|
+
repo: spec.slug,
|
|
619
|
+
ref: spec.ref,
|
|
620
|
+
sha,
|
|
621
|
+
language: target.language,
|
|
622
|
+
run: 'run',
|
|
623
|
+
packageName: target.packageName,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function initializeLocal(local) {
|
|
628
|
+
if (local.path === fs.realpathSync(process.cwd())) {
|
|
629
|
+
fail("local path is the current directory; run bc-pkg from a separate directory so it does not overwrite the package's own manifest.");
|
|
630
|
+
}
|
|
631
|
+
const target = detectTargetLocal(local);
|
|
632
|
+
const runSource = path.join(local.path, 'run');
|
|
633
|
+
if (!fs.existsSync(runSource) || !fs.statSync(runSource).isFile()) fail(`${local.path} has no run`);
|
|
634
|
+
linkRunFile(runSource);
|
|
635
|
+
writeNativeManifestLocal(local, target);
|
|
636
|
+
return {
|
|
637
|
+
repo: null,
|
|
638
|
+
ref: null,
|
|
639
|
+
sha: null,
|
|
640
|
+
language: target.language,
|
|
641
|
+
run: 'run',
|
|
642
|
+
packageName: target.packageName,
|
|
643
|
+
local: true,
|
|
644
|
+
path: local.path,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async function restoreRunIfMissing(meta) {
|
|
649
|
+
const runPath = path.join(process.cwd(), meta.run || 'run');
|
|
650
|
+
if (fs.existsSync(runPath)) return;
|
|
651
|
+
if (meta.local) {
|
|
652
|
+
const source = path.join(meta.path, meta.run || 'run');
|
|
653
|
+
if (!fs.existsSync(source)) fail(`${meta.path} has no ${meta.run || 'run'}`);
|
|
654
|
+
linkRunFile(source);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const [owner, repo] = meta.repo.split('/');
|
|
658
|
+
const spec = { owner, repo, slug: meta.repo, ref: meta.ref };
|
|
659
|
+
const runText = await fetchFile(spec, meta.sha, 'run', { required: true });
|
|
660
|
+
writeRunFile(runText);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// --- Babashka/JDK bootstrap for Clojure targets --------------------------
|
|
27
664
|
|
|
28
|
-
// Maps the host OS/arch to babashka release asset + Adoptium API parameters.
|
|
29
665
|
function resolvePlatform() {
|
|
30
|
-
const plat = process.platform;
|
|
31
|
-
const arch = process.arch;
|
|
666
|
+
const plat = process.platform;
|
|
667
|
+
const arch = process.arch;
|
|
32
668
|
const exeSuffix = plat === 'win32' ? '.exe' : '';
|
|
33
669
|
|
|
34
670
|
let bbOs;
|
|
@@ -47,7 +683,7 @@ function resolvePlatform() {
|
|
|
47
683
|
jdkOs = 'windows';
|
|
48
684
|
archiveExt = 'zip';
|
|
49
685
|
} else {
|
|
50
|
-
|
|
686
|
+
fail(`Unsupported OS: ${plat}`);
|
|
51
687
|
}
|
|
52
688
|
|
|
53
689
|
let bbArch;
|
|
@@ -59,17 +695,11 @@ function resolvePlatform() {
|
|
|
59
695
|
bbArch = 'amd64';
|
|
60
696
|
jdkArch = 'x64';
|
|
61
697
|
} else {
|
|
62
|
-
|
|
698
|
+
fail(`Unsupported CPU architecture: ${arch}`);
|
|
63
699
|
}
|
|
64
700
|
|
|
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
|
-
}
|
|
701
|
+
const bbArchToken = plat === 'linux' && arch === 'arm64' ? 'aarch64-static' : bbArch;
|
|
702
|
+
if (plat === 'win32' && arch === 'arm64') fail('babashka has no prebuilt Windows arm64 binary');
|
|
73
703
|
|
|
74
704
|
return {
|
|
75
705
|
exeSuffix,
|
|
@@ -84,22 +714,16 @@ function resolvePlatform() {
|
|
|
84
714
|
}
|
|
85
715
|
|
|
86
716
|
function cacheRoot() {
|
|
87
|
-
const base =
|
|
88
|
-
process.
|
|
89
|
-
|
|
90
|
-
: process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
717
|
+
const base = process.platform === 'win32'
|
|
718
|
+
? process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
|
|
719
|
+
: process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
91
720
|
return path.join(base, 'bc-pkg');
|
|
92
721
|
}
|
|
93
722
|
|
|
94
|
-
// --- Filesystem helpers --------------------------------------------------
|
|
95
|
-
|
|
96
723
|
function rmrf(target) {
|
|
97
724
|
fs.rmSync(target, { recursive: true, force: true });
|
|
98
725
|
}
|
|
99
726
|
|
|
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
727
|
async function installOnce(finalDir, install) {
|
|
104
728
|
if (fs.existsSync(finalDir)) return;
|
|
105
729
|
const tmp = `${finalDir}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -107,7 +731,7 @@ async function installOnce(finalDir, install) {
|
|
|
107
731
|
try {
|
|
108
732
|
await install(tmp);
|
|
109
733
|
if (fs.existsSync(finalDir)) {
|
|
110
|
-
rmrf(tmp);
|
|
734
|
+
rmrf(tmp);
|
|
111
735
|
return;
|
|
112
736
|
}
|
|
113
737
|
fs.mkdirSync(path.dirname(finalDir), { recursive: true });
|
|
@@ -127,52 +751,28 @@ async function installOnce(finalDir, install) {
|
|
|
127
751
|
}
|
|
128
752
|
|
|
129
753
|
async function download(url, destFile) {
|
|
130
|
-
const res = await fetch(url, {
|
|
131
|
-
redirect: 'follow',
|
|
132
|
-
headers: { 'user-agent': 'bc-pkg' },
|
|
133
|
-
});
|
|
754
|
+
const res = await fetch(url, { redirect: 'follow', headers: { 'user-agent': 'bc-pkg' } });
|
|
134
755
|
if (!res.ok || !res.body) {
|
|
135
|
-
|
|
136
|
-
`Download failed (HTTP ${res.status} ${res.statusText})\n ${url}`
|
|
137
|
-
);
|
|
756
|
+
fail(`Download failed (HTTP ${res.status} ${res.statusText})\n ${url}`);
|
|
138
757
|
}
|
|
139
758
|
await fs.promises.mkdir(path.dirname(destFile), { recursive: true });
|
|
140
759
|
await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(destFile));
|
|
141
760
|
}
|
|
142
761
|
|
|
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
762
|
function extract(archive, destDir) {
|
|
147
763
|
fs.mkdirSync(destDir, { recursive: true });
|
|
148
|
-
let r = spawnSync('tar', ['-xf', archive, '-C', destDir], {
|
|
149
|
-
stdio: ['ignore', 'inherit', 'inherit'],
|
|
150
|
-
});
|
|
764
|
+
let r = spawnSync('tar', ['-xf', archive, '-C', destDir], { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
151
765
|
if (r.error && r.error.code === 'ENOENT') {
|
|
152
766
|
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
|
-
}
|
|
767
|
+
r = spawnSync('powershell', ['-NoProfile', '-Command', `Expand-Archive -LiteralPath '${archive}' -DestinationPath '${destDir}' -Force`], { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
768
|
+
if (r.status !== 0) fail(`Failed to extract ${archive} (PowerShell fallback)`);
|
|
165
769
|
return;
|
|
166
770
|
}
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
if (r.status !== 0) {
|
|
170
|
-
throw new Error(`Failed to extract ${archive} (tar exit ${r.status})`);
|
|
771
|
+
fail(`'tar' not found on PATH; cannot extract ${archive}`);
|
|
171
772
|
}
|
|
773
|
+
if (r.status !== 0) fail(`Failed to extract ${archive} (tar exit ${r.status})`);
|
|
172
774
|
}
|
|
173
775
|
|
|
174
|
-
// Locates JAVA_HOME inside an extracted JDK. Layout differs per OS
|
|
175
|
-
// (linux: <root>/bin/java, macOS: <root>/Contents/Home/bin/java).
|
|
176
776
|
function findJavaHome(root, exeSuffix) {
|
|
177
777
|
const javaRel = path.join('bin', `java${exeSuffix}`);
|
|
178
778
|
const stack = [root];
|
|
@@ -185,15 +785,11 @@ function findJavaHome(root, exeSuffix) {
|
|
|
185
785
|
} catch {
|
|
186
786
|
continue;
|
|
187
787
|
}
|
|
188
|
-
for (const e of entries)
|
|
189
|
-
if (e.isDirectory()) stack.push(path.join(dir, e.name));
|
|
190
|
-
}
|
|
788
|
+
for (const e of entries) if (e.isDirectory()) stack.push(path.join(dir, e.name));
|
|
191
789
|
}
|
|
192
790
|
return null;
|
|
193
791
|
}
|
|
194
792
|
|
|
195
|
-
// --- Bootstrap steps -----------------------------------------------------
|
|
196
|
-
|
|
197
793
|
async function ensureBabashka(p) {
|
|
198
794
|
const version = DEFAULT_BB_VERSION;
|
|
199
795
|
const finalDir = path.join(cacheRoot(), 'bb', version);
|
|
@@ -212,23 +808,13 @@ async function ensureBabashka(p) {
|
|
|
212
808
|
extract(archive, tmp);
|
|
213
809
|
fs.unlinkSync(archive);
|
|
214
810
|
const exe = path.join(tmp, `bb${p.exeSuffix}`);
|
|
215
|
-
if (!fs.existsSync(exe))
|
|
216
|
-
throw new Error('babashka binary not found after extraction');
|
|
217
|
-
}
|
|
811
|
+
if (!fs.existsSync(exe)) fail('babashka binary not found after extraction');
|
|
218
812
|
if (process.platform !== 'win32') fs.chmodSync(exe, 0o755);
|
|
219
813
|
});
|
|
220
814
|
|
|
221
|
-
if (!fs.existsSync(bbPath)) {
|
|
222
|
-
throw new Error(
|
|
223
|
-
`babashka cache looks corrupt; remove ${finalDir} and retry`
|
|
224
|
-
);
|
|
225
|
-
}
|
|
815
|
+
if (!fs.existsSync(bbPath)) fail(`babashka cache looks corrupt; remove ${finalDir} and retry`);
|
|
226
816
|
if (process.platform !== 'win32') {
|
|
227
|
-
try {
|
|
228
|
-
fs.chmodSync(bbPath, 0o755);
|
|
229
|
-
} catch {
|
|
230
|
-
/* already executable */
|
|
231
|
-
}
|
|
817
|
+
try { fs.chmodSync(bbPath, 0o755); } catch {}
|
|
232
818
|
}
|
|
233
819
|
return bbPath;
|
|
234
820
|
}
|
|
@@ -240,9 +826,7 @@ async function ensureJdk(p) {
|
|
|
240
826
|
|
|
241
827
|
await installOnce(finalDir, async (tmp) => {
|
|
242
828
|
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`;
|
|
829
|
+
const url = `https://api.adoptium.net/v3/binary/latest/${feature}/ga/${p.jdkOs}/${p.jdkArch}/jdk/hotspot/normal/eclipse`;
|
|
246
830
|
log(`Installing Temurin JDK ${feature} (set JDK_VERSION to override)...`);
|
|
247
831
|
try {
|
|
248
832
|
await download(url, archive);
|
|
@@ -252,8 +836,7 @@ async function ensureJdk(p) {
|
|
|
252
836
|
extract(archive, tmp);
|
|
253
837
|
fs.unlinkSync(archive);
|
|
254
838
|
const home = findJavaHome(tmp, p.exeSuffix);
|
|
255
|
-
if (!home)
|
|
256
|
-
// Path relative to tmp stays valid after tmp is renamed to finalDir.
|
|
839
|
+
if (!home) fail('could not locate java in extracted JDK');
|
|
257
840
|
fs.writeFileSync(path.join(tmp, '.javahome'), path.relative(tmp, home));
|
|
258
841
|
});
|
|
259
842
|
|
|
@@ -263,75 +846,33 @@ async function ensureJdk(p) {
|
|
|
263
846
|
} catch {
|
|
264
847
|
javaHome = null;
|
|
265
848
|
}
|
|
266
|
-
if (
|
|
267
|
-
!javaHome ||
|
|
268
|
-
!fs.existsSync(path.join(javaHome, 'bin', `java${p.exeSuffix}`))
|
|
269
|
-
) {
|
|
849
|
+
if (!javaHome || !fs.existsSync(path.join(javaHome, 'bin', `java${p.exeSuffix}`))) {
|
|
270
850
|
javaHome = findJavaHome(finalDir, p.exeSuffix);
|
|
271
851
|
}
|
|
272
|
-
if (!javaHome) {
|
|
273
|
-
throw new Error(`JDK cache looks corrupt; remove ${finalDir} and retry`);
|
|
274
|
-
}
|
|
852
|
+
if (!javaHome) fail(`JDK cache looks corrupt; remove ${finalDir} and retry`);
|
|
275
853
|
return javaHome;
|
|
276
854
|
}
|
|
277
855
|
|
|
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
856
|
function ensureGit() {
|
|
293
857
|
if (process.platform !== 'linux') return;
|
|
294
858
|
if (commandWorks('git', ['--version'])) return;
|
|
295
859
|
|
|
296
|
-
const isRoot =
|
|
297
|
-
typeof process.getuid === 'function' && process.getuid() === 0;
|
|
860
|
+
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
298
861
|
const sudo = isRoot ? [] : binExists('sudo') ? ['sudo'] : null;
|
|
299
862
|
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
|
-
);
|
|
863
|
+
fail('git is missing and cannot be installed: not running as root and `sudo` is unavailable.\n Install git manually and re-run.');
|
|
305
864
|
}
|
|
306
865
|
|
|
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
866
|
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
|
-
},
|
|
867
|
+
{ bin: 'apt-get', steps: [['apt-get', 'update', '-y'], ['apt-get', 'install', '-y', 'git']], soft: [0] },
|
|
318
868
|
{ bin: 'dnf', steps: [['dnf', 'install', '-y', 'git']] },
|
|
319
869
|
{ bin: 'yum', steps: [['yum', 'install', '-y', 'git']] },
|
|
320
|
-
{
|
|
321
|
-
bin: 'zypper',
|
|
322
|
-
steps: [['zypper', '--non-interactive', 'install', 'git']],
|
|
323
|
-
},
|
|
870
|
+
{ bin: 'zypper', steps: [['zypper', '--non-interactive', 'install', 'git']] },
|
|
324
871
|
{ bin: 'pacman', steps: [['pacman', '-S', '--noconfirm', 'git']] },
|
|
325
872
|
{ bin: 'apk', steps: [['apk', 'add', '--no-cache', 'git']] },
|
|
326
873
|
];
|
|
327
874
|
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
|
-
}
|
|
875
|
+
if (!pm) fail('git is missing and no supported package manager was found. Install git manually and re-run.');
|
|
335
876
|
|
|
336
877
|
log(`Installing git via ${pm.bin}${sudo.length ? ' (sudo)' : ''}...`);
|
|
337
878
|
const env = { ...process.env, DEBIAN_FRONTEND: 'noninteractive' };
|
|
@@ -341,18 +882,12 @@ function ensureGit() {
|
|
|
341
882
|
const ok = !r.error && r.status === 0;
|
|
342
883
|
if (!ok && !(pm.soft && pm.soft.includes(i))) {
|
|
343
884
|
const why = r.error ? r.error.code : `exit ${r.status}`;
|
|
344
|
-
|
|
885
|
+
fail(`git install failed: \`${argv.join(' ')}\` (${why}).`);
|
|
345
886
|
}
|
|
346
887
|
});
|
|
347
|
-
|
|
348
|
-
if (!commandWorks('git', ['--version'])) {
|
|
349
|
-
throw new Error('git still not available after the install attempt.');
|
|
350
|
-
}
|
|
888
|
+
if (!commandWorks('git', ['--version'])) fail('git still not available after the install attempt.');
|
|
351
889
|
}
|
|
352
890
|
|
|
353
|
-
// --- bb.edn bootstrap ----------------------------------------------------
|
|
354
|
-
|
|
355
|
-
// Env augmented so spawned processes find the cached JDK and bb; nothing system-wide.
|
|
356
891
|
function bbEnv(javaHome, bbPath, extraEnv = {}) {
|
|
357
892
|
const env = { ...process.env, ...extraEnv };
|
|
358
893
|
const pathEntries = [];
|
|
@@ -360,293 +895,44 @@ function bbEnv(javaHome, bbPath, extraEnv = {}) {
|
|
|
360
895
|
env.JAVA_HOME = javaHome;
|
|
361
896
|
pathEntries.push(path.join(javaHome, 'bin'));
|
|
362
897
|
}
|
|
363
|
-
if (bbPath)
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
if (pathEntries.length) {
|
|
367
|
-
env.PATH = pathEntries.join(path.delimiter) + path.delimiter + (env.PATH || '');
|
|
368
|
-
}
|
|
898
|
+
if (bbPath) pathEntries.push(path.dirname(bbPath));
|
|
899
|
+
if (pathEntries.length) env.PATH = pathEntries.join(path.delimiter) + path.delimiter + (env.PATH || '');
|
|
369
900
|
return env;
|
|
370
901
|
}
|
|
371
902
|
|
|
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
|
-
}
|
|
903
|
+
function runBb(bbPath, args, javaHome, extraEnv = {}) {
|
|
904
|
+
return runCommand(bbPath, args, { env: bbEnv(javaHome, bbPath, extraEnv) });
|
|
504
905
|
}
|
|
505
906
|
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
907
|
+
// --- main ----------------------------------------------------------------
|
|
908
|
+
|
|
909
|
+
async function main(argv) {
|
|
910
|
+
let args = [...argv];
|
|
911
|
+
const first = args.length ? args[0] : null;
|
|
912
|
+
const localSpec = first != null && isLocalSpec(first) ? parseLocalSpec(first) : null;
|
|
913
|
+
const spec = localSpec ? null : first ? parseSpec(first) : null;
|
|
914
|
+
if (localSpec || spec) args = args.slice(1);
|
|
915
|
+
|
|
916
|
+
let meta = readMetadata();
|
|
917
|
+
if (localSpec) {
|
|
918
|
+
if (meta) {
|
|
919
|
+
validateExistingLocalMetadata(meta, localSpec);
|
|
920
|
+
} else {
|
|
921
|
+
meta = initializeLocal(localSpec);
|
|
518
922
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
);
|
|
923
|
+
} else if (spec) {
|
|
924
|
+
const sha = await resolveRef(spec);
|
|
925
|
+
if (meta) {
|
|
926
|
+
validateExistingMetadata(meta, spec, sha);
|
|
927
|
+
} else {
|
|
928
|
+
meta = await initialize(spec, sha);
|
|
592
929
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
return { owner, project, sha };
|
|
597
|
-
} finally {
|
|
598
|
-
rmrf(tmp);
|
|
930
|
+
} else if (!meta) {
|
|
931
|
+
fail(`No BigConfig CLI is initialized in this directory.\n\n${usage()}`);
|
|
599
932
|
}
|
|
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
933
|
|
|
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);
|
|
934
|
+
await restoreRunIfMissing(meta);
|
|
935
|
+
const code = await runTarget(meta, args);
|
|
650
936
|
process.exit(code);
|
|
651
937
|
}
|
|
652
938
|
|
|
@@ -657,12 +943,18 @@ if (require.main === module) {
|
|
|
657
943
|
});
|
|
658
944
|
}
|
|
659
945
|
|
|
660
|
-
// Exported for tests / inspection.
|
|
661
946
|
module.exports = {
|
|
947
|
+
parseSpec,
|
|
948
|
+
isLocalSpec,
|
|
949
|
+
parseLocalSpec,
|
|
950
|
+
resolveRef,
|
|
951
|
+
detectTarget,
|
|
952
|
+
detectTargetLocal,
|
|
953
|
+
readMetadata,
|
|
954
|
+
validateExistingMetadata,
|
|
955
|
+
validateExistingLocalMetadata,
|
|
662
956
|
resolvePlatform,
|
|
663
957
|
cacheRoot,
|
|
664
958
|
findJavaHome,
|
|
665
959
|
ensureGit,
|
|
666
|
-
validateBbEdnRepo,
|
|
667
|
-
ensureBbEdn,
|
|
668
960
|
};
|