@mcmcjs/julia 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/driver.jl ADDED
@@ -0,0 +1,186 @@
1
+ # MCMC.js fit driver. Reads a run-request JSON, runs the spec's backend (Turing or
2
+ # JuliaBUGS), and writes the draws as MCMCChains-JSON (the @mcmcjs/core samples
3
+ # artifact). On success a single line of provenance JSON is printed to stdout;
4
+ # failures print one-line JSON to stderr ({error, stage}) and exit nonzero. This is
5
+ # the only place that touches the backend chain types, so a chain-type change is
6
+ # contained here.
7
+ using JSON
8
+ using Random
9
+ using StableRNGs
10
+ using Turing
11
+ using Turing: VarName
12
+ using MCMCChains
13
+ using SHA
14
+ using Pkg
15
+ # Imported (not `using`) so they do not pull `@model` etc. into Main and clash
16
+ # with Turing's exports, which would break Turing model files on include.
17
+ import JuliaBUGS
18
+ import AdvancedHMC
19
+ import ForwardDiff
20
+
21
+ function build_sampler(sampler, backend)
22
+ algorithm = get(sampler, "algorithm", "NUTS")
23
+ algorithm == "NUTS" || error("unsupported sampler algorithm: $algorithm")
24
+ warmup = Int(get(sampler, "warmup", 1000))
25
+ adapt_delta = Float64(get(sampler, "adapt_delta", 0.8))
26
+ nuts = backend == "juliabugs" ? AdvancedHMC.NUTS(adapt_delta) : Turing.NUTS(warmup, adapt_delta)
27
+ return nuts, warmup
28
+ end
29
+
30
+ to_namedtuple(data) = (; (Symbol(k) => v for (k, v) in data)...)
31
+
32
+ # JuliaBUGS requires dense arrays with a concrete numeric eltype, but JSON parses
33
+ # them as Vector{Any} (and nested arrays as vectors of vectors). narrow promotes a
34
+ # numeric array to a concrete eltype and stacks equal-sized nested arrays into a
35
+ # dense array; an empty array becomes Float64[].
36
+ function narrow(v::AbstractVector)
37
+ isempty(v) && return Float64[]
38
+ elems = [narrow(x) for x in v]
39
+ if all(e -> e isa AbstractArray, elems) && allequal(size.(elems))
40
+ return stack(elems; dims = 1)
41
+ end
42
+ return eltype(elems) <: Real && !isconcretetype(eltype(elems)) ? float.(elems) : elems
43
+ end
44
+ narrow(x) = x
45
+ bugs_namedtuple(data) = (; (Symbol(k) => narrow(v) for (k, v) in data)...)
46
+
47
+ # JSON null arrives as `nothing`; map it to `missing` so a blanked outcome becomes
48
+ # a sampling (predictive) statement in the model.
49
+ to_missing(x) = x === nothing ? missing : (x isa AbstractVector ? map(to_missing, x) : x)
50
+ predict_namedtuple(data) = (; (Symbol(k) => to_missing(v) for (k, v) in data)...)
51
+
52
+ # Rebuild a posterior Chains (latents only) from our MCMCChains-JSON wire object so
53
+ # it can feed Turing.predict. The varname_to_symbol map is required for VarName
54
+ # indexing; target outcome columns are excluded.
55
+ function chains_from_wire(wire, targets)
56
+ sz = wire["size"]
57
+ nIter, nParams, nChains = Int(sz[1]), Int(sz[2]), Int(sz[3])
58
+ flat = wire["value_flat"]
59
+ allnames = wire["parameters"]
60
+ is_target(n) = any(t -> n == t || startswith(n, t * "["), targets)
61
+ keep = [n for n in wire["name_map"]["parameters"] if !is_target(n)]
62
+ index = Dict(n => i for (i, n) in enumerate(allnames))
63
+ arr = Array{Float64,3}(undef, nIter, length(keep), nChains)
64
+ for (newp, n) in enumerate(keep)
65
+ p = index[n]
66
+ for c in 1:nChains, i in 1:nIter
67
+ v = flat[i + (p - 1) * nIter + (c - 1) * nIter * nParams]
68
+ arr[i, newp, c] = v === nothing ? NaN : Float64(v)
69
+ end
70
+ end
71
+ syms = Symbol.(keep)
72
+ vmap = Dict{VarName,Symbol}(eval(:(Turing.@varname($(Meta.parse(n))))) => Symbol(n) for n in keep)
73
+ return MCMCChains.Chains(arr, syms, Dict(:parameters => syms); info = (; varname_to_symbol = vmap))
74
+ end
75
+
76
+ # Build the model and sample in one call so both run in the latest world age:
77
+ # the model methods are defined by `include` at runtime, so a plain call from
78
+ # this (older) world would fail with "method too new". invokelatest fixes that.
79
+ function build_and_sample(entry, data, sampler, draws, chains, rng)
80
+ model = entry(data)
81
+ # Sample into Turing's default chain type (a FlexiChain), then convert to
82
+ # MCMCChains for serialization.
83
+ chn = sample(rng, model, sampler, MCMCSerial(), draws, chains; progress = false)
84
+ return MCMCChains.Chains(chn)
85
+ end
86
+
87
+ # JuliaBUGS compiles the model at runtime, so building and sampling must run in
88
+ # separate latest-world frames; merging them reintroduces a world-age error. The
89
+ # result is already an MCMCChains.Chains, so it is not re-wrapped.
90
+ function sample_bugs(model, sampler, warmup, draws, chains, rng)
91
+ return JuliaBUGS.AbstractMCMC.sample(
92
+ rng, model, sampler, JuliaBUGS.AbstractMCMC.MCMCSerial(), draws, chains;
93
+ chain_type = MCMCChains.Chains, n_adapts = warmup, discard_initial = warmup, progress = false,
94
+ )
95
+ end
96
+
97
+ # Build the exact wire object parseMCMCChainsJson consumes: value_flat is indexed
98
+ # i + p*nIter + c*nIter*nParams (column-major over [iteration, parameter, chain]).
99
+ function chains_to_wire(chn)
100
+ arr = Array(chn.value)
101
+ nIter, nParams, nChains = size(arr)
102
+ flat = Vector{Union{Float64,Nothing}}(undef, nIter * nParams * nChains)
103
+ for c in 1:nChains, p in 1:nParams, i in 1:nIter
104
+ v = arr[i, p, c]
105
+ flat[i + (p - 1) * nIter + (c - 1) * nIter * nParams] = v === missing ? nothing : Float64(v)
106
+ end
107
+ return Dict(
108
+ "size" => [nIter, nParams, nChains],
109
+ "value_flat" => flat,
110
+ "parameters" => string.(names(chn)),
111
+ "name_map" => Dict(
112
+ "parameters" => string.(get(chn.name_map, :parameters, Symbol[])),
113
+ "internals" => string.(get(chn.name_map, :internals, Symbol[])),
114
+ ),
115
+ )
116
+ end
117
+
118
+ function provenance()
119
+ packages = Dict{String,String}()
120
+ for (_, info) in Pkg.dependencies()
121
+ if info.name in
122
+ ("Turing", "MCMCChains", "DynamicPPL", "AbstractMCMC", "JuliaBUGS", "AdvancedHMC", "ForwardDiff", "ADTypes") &&
123
+ info.version !== nothing
124
+ packages[info.name] = string(info.version)
125
+ end
126
+ end
127
+ manifest = joinpath(dirname(Base.active_project()), "Manifest.toml")
128
+ return Dict(
129
+ "julia_version" => string(VERSION),
130
+ "packages" => packages,
131
+ "manifest_sha256" => isfile(manifest) ? bytes2hex(sha256(read(manifest))) : "",
132
+ )
133
+ end
134
+
135
+ function main()
136
+ request = JSON.parsefile(ARGS[1])
137
+ out = request["out"]
138
+ mode = get(request, "mode", "fit")
139
+ backend = get(get(request, "backend", Dict()), "id", "turing")
140
+ stage = "compile"
141
+ try
142
+ Random.seed!(Int(request["seed"]))
143
+ rng = StableRNG(Int(request["seed"]))
144
+ include(abspath(request["model"]["file"]))
145
+ entry = getfield(Main, Symbol(request["model"]["entry"]))
146
+
147
+ wire = if mode == "predict"
148
+ backend == "turing" || error("predict is not yet supported for backend $backend")
149
+ model = Base.invokelatest(entry, predict_namedtuple(request["data"]))
150
+ stage = "load_samples"
151
+ rchn = chains_from_wire(JSON.parsefile(request["samples"]), request["predict"]["targets"])
152
+ stage = "predict"
153
+ pp = Base.invokelatest(predict, rng, model, rchn)
154
+ isempty(names(pp)) &&
155
+ error("no variables predicted; check that [predict].targets name unconditioned outcomes")
156
+ chains_to_wire(pp)
157
+ else
158
+ data = backend == "juliabugs" ? bugs_namedtuple(request["data"]) : to_namedtuple(request["data"])
159
+ sampler, warmup = build_sampler(request["sampler"], backend)
160
+ draws = Int(request["sampler"]["draws"])
161
+ chains = Int(get(request["sampler"], "chains", 4))
162
+ stage = "sample"
163
+ chn = if backend == "juliabugs"
164
+ model = Base.invokelatest(entry, data)
165
+ Base.invokelatest(sample_bugs, model, sampler, warmup, draws, chains, rng)
166
+ else
167
+ Base.invokelatest(build_and_sample, entry, data, sampler, draws, chains, rng)
168
+ end
169
+ chains_to_wire(chn)
170
+ end
171
+
172
+ stage = "write"
173
+ tmp = out * ".tmp"
174
+ open(tmp, "w") do io
175
+ JSON.print(io, wire)
176
+ end
177
+ mv(tmp, out; force = true)
178
+
179
+ println(JSON.json(provenance()))
180
+ catch err
181
+ println(stderr, JSON.json(Dict("error" => sprint(showerror, err), "stage" => stage)))
182
+ exit(1)
183
+ end
184
+ end
185
+
186
+ main()
package/dist/index.cjs CHANGED
@@ -20,22 +20,41 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ addVersion: () => addVersion,
24
+ assertVersionsInstalled: () => assertVersionsInstalled,
23
25
  detectJulia: () => detectJulia,
