bc-pkg 1.0.1 → 1.0.3
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 +31 -0
- package/bin/bc-pkg.js +216 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,37 @@ npx bc-pkg bigconfig-ai/once@2f4e8c0d0b4c4b8f0c3a9f6e2a1b5c7d8e9f0123 package va
|
|
|
24
24
|
On the first run, `bc-pkg` resolves the ref to a full SHA and pins it. Later
|
|
25
25
|
runs omit `<owner/repo@ref>` and keep using the pinned SHA.
|
|
26
26
|
|
|
27
|
+
## Local repositories
|
|
28
|
+
|
|
29
|
+
For live local development you can point `bc-pkg` at a local checkout of a
|
|
30
|
+
target package instead of a GitHub spec. The first argument is treated as a
|
|
31
|
+
local path when it starts with `/`, `./`, `../`, `~`, or is `.`/`..`:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npx bc-pkg ../once/typescript package build
|
|
35
|
+
npx bc-pkg /abs/path/to/once/clojure package build
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Local targets are wired up **live** — your uncommitted edits in the local
|
|
39
|
+
package are picked up on the next run, with no SHA pinning and no push:
|
|
40
|
+
|
|
41
|
+
| Target language | Local dependency |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| Clojure | `deps.edn` / `bb.edn` use `:local/root` |
|
|
44
|
+
| TypeScript | `package.json` uses a `file:` dependency |
|
|
45
|
+
| Python | `pyproject.toml` uses an editable `[tool.uv.sources]` path |
|
|
46
|
+
|
|
47
|
+
The `run` file is symlinked (not copied) so run-file edits are also live. Run
|
|
48
|
+
`bc-pkg` from a **separate** directory; pointing it at the current directory is
|
|
49
|
+
refused so it never overwrites the package's own manifest. Switching an
|
|
50
|
+
initialized directory between local and GitHub (or to a different local path) is
|
|
51
|
+
a hard error, just like a repo/ref/SHA mismatch.
|
|
52
|
+
|
|
53
|
+
Notes: TypeScript local dev requires the local package to be built (its
|
|
54
|
+
`dist/`); Python local dev installs the package editable. Python template data
|
|
55
|
+
ships as a top-level `resources` package, which BigConfig's renderer resolves
|
|
56
|
+
through `importlib.resources`, so no `./resources` directory is created.
|
|
57
|
+
|
|
27
58
|
## What is created
|
|
28
59
|
|
|
29
60
|
The launcher copies the target package's root `run` file into the current
|
package/bin/bc-pkg.js
CHANGED
|
@@ -27,7 +27,7 @@ function fail(msg) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
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`;
|
|
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
31
|
}
|
|
32
32
|
|
|
33
33
|
// --- generic process helpers -------------------------------------------
|
|
@@ -90,6 +90,37 @@ function parseSpec(arg) {
|
|
|
90
90
|
return { owner: m[1], repo: m[2], ref: m[3], slug: `${m[1]}/${m[2]}` };
|
|
91
91
|
}
|
|
92
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
|
+
|
|
93
124
|
function ghHeaders(accept) {
|
|
94
125
|
const headers = {
|
|
95
126
|
'user-agent': 'bc-pkg',
|
|
@@ -160,35 +191,63 @@ function sectionText(text, name) {
|
|
|
160
191
|
return next ? rest.slice(0, next.index) : rest;
|
|
161
192
|
}
|
|
162
193
|
|
|
163
|
-
|
|
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
|
-
|
|
194
|
+
function detectTargetFromManifests(label, depsEdn, packageJsonText, pyprojectText, clojureCoord) {
|
|
170
195
|
const found = [];
|
|
171
196
|
if (depsEdn != null) found.push('clojure');
|
|
172
197
|
if (packageJsonText != null) found.push('typescript');
|
|
173
198
|
if (pyprojectText != null) found.push('python');
|
|
174
199
|
if (found.length === 0) {
|
|
175
|
-
fail(`${
|
|
200
|
+
fail(`${label} has no deps.edn, package.json, or pyproject.toml`);
|
|
176
201
|
}
|
|
177
202
|
if (found.length > 1) {
|
|
178
|
-
fail(`${
|
|
203
|
+
fail(`${label} is ambiguous; found ${found.join(', ')} manifests`);
|
|
179
204
|
}
|
|
180
205
|
|
|
181
206
|
if (found[0] === 'typescript') {
|
|
182
207
|
const pkg = parseJson(packageJsonText, 'package.json');
|
|
183
|
-
if (!pkg.name) fail(`${
|
|
208
|
+
if (!pkg.name) fail(`${label} package.json has no name`);
|
|
184
209
|
return { language: 'typescript', packageName: pkg.name };
|
|
185
210
|
}
|
|
186
211
|
if (found[0] === 'python') {
|
|
187
212
|
const packageName = parsePyProjectName(pyprojectText);
|
|
188
|
-
if (!packageName) fail(`${
|
|
213
|
+
if (!packageName) fail(`${label} pyproject.toml has no [project].name`);
|
|
189
214
|
return { language: 'python', packageName };
|
|
190
215
|
}
|
|
191
|
-
return { language: 'clojure', packageName:
|
|
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
|
+
);
|
|
192
251
|
}
|
|
193
252
|
|
|
194
253
|
// --- native metadata -----------------------------------------------------
|
|
@@ -216,6 +275,8 @@ function metadataFromPyproject(file) {
|
|
|
216
275
|
language: get('language') || 'python',
|
|
217
276
|
run: get('run') || 'run',
|
|
218
277
|
packageName: get('package-name'),
|
|
278
|
+
local: /^\s*local\s*=\s*true\s*$/m.test(sec),
|
|
279
|
+
path: get('path'),
|
|
219
280
|
manifest: file,
|
|
220
281
|
};
|
|
221
282
|
}
|
|
@@ -223,7 +284,7 @@ function metadataFromPyproject(file) {
|
|
|
223
284
|
function metadataFromDepsEdn(file) {
|
|
224
285
|
if (!fs.existsSync(file)) return null;
|
|
225
286
|
const text = fs.readFileSync(file, 'utf8');
|
|
226
|
-
if (!text.includes(':bigconfig/repo')) return null;
|
|
287
|
+
if (!text.includes(':bigconfig/repo') && !text.includes(':bigconfig/path')) return null;
|
|
227
288
|
const get = (key) => {
|
|
228
289
|
const m = text.match(new RegExp(`:${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s+"([^"]+)"`));
|
|
229
290
|
return m ? m[1] : undefined;
|
|
@@ -234,6 +295,8 @@ function metadataFromDepsEdn(file) {
|
|
|
234
295
|
sha: get('bigconfig/sha'),
|
|
235
296
|
language: get('bigconfig/language') || 'clojure',
|
|
236
297
|
run: get('bigconfig/run') || 'run',
|
|
298
|
+
local: get('bigconfig/local') === 'true',
|
|
299
|
+
path: get('bigconfig/path'),
|
|
237
300
|
manifest: file,
|
|
238
301
|
};
|
|
239
302
|
}
|
|
@@ -249,13 +312,18 @@ function readMetadata(cwd = process.cwd()) {
|
|
|
249
312
|
}
|
|
250
313
|
if (metas.length === 0) return null;
|
|
251
314
|
const meta = metas[0];
|
|
252
|
-
if (
|
|
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) {
|
|
253
318
|
fail(`Incomplete BigConfig metadata in ${meta.manifest}`);
|
|
254
319
|
}
|
|
255
320
|
return meta;
|
|
256
321
|
}
|
|
257
322
|
|
|
258
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
|
+
}
|
|
259
327
|
const expectedRepo = spec.slug;
|
|
260
328
|
const problems = [];
|
|
261
329
|
if (meta.repo !== expectedRepo) problems.push(`repo ${JSON.stringify(meta.repo)} != ${JSON.stringify(expectedRepo)}`);
|
|
@@ -266,6 +334,23 @@ function validateExistingMetadata(meta, spec, sha) {
|
|
|
266
334
|
}
|
|
267
335
|
}
|
|
268
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
|
+
|
|
269
354
|
function quoteToml(s) {
|
|
270
355
|
return JSON.stringify(String(s));
|
|
271
356
|
}
|
|
@@ -276,6 +361,24 @@ function writeRunFile(text) {
|
|
|
276
361
|
if (process.platform !== 'win32') fs.chmodSync(target, 0o755);
|
|
277
362
|
}
|
|
278
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
|
+
|
|
279
382
|
function clojureCoord(spec) {
|
|
280
383
|
return `io.github.${spec.owner}/${spec.repo}`;
|
|
281
384
|
}
|
|
@@ -335,6 +438,62 @@ function writeNativeManifest(spec, sha, target) {
|
|
|
335
438
|
fail(`Unsupported language: ${target.language}`);
|
|
336
439
|
}
|
|
337
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
|
+
|
|
338
497
|
// --- target dependency setup and execution -------------------------------
|
|
339
498
|
|
|
340
499
|
async function ensureTargetDeps(meta) {
|
|
@@ -395,9 +554,36 @@ async function initialize(spec, sha) {
|
|
|
395
554
|
};
|
|
396
555
|
}
|
|
397
556
|
|
|
557
|
+
function initializeLocal(local) {
|
|
558
|
+
if (local.path === fs.realpathSync(process.cwd())) {
|
|
559
|
+
fail("local path is the current directory; run bc-pkg from a separate directory so it does not overwrite the package's own manifest.");
|
|
560
|
+
}
|
|
561
|
+
const target = detectTargetLocal(local);
|
|
562
|
+
const runSource = path.join(local.path, 'run');
|
|
563
|
+
if (!fs.existsSync(runSource) || !fs.statSync(runSource).isFile()) fail(`${local.path} has no run`);
|
|
564
|
+
linkRunFile(runSource);
|
|
565
|
+
writeNativeManifestLocal(local, target);
|
|
566
|
+
return {
|
|
567
|
+
repo: null,
|
|
568
|
+
ref: null,
|
|
569
|
+
sha: null,
|
|
570
|
+
language: target.language,
|
|
571
|
+
run: 'run',
|
|
572
|
+
packageName: target.packageName,
|
|
573
|
+
local: true,
|
|
574
|
+
path: local.path,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
398
578
|
async function restoreRunIfMissing(meta) {
|
|
399
579
|
const runPath = path.join(process.cwd(), meta.run || 'run');
|
|
400
580
|
if (fs.existsSync(runPath)) return;
|
|
581
|
+
if (meta.local) {
|
|
582
|
+
const source = path.join(meta.path, meta.run || 'run');
|
|
583
|
+
if (!fs.existsSync(source)) fail(`${meta.path} has no ${meta.run || 'run'}`);
|
|
584
|
+
linkRunFile(source);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
401
587
|
const [owner, repo] = meta.repo.split('/');
|
|
402
588
|
const spec = { owner, repo, slug: meta.repo, ref: meta.ref };
|
|
403
589
|
const runText = await fetchFile(spec, meta.sha, 'run', { required: true });
|
|
@@ -652,11 +838,19 @@ function runBb(bbPath, args, javaHome, extraEnv = {}) {
|
|
|
652
838
|
|
|
653
839
|
async function main(argv) {
|
|
654
840
|
let args = [...argv];
|
|
655
|
-
|
|
656
|
-
|
|
841
|
+
const first = args.length ? args[0] : null;
|
|
842
|
+
const localSpec = first != null && isLocalSpec(first) ? parseLocalSpec(first) : null;
|
|
843
|
+
const spec = localSpec ? null : first ? parseSpec(first) : null;
|
|
844
|
+
if (localSpec || spec) args = args.slice(1);
|
|
657
845
|
|
|
658
846
|
let meta = readMetadata();
|
|
659
|
-
if (
|
|
847
|
+
if (localSpec) {
|
|
848
|
+
if (meta) {
|
|
849
|
+
validateExistingLocalMetadata(meta, localSpec);
|
|
850
|
+
} else {
|
|
851
|
+
meta = initializeLocal(localSpec);
|
|
852
|
+
}
|
|
853
|
+
} else if (spec) {
|
|
660
854
|
const sha = await resolveRef(spec);
|
|
661
855
|
if (meta) {
|
|
662
856
|
validateExistingMetadata(meta, spec, sha);
|
|
@@ -681,10 +875,14 @@ if (require.main === module) {
|
|
|
681
875
|
|
|
682
876
|
module.exports = {
|
|
683
877
|
parseSpec,
|
|
878
|
+
isLocalSpec,
|
|
879
|
+
parseLocalSpec,
|
|
684
880
|
resolveRef,
|
|
685
881
|
detectTarget,
|
|
882
|
+
detectTargetLocal,
|
|
686
883
|
readMetadata,
|
|
687
884
|
validateExistingMetadata,
|
|
885
|
+
validateExistingLocalMetadata,
|
|
688
886
|
resolvePlatform,
|
|
689
887
|
cacheRoot,
|
|
690
888
|
findJavaHome,
|