askmri 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/bootstrap/uv.d.ts +45 -0
- package/dist/bootstrap/uv.js +138 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +213 -0
- package/dist/commands/passthrough.d.ts +10 -0
- package/dist/commands/passthrough.js +25 -0
- package/dist/commands/setup.d.ts +6 -0
- package/dist/commands/setup.js +40 -0
- package/dist/commands/version.d.ts +2 -0
- package/dist/commands/version.js +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +152 -0
- package/dist/log.d.ts +16 -0
- package/dist/log.js +85 -0
- package/dist/paths.d.ts +48 -0
- package/dist/paths.js +78 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type EnvGroup = "base" | "ants" | "totalseg" | "brats";
|
|
2
|
+
/**
|
|
3
|
+
* Bootstrap pipeline:
|
|
4
|
+
* 1. ensure `uv` is on PATH (install if missing)
|
|
5
|
+
* 2. ensure ASKMRI_HOME directories exist
|
|
6
|
+
* 3. ensure a venv with `askmri[full-light]` installed
|
|
7
|
+
*
|
|
8
|
+
* Per-tool-group venvs are created lazily via `ensureGroupVenv()` when
|
|
9
|
+
* installing models that require isolated environments (ants, totalseg, brats).
|
|
10
|
+
*
|
|
11
|
+
* The heavy ML extras stay opt-in via `askmri setup --with ants,total`.
|
|
12
|
+
*/
|
|
13
|
+
interface CommandCheck {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
version?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function commandExists(name: string): Promise<CommandCheck>;
|
|
18
|
+
export declare function ensureUv(): Promise<void>;
|
|
19
|
+
export declare function ensureDirs(): void;
|
|
20
|
+
export interface InstallOptions {
|
|
21
|
+
extras?: string[];
|
|
22
|
+
force?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function ensureVenv(opts?: InstallOptions): Promise<void>;
|
|
25
|
+
export interface GroupVenvOptions {
|
|
26
|
+
/** Force reinstall even if marker exists */
|
|
27
|
+
force?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Ensure a per-tool-group venv exists and has its dependencies installed.
|
|
31
|
+
* Used for isolated environments (ants, totalseg, brats) that have conflicting
|
|
32
|
+
* numpy requirements.
|
|
33
|
+
*
|
|
34
|
+
* - "base" is a no-op (always uses the main venv)
|
|
35
|
+
* - Other groups create ~/.askmri/venvs/{group}/ with Python 3.11
|
|
36
|
+
*
|
|
37
|
+
* Returns the venv path.
|
|
38
|
+
*/
|
|
39
|
+
export declare function ensureGroupVenv(group: EnvGroup, opts?: GroupVenvOptions): Promise<string>;
|
|
40
|
+
/**
|
|
41
|
+
* Check if a group's venv is installed (marker file exists).
|
|
42
|
+
*/
|
|
43
|
+
export declare function isGroupVenvInstalled(group: EnvGroup): boolean;
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=uv.d.ts.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { mkdirSync, existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { x } from "tinyexec";
|
|
3
|
+
import { log } from "../log.js";
|
|
4
|
+
import { paths, venvFor, venvMarkerFor } from "../paths.js";
|
|
5
|
+
const ENV_GROUP_LABELS = {
|
|
6
|
+
base: "Base (builtin)",
|
|
7
|
+
ants: "ANTsPy / ANTsPyNet",
|
|
8
|
+
totalseg: "TotalSegmentator",
|
|
9
|
+
brats: "BraTS Toolkit",
|
|
10
|
+
};
|
|
11
|
+
const ENV_GROUP_INSTALL_CMDS = {
|
|
12
|
+
base: null,
|
|
13
|
+
ants: "pip install antspynet",
|
|
14
|
+
totalseg: "pip install TotalSegmentator",
|
|
15
|
+
brats: "pip install brats-toolkit",
|
|
16
|
+
};
|
|
17
|
+
export async function commandExists(name) {
|
|
18
|
+
try {
|
|
19
|
+
const result = await x(name, ["--version"], { throwOnError: false });
|
|
20
|
+
if (result.exitCode === 0) {
|
|
21
|
+
const firstLine = result.stdout.trim().split("\n")[0];
|
|
22
|
+
return { ok: true, version: firstLine ?? result.stdout.trim() };
|
|
23
|
+
}
|
|
24
|
+
return { ok: false };
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { ok: false };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function ensureUv() {
|
|
31
|
+
const uv = await commandExists("uv");
|
|
32
|
+
if (uv.ok) {
|
|
33
|
+
log.ok(`uv detected (${uv.version})`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
log.warn("uv not found — installing it locally");
|
|
37
|
+
// Use the official installer; it places `uv` at ~/.local/bin/uv (Unix) or %USERPROFILE%\.local\bin (Win).
|
|
38
|
+
if (process.platform === "win32") {
|
|
39
|
+
await x("powershell", [
|
|
40
|
+
"-ExecutionPolicy",
|
|
41
|
+
"ByPass",
|
|
42
|
+
"-c",
|
|
43
|
+
"irm https://astral.sh/uv/install.ps1 | iex",
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
await x("sh", ["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"]);
|
|
48
|
+
}
|
|
49
|
+
const recheck = await commandExists("uv");
|
|
50
|
+
if (!recheck.ok) {
|
|
51
|
+
throw new Error("uv installation finished but `uv` is still not on PATH. Add ~/.local/bin to PATH and re-run.");
|
|
52
|
+
}
|
|
53
|
+
log.ok(`uv installed (${recheck.version})`);
|
|
54
|
+
}
|
|
55
|
+
export function ensureDirs() {
|
|
56
|
+
for (const dir of [paths.home, paths.bin, paths.venvs, paths.cache, paths.data, paths.logs]) {
|
|
57
|
+
if (!existsSync(dir))
|
|
58
|
+
mkdirSync(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function ensureVenv(opts = {}) {
|
|
62
|
+
ensureDirs();
|
|
63
|
+
const venvExists = existsSync(paths.venv);
|
|
64
|
+
if (venvExists && existsSync(paths.marker) && !opts.force) {
|
|
65
|
+
log.ok("Python environment already installed");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!venvExists) {
|
|
69
|
+
log.step(`Creating Python venv at ${paths.venv}`);
|
|
70
|
+
await x("uv", ["venv", "--python", "3.11", paths.venv]);
|
|
71
|
+
}
|
|
72
|
+
const pkg = opts.extras?.length ? `askmri[${opts.extras.join(",")}]` : "askmri";
|
|
73
|
+
log.step(`Installing ${pkg} into the venv (this may take a minute)`);
|
|
74
|
+
await x("uv", ["pip", "install", "--python", paths.venv, pkg], {
|
|
75
|
+
nodeOptions: { stdio: "inherit" },
|
|
76
|
+
});
|
|
77
|
+
// Drop a marker file so subsequent invocations skip the install step.
|
|
78
|
+
writeFileSync(paths.marker, new Date().toISOString());
|
|
79
|
+
log.ok("Python environment ready");
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Ensure a per-tool-group venv exists and has its dependencies installed.
|
|
83
|
+
* Used for isolated environments (ants, totalseg, brats) that have conflicting
|
|
84
|
+
* numpy requirements.
|
|
85
|
+
*
|
|
86
|
+
* - "base" is a no-op (always uses the main venv)
|
|
87
|
+
* - Other groups create ~/.askmri/venvs/{group}/ with Python 3.11
|
|
88
|
+
*
|
|
89
|
+
* Returns the venv path.
|
|
90
|
+
*/
|
|
91
|
+
export async function ensureGroupVenv(group, opts = {}) {
|
|
92
|
+
// "base" group uses the main venv — nothing to do
|
|
93
|
+
if (group === "base") {
|
|
94
|
+
return paths.venv;
|
|
95
|
+
}
|
|
96
|
+
ensureDirs();
|
|
97
|
+
const venvPath = venvFor(group);
|
|
98
|
+
const markerPath = venvMarkerFor(group);
|
|
99
|
+
const venvExists = existsSync(venvPath);
|
|
100
|
+
const markerExists = existsSync(markerPath);
|
|
101
|
+
// If already installed and not forcing, skip
|
|
102
|
+
if (venvExists && markerExists && !opts.force) {
|
|
103
|
+
log.ok(`${ENV_GROUP_LABELS[group]} environment already installed`);
|
|
104
|
+
return venvPath;
|
|
105
|
+
}
|
|
106
|
+
// Create the venv if it doesn't exist
|
|
107
|
+
if (!venvExists) {
|
|
108
|
+
log.step(`Creating isolated venv for ${ENV_GROUP_LABELS[group]} at ${venvPath}`);
|
|
109
|
+
await x("uv", ["venv", "--python", "3.11", venvPath]);
|
|
110
|
+
}
|
|
111
|
+
// Install group dependencies
|
|
112
|
+
const installCmd = ENV_GROUP_INSTALL_CMDS[group];
|
|
113
|
+
if (installCmd) {
|
|
114
|
+
// Extract package spec from "pip install <package>"
|
|
115
|
+
const packageSpec = installCmd.replace(/^pip install\s+/, "").replace(/^['"]|['"]$/g, "");
|
|
116
|
+
log.step(`Installing ${packageSpec} into ${group} venv`);
|
|
117
|
+
const result = await x("uv", ["pip", "install", "--python", venvPath, packageSpec], {
|
|
118
|
+
nodeOptions: { stdio: "inherit" },
|
|
119
|
+
throwOnError: false,
|
|
120
|
+
});
|
|
121
|
+
if (result.exitCode !== 0) {
|
|
122
|
+
throw new Error(`Failed to install ${packageSpec} (exit code ${result.exitCode})`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Drop marker only after successful install
|
|
126
|
+
writeFileSync(markerPath, new Date().toISOString());
|
|
127
|
+
log.ok(`${ENV_GROUP_LABELS[group]} environment ready`);
|
|
128
|
+
return venvPath;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Check if a group's venv is installed (marker file exists).
|
|
132
|
+
*/
|
|
133
|
+
export function isGroupVenvInstalled(group) {
|
|
134
|
+
if (group === "base") {
|
|
135
|
+
return existsSync(paths.marker);
|
|
136
|
+
}
|
|
137
|
+
return existsSync(venvMarkerFor(group));
|
|
138
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { totalmem } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { x } from "tinyexec";
|
|
5
|
+
import { commandExists } from "../bootstrap/uv.js";
|
|
6
|
+
import { log } from "../log.js";
|
|
7
|
+
import { monaiBundlesDir, paths, pythonBin, pythonCliBin } from "../paths.js";
|
|
8
|
+
/**
|
|
9
|
+
* Probe a Python import inside the managed venv. Returns the version
|
|
10
|
+
* string emitted by `<pkg>.__version__` when available, otherwise "ok".
|
|
11
|
+
*/
|
|
12
|
+
async function probePythonImport(pkg, attr = "__version__") {
|
|
13
|
+
if (!existsSync(pythonBin())) {
|
|
14
|
+
return { ok: false, detail: "venv missing" };
|
|
15
|
+
}
|
|
16
|
+
const script = `import ${pkg}; v=getattr(${pkg}, '${attr}', 'ok'); print(v)`;
|
|
17
|
+
try {
|
|
18
|
+
const r = await x(pythonBin(), ["-c", script], { throwOnError: false });
|
|
19
|
+
if (r.exitCode === 0) {
|
|
20
|
+
return { ok: true, detail: (r.stdout ?? "").trim() || "ok" };
|
|
21
|
+
}
|
|
22
|
+
return { ok: false, detail: "not installed" };
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return { ok: false, detail: "probe failed" };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function probeGpu() {
|
|
29
|
+
if (!existsSync(pythonBin())) {
|
|
30
|
+
return { ok: false, detail: "venv missing" };
|
|
31
|
+
}
|
|
32
|
+
const script = "import torch; "
|
|
33
|
+
+ "cuda=torch.cuda.is_available(); "
|
|
34
|
+
+ "mps=getattr(torch.backends,'mps',None) and torch.backends.mps.is_available(); "
|
|
35
|
+
+ "print('cuda' if cuda else ('mps' if mps else 'cpu'))";
|
|
36
|
+
try {
|
|
37
|
+
const r = await x(pythonBin(), ["-c", script], { throwOnError: false });
|
|
38
|
+
if (r.exitCode !== 0)
|
|
39
|
+
return { ok: false, detail: "torch not installed" };
|
|
40
|
+
const dev = (r.stdout ?? "").trim();
|
|
41
|
+
return { ok: dev !== "cpu", detail: dev || "cpu" };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { ok: false, detail: "probe failed" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function bundleCacheSummary() {
|
|
48
|
+
if (!existsSync(monaiBundlesDir))
|
|
49
|
+
return "empty";
|
|
50
|
+
try {
|
|
51
|
+
const entries = readdirSync(monaiBundlesDir).filter((e) => statSync(join(monaiBundlesDir, e)).isDirectory());
|
|
52
|
+
return entries.length === 0
|
|
53
|
+
? "empty"
|
|
54
|
+
: `${entries.length} bundle(s): ${entries.slice(0, 3).join(", ")}${entries.length > 3 ? "…" : ""}`;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return "unreadable";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function checks() {
|
|
61
|
+
const results = [];
|
|
62
|
+
// ---------- Core ----------
|
|
63
|
+
const node = process.versions.node;
|
|
64
|
+
const nodeMajor = node.split(".")[0];
|
|
65
|
+
results.push({
|
|
66
|
+
name: "Node.js",
|
|
67
|
+
required: true,
|
|
68
|
+
ok: nodeMajor !== undefined && parseInt(nodeMajor, 10) >= 18,
|
|
69
|
+
detail: `v${node}`,
|
|
70
|
+
fix: "Upgrade to Node 18.18+ from https://nodejs.org",
|
|
71
|
+
});
|
|
72
|
+
const uv = await commandExists("uv");
|
|
73
|
+
results.push({
|
|
74
|
+
name: "uv (Python installer)",
|
|
75
|
+
required: true,
|
|
76
|
+
ok: uv.ok,
|
|
77
|
+
detail: uv.version ?? "not found",
|
|
78
|
+
fix: "Run `askmri setup` to install uv automatically.",
|
|
79
|
+
});
|
|
80
|
+
const venvOk = existsSync(pythonCliBin());
|
|
81
|
+
results.push({
|
|
82
|
+
name: "Python venv + askmri-server",
|
|
83
|
+
required: true,
|
|
84
|
+
ok: venvOk,
|
|
85
|
+
detail: venvOk ? paths.venv : "not installed",
|
|
86
|
+
fix: "Run `askmri setup`",
|
|
87
|
+
});
|
|
88
|
+
// ---------- Optional binaries on PATH ----------
|
|
89
|
+
const dcm2niix = await commandExists("dcm2niix");
|
|
90
|
+
results.push({
|
|
91
|
+
name: "dcm2niix (DICOM → NIfTI)",
|
|
92
|
+
required: false,
|
|
93
|
+
ok: dcm2niix.ok,
|
|
94
|
+
detail: dcm2niix.version ?? "not found",
|
|
95
|
+
fix: "macOS: `brew install dcm2niix` · Ubuntu: `apt install dcm2niix`",
|
|
96
|
+
});
|
|
97
|
+
const synthstrip = await commandExists("mri_synthstrip");
|
|
98
|
+
results.push({
|
|
99
|
+
name: "mri_synthstrip (alt skull-strip)",
|
|
100
|
+
required: false,
|
|
101
|
+
ok: synthstrip.ok,
|
|
102
|
+
detail: synthstrip.version ?? "not found",
|
|
103
|
+
fix: "FreeSurfer 7.3+ or https://surfer.nmr.mgh.harvard.edu/docs/synthstrip",
|
|
104
|
+
});
|
|
105
|
+
const npx = await commandExists("npx");
|
|
106
|
+
results.push({
|
|
107
|
+
name: "npx (for bids-validator)",
|
|
108
|
+
required: false,
|
|
109
|
+
ok: npx.ok,
|
|
110
|
+
detail: npx.version ?? "not found",
|
|
111
|
+
fix: "Bundled with Node.js 18+",
|
|
112
|
+
});
|
|
113
|
+
// ---------- Optional Python packages ----------
|
|
114
|
+
if (venvOk) {
|
|
115
|
+
const optional = [
|
|
116
|
+
["pyradiomics", "radiomics", "pip install 'askmri-core[radiomics]'"],
|
|
117
|
+
["highdicom", "highdicom", "pip install 'askmri-core[interop]'"],
|
|
118
|
+
["nilearn", "nilearn", "pip install 'askmri-core[reports]'"],
|
|
119
|
+
["monai", "monai", "pip install 'askmri-core[monai]'"],
|
|
120
|
+
["dicomanonymizer", "dicomanonymizer", "pip install 'askmri-core[anonymize]'"],
|
|
121
|
+
["torch", "torch", "pip install 'askmri-core[torch]'"],
|
|
122
|
+
["antspynet", "antspynet", "pip install 'askmri-core[ants]'"],
|
|
123
|
+
["HD-BET", "HD_BET", "pip install 'askmri-core[hdbet]'"],
|
|
124
|
+
["TotalSegmentator", "totalsegmentator", "pip install TotalSegmentator"],
|
|
125
|
+
];
|
|
126
|
+
for (const [label, importName, fix] of optional) {
|
|
127
|
+
const r = await probePythonImport(importName);
|
|
128
|
+
results.push({
|
|
129
|
+
name: label,
|
|
130
|
+
required: false,
|
|
131
|
+
ok: r.ok,
|
|
132
|
+
detail: r.detail,
|
|
133
|
+
fix,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ---------- GPU ----------
|
|
138
|
+
if (venvOk) {
|
|
139
|
+
const gpu = await probeGpu();
|
|
140
|
+
results.push({
|
|
141
|
+
name: "Accelerator",
|
|
142
|
+
required: false,
|
|
143
|
+
ok: gpu.ok,
|
|
144
|
+
detail: gpu.detail,
|
|
145
|
+
fix: "Install torch with CUDA / MPS support for GPU acceleration.",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// ---------- MONAI bundle cache ----------
|
|
149
|
+
results.push({
|
|
150
|
+
name: "MONAI bundle cache",
|
|
151
|
+
required: false,
|
|
152
|
+
ok: existsSync(monaiBundlesDir),
|
|
153
|
+
detail: bundleCacheSummary(),
|
|
154
|
+
fix: "First `askmri-server monai-bundle download <name>` will create it.",
|
|
155
|
+
});
|
|
156
|
+
// ---------- Host resources ----------
|
|
157
|
+
const ram = totalmem() / 1024 / 1024 / 1024;
|
|
158
|
+
results.push({
|
|
159
|
+
name: "System RAM",
|
|
160
|
+
required: false,
|
|
161
|
+
ok: ram >= 8,
|
|
162
|
+
detail: `${ram.toFixed(1)} GiB`,
|
|
163
|
+
fix: "8 GiB minimum recommended; 16 GiB comfortable for TotalSegmentator.",
|
|
164
|
+
});
|
|
165
|
+
const dataDirExists = existsSync(paths.data);
|
|
166
|
+
results.push({
|
|
167
|
+
name: "Data directory",
|
|
168
|
+
required: false,
|
|
169
|
+
ok: dataDirExists,
|
|
170
|
+
detail: dataDirExists ? `${paths.data} (${dirSize(paths.data)})` : "missing",
|
|
171
|
+
fix: "`askmri setup` will create it.",
|
|
172
|
+
});
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
175
|
+
function dirSize(_dir) {
|
|
176
|
+
try {
|
|
177
|
+
const s = statSync(_dir);
|
|
178
|
+
return s.isDirectory() ? "ready" : "not a directory";
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return "unknown";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export async function doctor() {
|
|
185
|
+
log.header("AskMRI compatibility check");
|
|
186
|
+
const all = await checks();
|
|
187
|
+
let hasFailure = false;
|
|
188
|
+
for (const c of all) {
|
|
189
|
+
const label = `${c.name.padEnd(34)} ${c.detail}`;
|
|
190
|
+
if (c.ok) {
|
|
191
|
+
log.ok(label);
|
|
192
|
+
}
|
|
193
|
+
else if (c.required) {
|
|
194
|
+
log.error(label);
|
|
195
|
+
if (c.fix)
|
|
196
|
+
log.step(`fix: ${c.fix}`);
|
|
197
|
+
hasFailure = true;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
log.warn(label);
|
|
201
|
+
if (c.fix)
|
|
202
|
+
log.step(`fix: ${c.fix}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
console.error();
|
|
206
|
+
if (hasFailure) {
|
|
207
|
+
log.error("Some required dependencies are missing. Run `askmri setup`.");
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
log.ok("Ready for MRI research.");
|
|
211
|
+
log.step("Install optional extras: `askmri setup --with radiomics,reports,interop,monai,anonymize,full`");
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forward every argument unchanged to the bundled `askmri-server` Python
|
|
3
|
+
* binary. If the venv is missing, install it first.
|
|
4
|
+
*
|
|
5
|
+
* Examples:
|
|
6
|
+
* askmri info ./dicom → askmri-server info ./dicom
|
|
7
|
+
* askmri run ./dicom -m totalseg → askmri-server run ./dicom -m totalseg
|
|
8
|
+
*/
|
|
9
|
+
export declare function passthrough(args: string[]): Promise<number>;
|
|
10
|
+
//# sourceMappingURL=passthrough.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { x } from "tinyexec";
|
|
3
|
+
import { ensureUv, ensureVenv } from "../bootstrap/uv.js";
|
|
4
|
+
import { log } from "../log.js";
|
|
5
|
+
import { pythonCliBin } from "../paths.js";
|
|
6
|
+
/**
|
|
7
|
+
* Forward every argument unchanged to the bundled `askmri-server` Python
|
|
8
|
+
* binary. If the venv is missing, install it first.
|
|
9
|
+
*
|
|
10
|
+
* Examples:
|
|
11
|
+
* askmri info ./dicom → askmri-server info ./dicom
|
|
12
|
+
* askmri run ./dicom -m totalseg → askmri-server run ./dicom -m totalseg
|
|
13
|
+
*/
|
|
14
|
+
export async function passthrough(args) {
|
|
15
|
+
if (!existsSync(pythonCliBin())) {
|
|
16
|
+
log.info("First run — installing the Python pipeline…");
|
|
17
|
+
await ensureUv();
|
|
18
|
+
await ensureVenv();
|
|
19
|
+
}
|
|
20
|
+
const result = await x(pythonCliBin(), args, {
|
|
21
|
+
nodeOptions: { stdio: "inherit" },
|
|
22
|
+
throwOnError: false,
|
|
23
|
+
});
|
|
24
|
+
return result.exitCode ?? 1;
|
|
25
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type InstallOptions } from "../bootstrap/uv.js";
|
|
2
|
+
export interface SetupOptions extends InstallOptions {
|
|
3
|
+
}
|
|
4
|
+
export declare function setup(opts?: SetupOptions): Promise<number>;
|
|
5
|
+
export declare function parseSetupArgs(argv: string[]): SetupOptions;
|
|
6
|
+
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ensureUv, ensureVenv } from "../bootstrap/uv.js";
|
|
2
|
+
import { log } from "../log.js";
|
|
3
|
+
import { paths } from "../paths.js";
|
|
4
|
+
export async function setup(opts = {}) {
|
|
5
|
+
log.header("Setup");
|
|
6
|
+
log.info(`Home: ${paths.home}`);
|
|
7
|
+
if (opts.extras?.length)
|
|
8
|
+
log.info(`Extras: ${opts.extras.join(", ")}`);
|
|
9
|
+
if (opts.force)
|
|
10
|
+
log.info("Force reinstall: yes");
|
|
11
|
+
try {
|
|
12
|
+
await ensureUv();
|
|
13
|
+
await ensureVenv(opts);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
17
|
+
log.step("If this is a network issue, retry. Otherwise report at https://github.com/iplanwebsites/askmri/issues");
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
console.error();
|
|
21
|
+
log.ok("Setup complete.");
|
|
22
|
+
log.step("Next: `askmri doctor` to verify, or `askmri serve` to launch the viewer.");
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
export function parseSetupArgs(argv) {
|
|
26
|
+
const opts = {};
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const a = argv[i];
|
|
29
|
+
if (a === "--with" || a === "-w") {
|
|
30
|
+
const next = argv[++i];
|
|
31
|
+
if (!next)
|
|
32
|
+
continue;
|
|
33
|
+
opts.extras = next.split(",").map((s) => s.trim()).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
else if (a === "--force" || a === "-f") {
|
|
36
|
+
opts.force = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return opts;
|
|
40
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { x } from "tinyexec";
|
|
3
|
+
import kleur from "kleur";
|
|
4
|
+
import { VERSION } from "../version.js";
|
|
5
|
+
import { pythonCliBin } from "../paths.js";
|
|
6
|
+
import { log } from "../log.js";
|
|
7
|
+
async function readPythonVersions() {
|
|
8
|
+
if (!existsSync(pythonCliBin()))
|
|
9
|
+
return null;
|
|
10
|
+
const res = await x(pythonCliBin(), ["version-json"], { throwOnError: false });
|
|
11
|
+
if (res.exitCode !== 0)
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(res.stdout);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function versionCommand(args) {
|
|
21
|
+
const check = args.includes("--check");
|
|
22
|
+
const json = args.includes("--json");
|
|
23
|
+
const py = await readPythonVersions();
|
|
24
|
+
if (json) {
|
|
25
|
+
console.log(JSON.stringify({
|
|
26
|
+
launcher: VERSION,
|
|
27
|
+
python: py,
|
|
28
|
+
}, null, 2));
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
if (!check) {
|
|
32
|
+
console.log(VERSION);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
// --check mode: print a comparison table and fail on drift.
|
|
36
|
+
console.error(kleur.bold("Version alignment\n"));
|
|
37
|
+
const row = (label, value, expected) => {
|
|
38
|
+
const v = value ?? kleur.dim("(not installed)");
|
|
39
|
+
const ok = !expected || value === expected;
|
|
40
|
+
const mark = ok ? kleur.green("✓") : kleur.red("✗");
|
|
41
|
+
console.error(` ${mark} ${label.padEnd(20)} ${v}`);
|
|
42
|
+
return ok;
|
|
43
|
+
};
|
|
44
|
+
let allAligned = row("launcher (npm)", VERSION);
|
|
45
|
+
if (!py) {
|
|
46
|
+
console.error(kleur.yellow("\n Python pipeline is not installed yet."));
|
|
47
|
+
console.error(kleur.dim(" Run `askmri setup` to install it."));
|
|
48
|
+
return check ? 1 : 0;
|
|
49
|
+
}
|
|
50
|
+
allAligned = row("askmri-cli", py["askmri-cli"], VERSION) && allAligned;
|
|
51
|
+
allAligned = row("askmri-core", py["askmri-core"], VERSION) && allAligned;
|
|
52
|
+
allAligned = row("askmri-api", py["askmri-api"], VERSION) && allAligned;
|
|
53
|
+
allAligned = row("askmri (meta)", py.askmri, VERSION) && allAligned;
|
|
54
|
+
console.error();
|
|
55
|
+
if (!allAligned) {
|
|
56
|
+
log.error("Version drift between the npm launcher and the installed Python pipeline.");
|
|
57
|
+
log.step("Run `askmri setup --force` to reinstall and align.");
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
log.ok("All versions aligned.");
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import kleur from "kleur";
|
|
3
|
+
import { doctor } from "./commands/doctor.js";
|
|
4
|
+
import { setup, parseSetupArgs } from "./commands/setup.js";
|
|
5
|
+
import { passthrough } from "./commands/passthrough.js";
|
|
6
|
+
import { versionCommand } from "./commands/version.js";
|
|
7
|
+
import { paths } from "./paths.js";
|
|
8
|
+
import { VERSION } from "./version.js";
|
|
9
|
+
import { log, initFileLogging } from "./log.js";
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// MINIMAL_MODE: Toggle full CLI features for npm publish.
|
|
12
|
+
// To enable full mode: set MINIMAL_MODE = false and uncomment the loaders.
|
|
13
|
+
// ============================================================================
|
|
14
|
+
const MINIMAL_MODE = true;
|
|
15
|
+
// const MINIMAL_MODE = false
|
|
16
|
+
// const loadServe = () => import("./commands/serve.js")
|
|
17
|
+
// const loadOpen = () => import("./commands/open.js")
|
|
18
|
+
// const loadConfig = () => import("./commands/config.js")
|
|
19
|
+
// const loadDatasets = () => import("./commands/datasets.js")
|
|
20
|
+
const HELP_MINIMAL = `${kleur.bold("askmri")} v${VERSION} — MRI research toolkit
|
|
21
|
+
|
|
22
|
+
${kleur.dim("Usage:")} askmri <command> [options]
|
|
23
|
+
|
|
24
|
+
${kleur.bold("Commands:")}
|
|
25
|
+
${kleur.cyan("doctor")} Check Python, uv, environment
|
|
26
|
+
${kleur.cyan("setup")} [--with X] Install the Python pipeline
|
|
27
|
+
${kleur.cyan("version")} [--check] Print version
|
|
28
|
+
${kleur.cyan("home")} Print the AskMRI home directory
|
|
29
|
+
${kleur.cyan("help")} Show this message
|
|
30
|
+
|
|
31
|
+
${kleur.bold("Examples:")}
|
|
32
|
+
${kleur.dim("$")} npx askmri doctor
|
|
33
|
+
${kleur.dim("$")} npx askmri setup
|
|
34
|
+
|
|
35
|
+
${kleur.bold("Environment:")}
|
|
36
|
+
ASKMRI_HOME Override the install directory (default: ~/.askmri)
|
|
37
|
+
|
|
38
|
+
More at https://askmri.com
|
|
39
|
+
`;
|
|
40
|
+
const HELP_FULL = `${kleur.bold("askmri")} v${VERSION} — MRI research toolkit (research preview, not for clinical use)
|
|
41
|
+
|
|
42
|
+
${kleur.dim("Usage:")} askmri <command> [options]
|
|
43
|
+
|
|
44
|
+
${kleur.bold("Launcher commands:")}
|
|
45
|
+
${kleur.cyan("open")} Open the viewer in a desktop app window (chromeless)
|
|
46
|
+
${kleur.cyan("serve")} [--port P] Start the web viewer + API (headless, for dev/scripting)
|
|
47
|
+
${kleur.cyan("datasets")} <cmd> Browse, download, and process public datasets
|
|
48
|
+
${kleur.cyan("doctor")} Check uv, venv, optional Python deps, GPU, MONAI cache
|
|
49
|
+
${kleur.cyan("setup")} [--with X] Install the Python pipeline (extras: radiomics,reports,interop,monai,anonymize,torch,ants,hdbet,full)
|
|
50
|
+
${kleur.cyan("config")} <cmd> Manage API keys (set/get/list/delete/sync)
|
|
51
|
+
${kleur.cyan("version")} [--check] Print launcher version; --check compares against the Python pipeline
|
|
52
|
+
${kleur.cyan("home")} Print the AskMRI home directory
|
|
53
|
+
${kleur.cyan("help")} Show this message
|
|
54
|
+
|
|
55
|
+
${kleur.bold("Passthrough to the Python pipeline (askmri-server):")}
|
|
56
|
+
${kleur.cyan("info")} <folder> DICOM metadata
|
|
57
|
+
${kleur.cyan("run")} <folder> -m <method> Full pipeline
|
|
58
|
+
${kleur.cyan("preprocess")} <folder> DICOM → NIfTI (auto-detects body region)
|
|
59
|
+
${kleur.cyan("analyze")} <file> -m <method> Run a single method
|
|
60
|
+
${kleur.cyan("stats")} <file> Volume statistics
|
|
61
|
+
${kleur.cyan("analyze-all")} <file> Run multiple methods on one input
|
|
62
|
+
${kleur.cyan("radiomics")} <image> <mask> PyRadiomics features per label
|
|
63
|
+
${kleur.cyan("export-dicom-seg")} <mask> <reference> Mask → DICOM-SEG via highdicom
|
|
64
|
+
${kleur.cyan("anonymize")} <path> PHI scrubbing on DICOM file/dir
|
|
65
|
+
${kleur.cyan("html-report")} <scan_dir> Printable HTML report (nilearn)
|
|
66
|
+
${kleur.cyan("monai-bundle")} {list|info|download|run} MONAI model-zoo loader
|
|
67
|
+
${kleur.cyan("validate-bids")} <path> Validate a BIDS dataset (npx)
|
|
68
|
+
|
|
69
|
+
${kleur.bold("Examples:")}
|
|
70
|
+
${kleur.dim("$")} npx askmri open # Launch viewer in app window
|
|
71
|
+
${kleur.dim("$")} npx askmri serve --port 5173 # Headless server for dev
|
|
72
|
+
${kleur.dim("$")} npx askmri doctor
|
|
73
|
+
${kleur.dim("$")} npx askmri setup
|
|
74
|
+
${kleur.dim("$")} npx askmri run ./dicom -m totalseg
|
|
75
|
+
|
|
76
|
+
${kleur.bold("Environment:")}
|
|
77
|
+
ASKMRI_HOME Override the install directory (default: ~/.askmri)
|
|
78
|
+
ASKMRI_QUIET Set to "1" to suppress non-error console output
|
|
79
|
+
ASKMRI_NO_LOG Set to "1" to disable file logging
|
|
80
|
+
`;
|
|
81
|
+
const HELP = MINIMAL_MODE ? HELP_MINIMAL : HELP_FULL;
|
|
82
|
+
// Commands that should NOT auto-print the launcher banner (they need clean
|
|
83
|
+
// stdout for piping, or print their own banner).
|
|
84
|
+
const QUIET_COMMANDS = new Set([
|
|
85
|
+
"version",
|
|
86
|
+
"home",
|
|
87
|
+
"help",
|
|
88
|
+
"-h",
|
|
89
|
+
"--help",
|
|
90
|
+
"-v",
|
|
91
|
+
"--version",
|
|
92
|
+
]);
|
|
93
|
+
async function main(argv) {
|
|
94
|
+
const [cmd, ...rest] = argv;
|
|
95
|
+
if (cmd && !QUIET_COMMANDS.has(cmd)) {
|
|
96
|
+
initFileLogging();
|
|
97
|
+
log.banner(VERSION, paths.home);
|
|
98
|
+
}
|
|
99
|
+
switch (cmd) {
|
|
100
|
+
case undefined:
|
|
101
|
+
case "-h":
|
|
102
|
+
case "--help":
|
|
103
|
+
case "help":
|
|
104
|
+
console.error(HELP);
|
|
105
|
+
return 0;
|
|
106
|
+
case "-v":
|
|
107
|
+
case "--version":
|
|
108
|
+
case "version":
|
|
109
|
+
return versionCommand(rest);
|
|
110
|
+
case "home":
|
|
111
|
+
console.log(paths.home);
|
|
112
|
+
return 0;
|
|
113
|
+
case "doctor":
|
|
114
|
+
return doctor();
|
|
115
|
+
case "setup":
|
|
116
|
+
if (rest.length === 0 || rest.every((a) => a.startsWith("-"))) {
|
|
117
|
+
return setup(parseSetupArgs(rest));
|
|
118
|
+
}
|
|
119
|
+
if (MINIMAL_MODE) {
|
|
120
|
+
console.error(`Unknown command: ${cmd}`);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
return passthrough([cmd, ...rest]);
|
|
124
|
+
// [FULL] case "serve": {
|
|
125
|
+
// [FULL] const { serve, parseServeArgs } = await loadServe()
|
|
126
|
+
// [FULL] return serve(parseServeArgs(rest))
|
|
127
|
+
// [FULL] }
|
|
128
|
+
// [FULL] case "open": {
|
|
129
|
+
// [FULL] const { open, parseOpenArgs } = await loadOpen()
|
|
130
|
+
// [FULL] return open(parseOpenArgs(rest))
|
|
131
|
+
// [FULL] }
|
|
132
|
+
// [FULL] case "config": {
|
|
133
|
+
// [FULL] const { config } = await loadConfig()
|
|
134
|
+
// [FULL] return config(rest)
|
|
135
|
+
// [FULL] }
|
|
136
|
+
// [FULL] case "datasets": {
|
|
137
|
+
// [FULL] const { datasets } = await loadDatasets()
|
|
138
|
+
// [FULL] return datasets(rest)
|
|
139
|
+
// [FULL] }
|
|
140
|
+
// [FULL] default:
|
|
141
|
+
// [FULL] return passthrough([cmd, ...rest])
|
|
142
|
+
default:
|
|
143
|
+
console.error(`Unknown command: ${cmd}`);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
main(process.argv.slice(2))
|
|
148
|
+
.then((code) => process.exit(code))
|
|
149
|
+
.catch((err) => {
|
|
150
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function initFileLogging(): string | null;
|
|
2
|
+
export declare const log: {
|
|
3
|
+
info(msg: string): void;
|
|
4
|
+
ok(msg: string): void;
|
|
5
|
+
warn(msg: string): void;
|
|
6
|
+
error(msg: string): void;
|
|
7
|
+
step(msg: string): void;
|
|
8
|
+
header(msg: string): void;
|
|
9
|
+
/** Print the launcher's identity banner on every non-quiet invocation. */
|
|
10
|
+
banner(version: string, home: string): void;
|
|
11
|
+
/** Used by the Python subprocess proxy in serve.ts. */
|
|
12
|
+
python(line: string): void;
|
|
13
|
+
/** Filesystem path of the current log file, if file logging is active. */
|
|
14
|
+
readonly file: string | null;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=log.d.ts.map
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import kleur from "kleur";
|
|
2
|
+
import { existsSync, mkdirSync, appendFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { platform, arch, totalmem, release } from "node:os";
|
|
5
|
+
import { paths } from "./paths.js";
|
|
6
|
+
const isQuiet = process.env.ASKMRI_QUIET === "1";
|
|
7
|
+
const fileLoggingDisabled = process.env.ASKMRI_NO_LOG === "1";
|
|
8
|
+
let logFile = null;
|
|
9
|
+
export function initFileLogging() {
|
|
10
|
+
if (fileLoggingDisabled)
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
if (!existsSync(paths.logs))
|
|
14
|
+
mkdirSync(paths.logs, { recursive: true });
|
|
15
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
16
|
+
logFile = join(paths.logs, `launcher-${today}.log`);
|
|
17
|
+
appendFileSync(logFile, `\n${new Date().toISOString()} ──── new invocation ${process.argv.slice(2).join(" ")}\n`);
|
|
18
|
+
return logFile;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Disk full / permission denied — fall back to console-only.
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function writeToFile(level, msg) {
|
|
26
|
+
if (!logFile)
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
appendFileSync(logFile, `${new Date().toISOString()} ${level.padEnd(5)} ${msg}\n`);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// ignore — best effort
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const log = {
|
|
36
|
+
info(msg) {
|
|
37
|
+
if (!isQuiet)
|
|
38
|
+
console.error(kleur.cyan("·"), msg);
|
|
39
|
+
writeToFile("info", msg);
|
|
40
|
+
},
|
|
41
|
+
ok(msg) {
|
|
42
|
+
if (!isQuiet)
|
|
43
|
+
console.error(kleur.green("✓"), msg);
|
|
44
|
+
writeToFile("ok", msg);
|
|
45
|
+
},
|
|
46
|
+
warn(msg) {
|
|
47
|
+
console.error(kleur.yellow("!"), msg);
|
|
48
|
+
writeToFile("warn", msg);
|
|
49
|
+
},
|
|
50
|
+
error(msg) {
|
|
51
|
+
console.error(kleur.red("✗"), msg);
|
|
52
|
+
writeToFile("error", msg);
|
|
53
|
+
},
|
|
54
|
+
step(msg) {
|
|
55
|
+
if (!isQuiet)
|
|
56
|
+
console.error(kleur.dim("→"), kleur.dim(msg));
|
|
57
|
+
writeToFile("step", msg);
|
|
58
|
+
},
|
|
59
|
+
header(msg) {
|
|
60
|
+
if (!isQuiet)
|
|
61
|
+
console.error("\n" + kleur.bold(msg));
|
|
62
|
+
writeToFile("info", msg);
|
|
63
|
+
},
|
|
64
|
+
/** Print the launcher's identity banner on every non-quiet invocation. */
|
|
65
|
+
banner(version, home) {
|
|
66
|
+
const platformInfo = `${platform()} ${arch()} · node ${process.versions.node} · ${(totalmem() / 1024 / 1024 / 1024).toFixed(1)} GiB`;
|
|
67
|
+
const banner = `askmri v${version} ${kleur.dim(`(${platformInfo})`)}`;
|
|
68
|
+
if (!isQuiet) {
|
|
69
|
+
console.error(kleur.bold().magenta(banner));
|
|
70
|
+
console.error(kleur.dim(`home: ${home}` + (logFile ? ` · log: ${logFile}` : "")));
|
|
71
|
+
}
|
|
72
|
+
writeToFile("info", `banner: askmri v${version} ${platformInfo}`);
|
|
73
|
+
writeToFile("info", `home: ${home}, kernel: ${release()}`);
|
|
74
|
+
},
|
|
75
|
+
/** Used by the Python subprocess proxy in serve.ts. */
|
|
76
|
+
python(line) {
|
|
77
|
+
if (!isQuiet)
|
|
78
|
+
process.stderr.write(kleur.dim("[py] ") + line);
|
|
79
|
+
writeToFile("py", line.trimEnd());
|
|
80
|
+
},
|
|
81
|
+
/** Filesystem path of the current log file, if file logging is active. */
|
|
82
|
+
get file() {
|
|
83
|
+
return logFile;
|
|
84
|
+
},
|
|
85
|
+
};
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EnvGroup } from "@askmri/content";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the AskMRI home directory. Honours $ASKMRI_HOME, then falls back
|
|
4
|
+
* to ~/.askmri. The directory holds:
|
|
5
|
+
*
|
|
6
|
+
* bin/ — installed askmri-server binary (uv-managed)
|
|
7
|
+
* venv/ — Python virtual environment (base/server)
|
|
8
|
+
* venvs/ — per-tool-group isolated venvs (ants, totalseg, brats)
|
|
9
|
+
* cache/ — preprocessing + model weight cache
|
|
10
|
+
* data/ — default scan and output storage
|
|
11
|
+
* logs/ — launcher + server logs
|
|
12
|
+
*/
|
|
13
|
+
export declare function askmriHome(): string;
|
|
14
|
+
export declare const paths: {
|
|
15
|
+
home: string;
|
|
16
|
+
bin: string;
|
|
17
|
+
venv: string;
|
|
18
|
+
venvs: string;
|
|
19
|
+
cache: string;
|
|
20
|
+
data: string;
|
|
21
|
+
logs: string;
|
|
22
|
+
marker: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Get the venv path for a specific environment group.
|
|
26
|
+
* - "base" uses the main venv at ~/.askmri/venv/ (where the server runs)
|
|
27
|
+
* - Other groups use isolated venvs at ~/.askmri/venvs/{group}/
|
|
28
|
+
*/
|
|
29
|
+
export declare function venvFor(group: EnvGroup): string;
|
|
30
|
+
/**
|
|
31
|
+
* Get the Python binary path for a specific environment group's venv.
|
|
32
|
+
*/
|
|
33
|
+
export declare function pythonBinFor(group: EnvGroup): string;
|
|
34
|
+
/**
|
|
35
|
+
* Get the pip binary path for a specific environment group's venv.
|
|
36
|
+
*/
|
|
37
|
+
export declare function pipBinFor(group: EnvGroup): string;
|
|
38
|
+
/**
|
|
39
|
+
* Marker file path indicating a group's venv is installed.
|
|
40
|
+
*/
|
|
41
|
+
export declare function venvMarkerFor(group: EnvGroup): string;
|
|
42
|
+
/** Path to the `askmri-server` binary inside the managed venv. */
|
|
43
|
+
export declare function pythonCliBin(): string;
|
|
44
|
+
/** Path to the `python` binary inside the managed venv (base). */
|
|
45
|
+
export declare function pythonBin(): string;
|
|
46
|
+
/** Cache directory for MONAI bundles (shared with the Python side). */
|
|
47
|
+
export declare const monaiBundlesDir: string;
|
|
48
|
+
//# sourceMappingURL=paths.d.ts.map
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { homedir, platform } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { env } from "node:process";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the AskMRI home directory. Honours $ASKMRI_HOME, then falls back
|
|
6
|
+
* to ~/.askmri. The directory holds:
|
|
7
|
+
*
|
|
8
|
+
* bin/ — installed askmri-server binary (uv-managed)
|
|
9
|
+
* venv/ — Python virtual environment (base/server)
|
|
10
|
+
* venvs/ — per-tool-group isolated venvs (ants, totalseg, brats)
|
|
11
|
+
* cache/ — preprocessing + model weight cache
|
|
12
|
+
* data/ — default scan and output storage
|
|
13
|
+
* logs/ — launcher + server logs
|
|
14
|
+
*/
|
|
15
|
+
export function askmriHome() {
|
|
16
|
+
if (env.ASKMRI_HOME)
|
|
17
|
+
return resolve(env.ASKMRI_HOME);
|
|
18
|
+
return join(homedir(), ".askmri");
|
|
19
|
+
}
|
|
20
|
+
export const paths = {
|
|
21
|
+
home: askmriHome(),
|
|
22
|
+
bin: join(askmriHome(), "bin"),
|
|
23
|
+
venv: join(askmriHome(), "venv"),
|
|
24
|
+
venvs: join(askmriHome(), "venvs"),
|
|
25
|
+
cache: join(askmriHome(), "cache"),
|
|
26
|
+
data: join(askmriHome(), "data"),
|
|
27
|
+
logs: join(askmriHome(), "logs"),
|
|
28
|
+
marker: join(askmriHome(), "INSTALLED"),
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Get the venv path for a specific environment group.
|
|
32
|
+
* - "base" uses the main venv at ~/.askmri/venv/ (where the server runs)
|
|
33
|
+
* - Other groups use isolated venvs at ~/.askmri/venvs/{group}/
|
|
34
|
+
*/
|
|
35
|
+
export function venvFor(group) {
|
|
36
|
+
if (group === "base")
|
|
37
|
+
return paths.venv;
|
|
38
|
+
return join(paths.venvs, group);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get the Python binary path for a specific environment group's venv.
|
|
42
|
+
*/
|
|
43
|
+
export function pythonBinFor(group) {
|
|
44
|
+
const venv = venvFor(group);
|
|
45
|
+
const ext = platform() === "win32" ? ".exe" : "";
|
|
46
|
+
const dir = platform() === "win32" ? "Scripts" : "bin";
|
|
47
|
+
return join(venv, dir, `python${ext}`);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get the pip binary path for a specific environment group's venv.
|
|
51
|
+
*/
|
|
52
|
+
export function pipBinFor(group) {
|
|
53
|
+
const venv = venvFor(group);
|
|
54
|
+
const ext = platform() === "win32" ? ".exe" : "";
|
|
55
|
+
const dir = platform() === "win32" ? "Scripts" : "bin";
|
|
56
|
+
// uv creates pip3, not pip
|
|
57
|
+
return join(venv, dir, `pip3${ext}`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Marker file path indicating a group's venv is installed.
|
|
61
|
+
*/
|
|
62
|
+
export function venvMarkerFor(group) {
|
|
63
|
+
if (group === "base")
|
|
64
|
+
return paths.marker;
|
|
65
|
+
return join(paths.venvs, group, "INSTALLED");
|
|
66
|
+
}
|
|
67
|
+
/** Path to the `askmri-server` binary inside the managed venv. */
|
|
68
|
+
export function pythonCliBin() {
|
|
69
|
+
const ext = platform() === "win32" ? ".exe" : "";
|
|
70
|
+
const dir = platform() === "win32" ? "Scripts" : "bin";
|
|
71
|
+
return join(paths.venv, dir, `askmri-server${ext}`);
|
|
72
|
+
}
|
|
73
|
+
/** Path to the `python` binary inside the managed venv (base). */
|
|
74
|
+
export function pythonBin() {
|
|
75
|
+
return pythonBinFor("base");
|
|
76
|
+
}
|
|
77
|
+
/** Cache directory for MONAI bundles (shared with the Python side). */
|
|
78
|
+
export const monaiBundlesDir = join(askmriHome(), "monai-bundles");
|
package/dist/version.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "askmri",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "The open AI workbench for MRI research.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mri",
|
|
7
|
+
"nifti",
|
|
8
|
+
"research",
|
|
9
|
+
"viewer"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://askmri.com",
|
|
12
|
+
"bugs": "https://github.com/askmri/askmri/issues",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/askmri/askmri.git"
|
|
16
|
+
},
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"askmri": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"!dist/__tests__",
|
|
25
|
+
"!dist/**/*.test.*",
|
|
26
|
+
"!dist/**/*.e2e.test.*",
|
|
27
|
+
"!dist/**/*.map",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.18"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.minimal.json && node scripts/post-build.mjs",
|
|
35
|
+
"build:full": "tsc -p tsconfig.json && node scripts/post-build.mjs",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
37
|
+
"dev": "tsx src/index.ts",
|
|
38
|
+
"clean": "rm -rf dist",
|
|
39
|
+
"test:chat": "tsx src/trpc/procedures/chat.e2e.test.ts"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@ai-sdk/anthropic": "^1",
|
|
43
|
+
"@ai-sdk/openai": "^1",
|
|
44
|
+
"@askmri/agent": "workspace:*",
|
|
45
|
+
"@askmri/api": "workspace:*",
|
|
46
|
+
"@askmri/content": "workspace:*",
|
|
47
|
+
"@askmri/db": "workspace:*",
|
|
48
|
+
"@askmri/shared": "workspace:*",
|
|
49
|
+
"@trpc/server": "^11.7.1",
|
|
50
|
+
"ai": "^4",
|
|
51
|
+
"kleur": "^4.1.5",
|
|
52
|
+
"nanoid": "^5",
|
|
53
|
+
"tinyexec": "^1.0.1",
|
|
54
|
+
"zod": "^3.25.34"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^24.12.3",
|
|
58
|
+
"tsx": "^4.20.0",
|
|
59
|
+
"typescript": "~6.0.2"
|
|
60
|
+
}
|
|
61
|
+
}
|