24
26
  detectJuliaup: () => detectJuliaup,
25
- runDoctor: () => runDoctor
27
+ ensureProject: () => ensureProject,
28
+ gcVersions: () => gcVersions,
29
+ juliaEngine: () => juliaEngine,
30
+ juliaupInstallCommand: () => juliaupInstallCommand,
31
+ listVersions: () => listVersions,
32
+ managedProjectDir: () => managedProjectDir,
33
+ managedProjectReady: () => managedProjectReady,
34
+ planSetup: () => planSetup,
35
+ predictData: () => predictData,
36
+ removeVersion: () => removeVersion,
37
+ resolveVersion: () => resolveVersion,
38
+ runDoctor: () => runDoctor,
39
+ runFit: () => runFit,
40
+ runMatrix: () => runMatrix,
41
+ runPredict: () => runPredict,
42
+ runSetup: () => runSetup,
43
+ setDefaultVersion: () => setDefaultVersion,
44
+ updateVersion: () => updateVersion
26
45
  });
27
46
  module.exports = __toCommonJS(index_exports);
28
47
 
48
+ // ../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.15_typescript@6.0.3/node_modules/tsup/assets/cjs_shims.js
49
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
50
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
51
+
29
52
  // src/environment.ts
30
- var import_node_child_process = require("child_process");
31
53
  var import_node_os = require("os");
32
54
  var import_node_path = require("path");
33
- var import_node_util = require("util");
34
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
35
- var defaultRunner = async (command, args) => {
36
- const { stdout } = await execFileAsync(command, args, { timeout: 1e4 });
37
- return stdout;
38
- };
55
+ var import_engine = require("@mcmcjs/engine");
56
+ var import_engine2 = require("@mcmcjs/engine");
57
+ var defaultRunner = (0, import_engine.createRunner)();
39
58
  function candidates(binary) {
40
59
  return [(0, import_node_path.join)((0, import_node_os.homedir)(), ".juliaup", "bin", binary), binary];
41
60
  }
@@ -62,10 +81,465 @@ async function runDoctor(runner) {
62
81
  const [juliaup, julia] = await Promise.all([detectJuliaup(runner), detectJulia(runner)]);
63
82
  return { juliaup, julia, ready: julia.found };
64
83
  }
84
+
85
+ // src/engine.ts
86
+ var JULIAUP_URL = "https://github.com/JuliaLang/juliaup";
87
+ var juliaEngine = {
88
+ id: "julia",
89
+ displayName: "Julia",
90
+ capabilities: { setup: true, versions: true, fit: true, predict: true },
91
+ async doctor(ctx) {
92
+ const report = await runDoctor(ctx.run);
93
+ return {
94
+ engineId: "julia",
95
+ ready: report.ready,
96
+ tools: [
97
+ { name: "juliaup", ...report.juliaup },
98
+ { name: "julia", ...report.julia }
99
+ ],
100
+ hint: report.ready ? void 0 : `Julia not found. Install it with juliaup (${JULIAUP_URL}), or run \`mcmc setup\`.`
101
+ };
102
+ }
103
+ };
104
+
105
+ // src/fit.ts
106
+ var import_node_fs = require("fs");
107
+ var import_node_os2 = require("os");
108
+ var import_node_path3 = require("path");
109
+ var import_core = require("@mcmcjs/core");
110
+
111
+ // src/runner-common.ts
112
+ var import_node_crypto = require("crypto");
113
+ var import_node_path2 = require("path");
114
+ var import_node_url = require("url");
115
+ function driverPath() {
116
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl)), "driver.jl");
117
+ }
118
+ function sha256(text) {
119
+ return (0, import_node_crypto.createHash)("sha256").update(text).digest("hex");
120
+ }
121
+ function lastJsonLine(text) {
122
+ for (const line of text.trimEnd().split("\n").reverse()) {
123
+ const trimmed = line.trim();
124
+ if (!trimmed.startsWith("{")) continue;
125
+ try {
126
+ return JSON.parse(trimmed);
127
+ } catch {
128
+ return void 0;
129
+ }
130
+ }
131
+ return void 0;
132
+ }
133
+ var STAGES = /* @__PURE__ */ new Set([
134
+ "compile",
135
+ "sample",
136
+ "load_samples",
137
+ "predict",
138
+ "write",
139
+ "spawn"
140
+ ]);
141
+ function toStage(value) {
142
+ return typeof value === "string" && STAGES.has(value) ? value : void 0;
143
+ }
144
+
145
+ // src/fit.ts
146
+ async function runFit(spec, resolved, io) {
147
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
148
+ const start = performance.now();
149
+ const runtimeRequested = spec.backend.version;
150
+ const tmp = io.tmpDir ?? (0, import_node_fs.mkdtempSync)((0, import_node_path3.join)((0, import_node_os2.tmpdir)(), "mcmcjs-fit-"));
151
+ const requestPath = (0, import_node_path3.join)(tmp, "request.json");
152
+ (0, import_node_fs.writeFileSync)(
153
+ requestPath,
154
+ JSON.stringify({
155
+ schema_version: spec.schema_version,
156
+ backend: { id: spec.backend.id },
157
+ model: { file: spec.modelPath, entry: spec.model.entry },
158
+ data: spec.data,
159
+ sampler: spec.sampler,
160
+ seed: spec.seed,
161
+ out: io.outPath
162
+ })
163
+ );
164
+ const args = [
165
+ ...resolved.args,
166
+ "--startup-file=no",
167
+ `--project=${io.projectDir}`,
168
+ driverPath(),
169
+ requestPath
170
+ ];
171
+ const { stdout, stderr, code } = await io.spawn(resolved.command, args);
172
+ const elapsedMs = Math.round(performance.now() - start);
173
+ if (code !== 0 || !(0, import_node_fs.existsSync)(io.outPath)) {
174
+ const failure = lastJsonLine(stderr);
175
+ const stage = toStage(failure?.stage);
176
+ const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
177
+ return { status: "error", runtimeRequested, elapsedMs, stage, error };
178
+ }
179
+ try {
180
+ (0, import_core.parseSamples)((0, import_node_fs.readFileSync)(io.outPath, "utf8"));
181
+ } catch (error) {
182
+ return {
183
+ status: "error",
184
+ runtimeRequested,
185
+ elapsedMs,
186
+ stage: "write",
187
+ error: `samples file did not parse: ${error.message}`
188
+ };
189
+ }
190
+ const provenance = lastJsonLine(stdout);
191
+ const julia = provenance?.julia_version;
192
+ const record = {
193
+ schema_version: import_core.RUN_RECORD_SCHEMA_VERSION,
194
+ spec_hash: spec.specHash,
195
+ seed: spec.seed,
196
+ backend: { id: spec.backend.id, runtime: spec.backend.runtime },
197
+ runtime: { requested: runtimeRequested, actual: julia, path: resolved.command },
198
+ manifest_sha256: provenance?.manifest_sha256,
199
+ packages: provenance?.packages,
200
+ model_sha256: (0, import_node_fs.existsSync)(spec.modelPath) ? sha256((0, import_node_fs.readFileSync)(spec.modelPath, "utf8")) : void 0,
201
+ data_sha256: sha256((0, import_core.canonicalJson)(spec.data)),
202
+ samples_file: io.outPath,
203
+ started_at: startedAt,
204
+ elapsed_ms: elapsedMs
205
+ };
206
+ (0, import_node_fs.writeFileSync)(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
207
+ `);
208
+ return {
209
+ status: "ok",
210
+ samplesFile: io.outPath,
211
+ runtimeRequested,
212
+ runtimeActual: julia,
213
+ elapsedMs
214
+ };
215
+ }
216
+
217
+ // src/predict.ts
218
+ var import_node_fs2 = require("fs");
219
+ var import_node_os3 = require("os");
220
+ var import_node_path4 = require("path");
221
+ var import_core2 = require("@mcmcjs/core");
222
+ function predictData(spec) {
223
+ const predict = spec.predict;
224
+ if (!predict) throw new Error("spec has no [predict] block");
225
+ const data = { ...spec.data, ...predict.data ?? {} };
226
+ const blank = (value) => Array.isArray(value) ? value.map(blank) : null;
227
+ for (const target of predict.targets) {
228
+ data[target] = blank(data[target]);
229
+ }
230
+ return data;
231
+ }
232
+ async function runPredict(spec, resolved, io) {
233
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
234
+ const start = performance.now();
235
+ const runtimeRequested = spec.backend.version;
236
+ const data = predictData(spec);
237
+ const tmp = io.tmpDir ?? (0, import_node_fs2.mkdtempSync)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "mcmcjs-predict-"));
238
+ const requestPath = (0, import_node_path4.join)(tmp, "request.json");
239
+ (0, import_node_fs2.writeFileSync)(
240
+ requestPath,
241
+ JSON.stringify({
242
+ mode: "predict",
243
+ schema_version: spec.schema_version,
244
+ backend: { id: spec.backend.id },
245
+ model: { file: spec.modelPath, entry: spec.model.entry },
246
+ data,
247
+ predict: { targets: spec.predict?.targets ?? [] },
248
+ samples: io.samplesPath,
249
+ seed: spec.seed,
250
+ out: io.outPath
251
+ })
252
+ );
253
+ const args = [
254
+ ...resolved.args,
255
+ "--startup-file=no",
256
+ `--project=${io.projectDir}`,
257
+ driverPath(),
258
+ requestPath
259
+ ];
260
+ const { stdout, stderr, code } = await io.spawn(resolved.command, args);
261
+ const elapsedMs = Math.round(performance.now() - start);
262
+ if (code !== 0 || !(0, import_node_fs2.existsSync)(io.outPath)) {
263
+ const failure = lastJsonLine(stderr);
264
+ const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
265
+ return { status: "error", runtimeRequested, elapsedMs, stage: toStage(failure?.stage), error };
266
+ }
267
+ try {
268
+ (0, import_core2.parseSamples)((0, import_node_fs2.readFileSync)(io.outPath, "utf8"));
269
+ } catch (error) {
270
+ return {
271
+ status: "error",
272
+ runtimeRequested,
273
+ elapsedMs,
274
+ stage: "write",
275
+ error: `samples file did not parse: ${error.message}`
276
+ };
277
+ }
278
+ const provenance = lastJsonLine(stdout);
279
+ const julia = provenance?.julia_version;
280
+ const record = {
281
+ schema_version: import_core2.RUN_RECORD_SCHEMA_VERSION,
282
+ spec_hash: spec.specHash,
283
+ seed: spec.seed,
284
+ backend: { id: spec.backend.id, runtime: spec.backend.runtime },
285
+ runtime: { requested: runtimeRequested, actual: julia, path: resolved.command },
286
+ manifest_sha256: provenance?.manifest_sha256,
287
+ packages: provenance?.packages,
288
+ model_sha256: (0, import_node_fs2.existsSync)(spec.modelPath) ? sha256((0, import_node_fs2.readFileSync)(spec.modelPath, "utf8")) : void 0,
289
+ data_sha256: sha256((0, import_core2.canonicalJson)(data)),
290
+ posterior_samples: io.samplesPath,
291
+ posterior_samples_sha256: (0, import_node_fs2.existsSync)(io.samplesPath) ? sha256((0, import_node_fs2.readFileSync)(io.samplesPath, "utf8")) : void 0,
292
+ samples_file: io.outPath,
293
+ started_at: startedAt,
294
+ elapsed_ms: elapsedMs
295
+ };
296
+ (0, import_node_fs2.writeFileSync)(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
297
+ `);
298
+ return {
299
+ status: "ok",
300
+ samplesFile: io.outPath,
301
+ runtimeRequested,
302
+ runtimeActual: julia,
303
+ elapsedMs
304
+ };
305
+ }
306
+
307
+ // src/project.ts
308
+ var import_node_fs3 = require("fs");
309
+ var import_node_os4 = require("os");
310
+ var import_node_path5 = require("path");
311
+ var import_engine3 = require("@mcmcjs/engine");
312
+ var PACKAGES = [
313
+ "Turing",
314
+ "MCMCChains",
315
+ "JuliaBUGS",
316
+ "AdvancedHMC",
317
+ "ForwardDiff",
318
+ "JSON",
319
+ "StableRNGs"
320
+ ];
321
+ function managedProjectDir() {
322
+ const base = process.env.XDG_DATA_HOME || (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".local", "share");
323
+ return (0, import_node_path5.join)(base, "mcmcjs", "julia", "env");
324
+ }
325
+ function sentinelPath(dir) {
326
+ return (0, import_node_path5.join)(dir, ".mcmcjs-packages.json");
327
+ }
328
+ function provisioned(dir) {
329
+ try {
330
+ return (0, import_node_fs3.readFileSync)(sentinelPath(dir), "utf8").trim() === JSON.stringify([...PACKAGES].sort());
331
+ } catch {
332
+ return false;
333
+ }
334
+ }
335
+ function managedProjectReady(dir = managedProjectDir()) {
336
+ return (0, import_node_fs3.existsSync)((0, import_node_path5.join)(dir, "Project.toml")) && provisioned(dir);
337
+ }
338
+ async function ensureProject(juliaBin, run = (0, import_engine3.createRunner)(30 * 6e4), dir = managedProjectDir()) {
339
+ if (managedProjectReady(dir)) return dir;
340
+ (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
341
+ const code = `using Pkg; Pkg.add(${JSON.stringify(PACKAGES)}); Pkg.precompile()`;
342
+ await run(juliaBin, ["--startup-file=no", `--project=${dir}`, "-e", code]);
343
+ (0, import_node_fs3.writeFileSync)(sentinelPath(dir), JSON.stringify([...PACKAGES].sort()));
344
+ return dir;
345
+ }
346
+
347
+ // src/run-matrix.ts
348
+ var import_node_path6 = require("path");
349
+ async function runMatrix(spec, versions, io) {
350
+ const entries = [];
351
+ for (const version of versions) {
352
+ let entry;
353
+ try {
354
+ const resolved = await io.resolve(version);
355
+ const versioned = { ...spec, backend: { ...spec.backend, version } };
356
+ const result = await runFit(versioned, resolved, {
357
+ spawn: io.spawn,
358
+ projectDir: io.projectDir,
359
+ outPath: (0, import_node_path6.join)(io.outDir, `${version}.samples.json`)
360
+ });
361
+ entry = {
362
+ version,
363
+ status: result.status,
364
+ samplesFile: result.samplesFile,
365
+ runtimeActual: result.runtimeActual,
366
+ elapsedMs: result.elapsedMs,
367
+ stage: result.stage,
368
+ error: result.error
369
+ };
370
+ } catch (error) {
371
+ entry = { version, status: "error", elapsedMs: 0, error: error.message };
372
+ }
373
+ entries.push(entry);
374
+ if (entry.status === "error" && !io.keepGoing) break;
375
+ }
376
+ return {
377
+ entries,
378
+ ok: entries.length === versions.length && entries.every((e) => e.status === "ok")
379
+ };
380
+ }
381
+
382
+ // src/setup.ts
383
+ var JULIAUP_INSTALL_URL = "https://install.julialang.org";
384
+ function juliaupInstallCommand(platform) {
385
+ if (platform === "win32") {
386
+ return null;
387
+ }
388
+ return { command: "sh", args: ["-c", `curl -fsSL ${JULIAUP_INSTALL_URL} | sh -s -- --yes`] };
389
+ }
390
+ function planSetup(report, platform) {
391
+ if (report.julia.found) return [];
392
+ if (report.juliaup.found) {
393
+ return [
394
+ {
395
+ tool: "julia",
396
+ label: "install the latest stable Julia via juliaup",
397
+ command: { command: report.juliaup.path ?? "juliaup", args: ["add", "release"] }
398
+ }
399
+ ];
400
+ }
401
+ return [
402
+ {
403
+ tool: "juliaup",
404
+ label: "install juliaup (this also installs Julia)",
405
+ command: juliaupInstallCommand(platform)
406
+ }
407
+ ];
408
+ }
409
+ async function runSetup(options = {}) {
410
+ const {
411
+ runner,
412
+ installer = (0, import_engine2.createRunner)(15 * 6e4),
413
+ platform = process.platform,
414
+ dryRun = false
415
+ } = options;
416
+ const before = await runDoctor(runner);
417
+ if (before.ready) return { ...before, steps: [] };
418
+ const plan = planSetup(before, platform);
419
+ if (dryRun) {
420
+ return {
421
+ ...before,
422
+ steps: plan.map((step) => ({ ...step, status: step.command ? "skipped" : "unsupported" }))
423
+ };
424
+ }
425
+ const steps = [];
426
+ for (const step of plan) {
427
+ if (!step.command) {
428
+ steps.push({ ...step, status: "unsupported" });
429
+ continue;
430
+ }
431
+ try {
432
+ await installer(step.command.command, step.command.args);
433
+ steps.push({ ...step, status: "ran" });
434
+ } catch (error) {
435
+ const detail = error instanceof Error ? error.message : String(error);
436
+ steps.push({ ...step, status: "failed", detail });
437
+ }
438
+ }
439
+ const after = await runDoctor(runner);
440
+ return { juliaup: after.juliaup, julia: after.julia, ready: after.ready, steps };
441
+ }
442
+
443
+ // src/versions.ts
444
+ function parseChannel(value) {
445
+ const c = value;
446
+ if (!c || typeof c.Name !== "string" || typeof c.File !== "string") {
447
+ throw new Error("unexpected `juliaup api getconfig1` channel shape");
448
+ }
449
+ return {
450
+ Name: c.Name,
451
+ File: c.File,
452
+ Args: Array.isArray(c.Args) ? c.Args : void 0,
453
+ Version: typeof c.Version === "string" ? c.Version : void 0,
454
+ Arch: typeof c.Arch === "string" ? c.Arch : void 0
455
+ };
456
+ }
457
+ function parseGetconfig1(raw) {
458
+ let data;
459
+ try {
460
+ data = JSON.parse(raw);
461
+ } catch {
462
+ throw new Error("could not parse `juliaup api getconfig1` output as JSON");
463
+ }
464
+ const obj = data;
465
+ if (!obj || typeof obj !== "object" || !Array.isArray(obj.OtherChannels)) {
466
+ throw new Error("unexpected `juliaup api getconfig1` output");
467
+ }
468
+ return {
469
+ DefaultChannel: obj.DefaultChannel ? parseChannel(obj.DefaultChannel) : null,
470
+ OtherChannels: obj.OtherChannels.map(parseChannel)
471
+ };
472
+ }
473
+ function toVersion(channel, isDefault) {
474
+ return { id: channel.Name, version: channel.Version, path: channel.File || void 0, isDefault };
475
+ }
476
+ async function listVersions(bin, run) {
477
+ const config = parseGetconfig1(await run(bin, ["api", "getconfig1"]));
478
+ const versions = [];
479
+ if (config.DefaultChannel) versions.push(toVersion(config.DefaultChannel, true));
480
+ for (const channel of config.OtherChannels) versions.push(toVersion(channel, false));
481
+ return versions;
482
+ }
483
+ async function addVersion(bin, channel, opts, run) {
484
+ await run(bin, ["add", channel]);
485
+ if (opts.default) await run(bin, ["default", channel]);
486
+ }
487
+ async function removeVersion(bin, channel, run) {
488
+ await run(bin, ["remove", channel]);
489
+ }
490
+ async function setDefaultVersion(bin, channel, run) {
491
+ await run(bin, ["default", channel]);
492
+ }
493
+ async function updateVersion(bin, channel, run) {
494
+ await run(bin, channel ? ["update", channel] : ["update"]);
495
+ }
496
+ async function gcVersions(bin, run) {
497
+ await run(bin, ["gc"]);
498
+ }
499
+ async function resolveVersion(bin, channel, run) {
500
+ const config = parseGetconfig1(await run(bin, ["api", "getconfig1"]));
501
+ const all = config.DefaultChannel ? [config.DefaultChannel, ...config.OtherChannels] : config.OtherChannels;
502
+ const match = all.find((c) => c.Name === channel);
503
+ if (!match?.File) {
504
+ throw new Error(
505
+ `Julia version "${channel}" is not installed. Add it with: mcmc julia version add ${channel}`
506
+ );
507
+ }
508
+ return { command: match.File, args: match.Args ?? [] };
509
+ }
510
+ function assertVersionsInstalled(channels, installed) {
511
+ const have = new Set(installed.map((v) => v.id));
512
+ const missing = channels.filter((c) => !have.has(c));
513
+ if (missing.length > 0) {
514
+ const fixes = missing.map((m) => `mcmc julia version add ${m}`).join("; ");
515
+ throw new Error(
516
+ `Julia version(s) not installed: ${missing.join(", ")}. Install with: ${fixes}`
517
+ );
518
+ }
519
+ }
65
520
  // Annotate the CommonJS export names for ESM import in node:
66
521
  0 && (module.exports = {
522
+ addVersion,
523
+ assertVersionsInstalled,
67
524
  detectJulia,
68
525
  detectJuliaup,
69
- runDoctor
526
+ ensureProject,
527
+ gcVersions,
528
+ juliaEngine,
529
+ juliaupInstallCommand,
530
+ listVersions,
531
+ managedProjectDir,
532
+ managedProjectReady,
533
+ planSetup,
534
+ predictData,
535
+ removeVersion,
536
+ resolveVersion,
537
+ runDoctor,
538
+ runFit,
539
+ runMatrix,
540
+ runPredict,
541
+ runSetup,
542
+ setDefaultVersion,
543
+ updateVersion
70
544
  });
71
545
  //# sourceMappingURL=index.cjs.map