@chengxianglibra/dsh-data-analysis 0.1.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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -0
  3. package/cordis.patch.yml +11 -0
  4. package/lib/bin/environment.js +91 -0
  5. package/lib/bridges.js +11 -0
  6. package/lib/client.js +744 -0
  7. package/lib/compatibility.js +66 -0
  8. package/lib/datasource/access.js +60 -0
  9. package/lib/datasource/bridge-programs.js +80 -0
  10. package/lib/datasource/bridge.js +204 -0
  11. package/lib/datasource/credentials.js +24 -0
  12. package/lib/datasource/index.js +4 -0
  13. package/lib/datasource/shell-env.js +386 -0
  14. package/lib/datasource/test.js +65 -0
  15. package/lib/disclosure/activation.js +369 -0
  16. package/lib/disclosure/bridge-program.js +11 -0
  17. package/lib/disclosure/bridge.js +65 -0
  18. package/lib/disclosure/help.js +227 -0
  19. package/lib/disclosure/index.js +3 -0
  20. package/lib/environment/binding.js +273 -0
  21. package/lib/environment/doctor.js +123 -0
  22. package/lib/environment/errors.js +11 -0
  23. package/lib/environment/index.js +7 -0
  24. package/lib/environment/runtime.js +558 -0
  25. package/lib/environment/source.js +6 -0
  26. package/lib/environment/subprocess.js +178 -0
  27. package/lib/environment/summary.js +19 -0
  28. package/lib/environment/workspace.js +49 -0
  29. package/lib/evidence/bridge-program.js +153 -0
  30. package/lib/evidence/bridge.js +166 -0
  31. package/lib/evidence/index.js +2 -0
  32. package/lib/evidence/sources.js +263 -0
  33. package/lib/index.js +7 -0
  34. package/lib/plugin.js +179 -0
  35. package/lib/types/bridges.d.ts +11 -0
  36. package/lib/types/client.d.ts +125 -0
  37. package/lib/types/compatibility.d.ts +29 -0
  38. package/lib/types/datasource/access.d.ts +27 -0
  39. package/lib/types/datasource/bridge.d.ts +53 -0
  40. package/lib/types/datasource/index.d.ts +8 -0
  41. package/lib/types/datasource/shell-env.d.ts +38 -0
  42. package/lib/types/datasource/test.d.ts +32 -0
  43. package/lib/types/disclosure/activation.d.ts +73 -0
  44. package/lib/types/disclosure/bridge.d.ts +28 -0
  45. package/lib/types/disclosure/help.d.ts +64 -0
  46. package/lib/types/disclosure/index.d.ts +5 -0
  47. package/lib/types/environment/binding.d.ts +26 -0
  48. package/lib/types/environment/doctor.d.ts +8 -0
  49. package/lib/types/environment/errors.d.ts +7 -0
  50. package/lib/types/environment/index.d.ts +9 -0
  51. package/lib/types/environment/runtime.d.ts +17 -0
  52. package/lib/types/environment/source.d.ts +7 -0
  53. package/lib/types/environment/subprocess.d.ts +19 -0
  54. package/lib/types/environment/types.d.ts +96 -0
  55. package/lib/types/environment/workspace.d.ts +10 -0
  56. package/lib/types/evidence/bridge.d.ts +40 -0
  57. package/lib/types/evidence/index.d.ts +4 -0
  58. package/lib/types/evidence/sources.d.ts +57 -0
  59. package/lib/types/index.d.ts +9 -0
  60. package/lib/types/plugin.d.ts +43 -0
  61. package/package.json +149 -0
  62. package/python/report-kit/dist/dsh_data_analysis_report_kit-3.0.0-py3-none-any.whl +0 -0
  63. package/skills/dsh-data-analysis-report/SKILL.md +57 -0
  64. package/skills/dsh-data-analysis-report/assets/marivo-artifact.js +113 -0
  65. package/skills/dsh-data-analysis-report/assets/marivo-session-dag.js +1201 -0
  66. package/skills/dsh-data-analysis-report/assets/report-data.js +449 -0
@@ -0,0 +1,558 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { access, chmod, cp, mkdir, readFile, realpath, rename, rm, stat, writeFile, } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
8
+ import { MARIVO_PACKAGE_SPEC, MARIVO_VERSION, RUNTIME_INSTALLATION_VERSION, } from "../compatibility.js";
9
+ import { MarivoEnvironmentError } from "./errors.js";
10
+ import { FixedSubprocessPolicy } from "./subprocess.js";
11
+ export const SHARED_PYTHON_SPEC = '3.10';
12
+ const PINNED_MARIVO_VERSION = MARIVO_VERSION;
13
+ export const SHARED_MARIVO_PACKAGE_SPEC = MARIVO_PACKAGE_SPEC;
14
+ export const REPORT_KIT_DISTRIBUTION = 'dsh-data-analysis-report-kit';
15
+ export const REPORT_KIT_VERSION = '3.0.0';
16
+ export const REPORT_KIT_PANDAS_RANGE = '>=2.2.0,<3.0.0';
17
+ export const REPORT_KIT_WHEEL_FILENAME = 'dsh_data_analysis_report_kit-3.0.0-py3-none-any.whl';
18
+ export const DEFAULT_SHARED_RUNTIME_INSTALL_TIMEOUT_MS = 600_000;
19
+ const INSTALLATION_SCHEMA = RUNTIME_INSTALLATION_VERSION;
20
+ const INSTALLATION_FILENAME = 'installation.json';
21
+ const SKILL_NAMES = ['marivo-analysis', 'marivo-semantic'];
22
+ const REPORT_ADAPTER_KIND = 'dsh-data-analysis-report-transport-adapter';
23
+ const PROBE_SCRIPT = String.raw `
24
+ import json
25
+ import os
26
+ import sys
27
+ import marivo
28
+ import pandas
29
+ import dsh_data_analysis_report
30
+ from packaging.specifiers import SpecifierSet
31
+ from packaging.version import Version
32
+
33
+ print(json.dumps({
34
+ "python_executable": os.path.abspath(sys.executable),
35
+ "marivo_version": marivo.__version__,
36
+ "package_path": os.path.abspath(marivo.__file__ or ""),
37
+ "pandas_version": pandas.__version__,
38
+ "pandas_supported": Version(pandas.__version__) in SpecifierSet(">=2.2.0,<3.0.0"),
39
+ "report_kit_version": dsh_data_analysis_report.__version__,
40
+ "report_kit_package_path": os.path.abspath(dsh_data_analysis_report.__file__ or ""),
41
+ "report_kit_public_imports": all(callable(value) for value in (
42
+ dsh_data_analysis_report.emit_computed,
43
+ dsh_data_analysis_report.emit_dataset,
44
+ dsh_data_analysis_report.emit_session_trace,
45
+ )),
46
+ }, sort_keys=True))
47
+ `.trim();
48
+ const PYTHON_VERSION_SCRIPT = String.raw `
49
+ import json
50
+ import os
51
+ import sys
52
+
53
+ print(json.dumps({
54
+ "python_executable": os.path.abspath(sys.executable),
55
+ "version": list(sys.version_info[:3]),
56
+ "prefix": os.path.abspath(sys.prefix),
57
+ }, sort_keys=True))
58
+ `.trim();
59
+ function administratorRepair(executable, reportKitWheel) {
60
+ const dependencies = [SHARED_MARIVO_PACKAGE_SPEC, `pandas${REPORT_KIT_PANDAS_RANGE}`];
61
+ const commands = [
62
+ {
63
+ executable,
64
+ args: ['-m', 'pip', 'install', '--upgrade', ...dependencies],
65
+ },
66
+ {
67
+ executable,
68
+ args: ['-m', 'pip', 'install', '--no-deps', reportKitWheel],
69
+ },
70
+ ];
71
+ const commandText = (command) => [command.executable, ...command.args].map((value) => JSON.stringify(value)).join(' ');
72
+ return {
73
+ message: `Run ${commandText(commands[0])}, then run ${commandText(commands[1])}`,
74
+ commands,
75
+ };
76
+ }
77
+ function positiveTimeout(value) {
78
+ const resolved = value ?? DEFAULT_SHARED_RUNTIME_INSTALL_TIMEOUT_MS;
79
+ if (!Number.isSafeInteger(resolved) || resolved < 1) {
80
+ throw new MarivoEnvironmentError('shared-runtime-config-invalid', 'Shared Marivo runtime installTimeoutMs must be a positive safe integer', { installTimeoutMs: resolved });
81
+ }
82
+ return resolved;
83
+ }
84
+ function normalizeAbsolute(name, value) {
85
+ if (!path.isAbsolute(value)) {
86
+ throw new MarivoEnvironmentError('shared-runtime-config-invalid', `${name} must be an absolute path: ${value}`, { [name]: value });
87
+ }
88
+ return path.normalize(value);
89
+ }
90
+ function defaultRuntimeRoot() {
91
+ return path.join(resolveDshHome(), 'dsh-data-analysis', 'runtimes', 'marivo');
92
+ }
93
+ function bundledReportKitWheel() {
94
+ return fileURLToPath(new URL(`../../python/report-kit/dist/${REPORT_KIT_WHEEL_FILENAME}`, import.meta.url));
95
+ }
96
+ async function assertReportKitWheel(filename) {
97
+ const selected = path.normalize(path.resolve(filename));
98
+ if (path.basename(selected) !== REPORT_KIT_WHEEL_FILENAME) {
99
+ throw new MarivoEnvironmentError('shared-runtime-report-kit-wheel-invalid', `Bundled report-kit wheel must be named ${REPORT_KIT_WHEEL_FILENAME}`, { expectedWheel: REPORT_KIT_WHEEL_FILENAME, actualWheel: path.basename(selected) });
100
+ }
101
+ try {
102
+ if (!(await stat(selected)).isFile())
103
+ throw new Error('not a file');
104
+ }
105
+ catch (cause) {
106
+ throw new MarivoEnvironmentError('shared-runtime-report-kit-wheel-unavailable', `Bundled report-kit wheel is unavailable: ${selected}`, {
107
+ distribution: REPORT_KIT_DISTRIBUTION,
108
+ version: REPORT_KIT_VERSION,
109
+ wheel: selected,
110
+ }, { cause });
111
+ }
112
+ return selected;
113
+ }
114
+ function venvPython(runtimeRoot) {
115
+ return path.join(runtimeRoot, '.venv', process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python');
116
+ }
117
+ function boundedText(buffer, max = 4_000) {
118
+ return buffer.toString('utf8').slice(0, max);
119
+ }
120
+ function requireSuccess(stage, result) {
121
+ if (result.exitCode === 0)
122
+ return result;
123
+ throw new MarivoEnvironmentError('shared-runtime-install-failed', `Shared Marivo runtime stage failed: ${stage}`, {
124
+ stage,
125
+ exitCode: result.exitCode,
126
+ signal: result.signal,
127
+ stdout: boundedText(result.stdout),
128
+ stderr: boundedText(result.stderr),
129
+ });
130
+ }
131
+ async function assertExecutable(executable) {
132
+ const selected = path.normalize(path.resolve(executable));
133
+ try {
134
+ const info = await stat(selected);
135
+ if (!info.isFile())
136
+ throw new Error('not a file');
137
+ await access(selected, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
138
+ return selected;
139
+ }
140
+ catch (cause) {
141
+ throw new MarivoEnvironmentError('python-unavailable', `Shared Marivo Python executable is missing or not executable: ${selected}`, { pythonExecutable: selected }, { cause });
142
+ }
143
+ }
144
+ function parseJsonObject(stage, result) {
145
+ requireSuccess(stage, result);
146
+ try {
147
+ const value = JSON.parse(result.stdout.toString('utf8'));
148
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
149
+ throw new Error('not an object');
150
+ return value;
151
+ }
152
+ catch (cause) {
153
+ throw new MarivoEnvironmentError('shared-runtime-install-failed', `Shared Marivo runtime stage returned invalid JSON: ${stage}`, { stage, stdout: boundedText(result.stdout), stderr: boundedText(result.stderr) }, { cause });
154
+ }
155
+ }
156
+ async function probeRuntime(runtimeRoot, executable, environment, timeoutMs, reportKitWheel, administrator, expectedMarivoVersion) {
157
+ const canonical = await assertExecutable(executable);
158
+ const policy = new FixedSubprocessPolicy(runtimeRoot, environment);
159
+ const result = await policy.run({
160
+ executable: canonical,
161
+ args: ['-c', PROBE_SCRIPT],
162
+ limits: { timeoutMs, stdoutMaxBytes: 16_384, stderrMaxBytes: 16_384 },
163
+ });
164
+ const repair = administrator ? administratorRepair(canonical, reportKitWheel) : undefined;
165
+ if (administrator && result.exitCode !== 0) {
166
+ throw new MarivoEnvironmentError('shared-runtime-package-unavailable', `Administrator Python must provide Marivo ${PINNED_MARIVO_VERSION}, pandas ${REPORT_KIT_PANDAS_RANGE}, and ${REPORT_KIT_DISTRIBUTION} ${REPORT_KIT_VERSION}. ${repair.message}`, {
167
+ pythonExecutable: canonical,
168
+ marivoVersion: PINNED_MARIVO_VERSION,
169
+ pandasRange: REPORT_KIT_PANDAS_RANGE,
170
+ reportKitDistribution: REPORT_KIT_DISTRIBUTION,
171
+ reportKitVersion: REPORT_KIT_VERSION,
172
+ reportKitWheel: reportKitWheel,
173
+ exitCode: result.exitCode,
174
+ stderr: boundedText(result.stderr),
175
+ repairCommands: repair.commands,
176
+ });
177
+ }
178
+ const probe = parseJsonObject('probe Runtime package identity', result);
179
+ if (typeof probe.python_executable !== 'string' ||
180
+ typeof probe.marivo_version !== 'string' ||
181
+ probe.marivo_version.length === 0 ||
182
+ typeof probe.package_path !== 'string' ||
183
+ probe.package_path.length === 0 ||
184
+ typeof probe.pandas_version !== 'string' ||
185
+ probe.pandas_version.length === 0 ||
186
+ typeof probe.pandas_supported !== 'boolean' ||
187
+ typeof probe.report_kit_version !== 'string' ||
188
+ probe.report_kit_version.length === 0 ||
189
+ typeof probe.report_kit_package_path !== 'string' ||
190
+ probe.report_kit_package_path.length === 0 ||
191
+ probe.report_kit_public_imports !== true) {
192
+ throw new MarivoEnvironmentError('shared-runtime-identity-mismatch', 'Shared Runtime returned an incomplete package identity', { probe });
193
+ }
194
+ const actualPython = await realpath(path.resolve(probe.python_executable));
195
+ const selectedPython = await realpath(canonical);
196
+ if (expectedMarivoVersion !== undefined && probe.marivo_version !== expectedMarivoVersion) {
197
+ throw new MarivoEnvironmentError('shared-runtime-version-unsupported', `Shared Marivo runtime version ${probe.marivo_version} is unsupported; install ${expectedMarivoVersion}.${repair === undefined ? '' : ` ${repair.message}`}`, {
198
+ supportedMarivoVersion: expectedMarivoVersion,
199
+ actualMarivoVersion: probe.marivo_version,
200
+ packagePath: probe.package_path,
201
+ ...(repair === undefined ? {} : { repairCommands: repair.commands }),
202
+ });
203
+ }
204
+ if (!probe.pandas_supported) {
205
+ throw new MarivoEnvironmentError('shared-runtime-pandas-unsupported', `Shared Runtime pandas ${probe.pandas_version} is unsupported; install ${REPORT_KIT_PANDAS_RANGE}.${repair === undefined ? '' : ` ${repair.message}`}`, {
206
+ supportedPandasRange: REPORT_KIT_PANDAS_RANGE,
207
+ actualPandasVersion: probe.pandas_version,
208
+ ...(repair === undefined ? {} : { repairCommands: repair.commands }),
209
+ });
210
+ }
211
+ if (probe.report_kit_version !== REPORT_KIT_VERSION) {
212
+ throw new MarivoEnvironmentError('shared-runtime-report-kit-unsupported', `Shared Runtime report kit ${probe.report_kit_version} is unsupported; install ${REPORT_KIT_VERSION} from ${reportKitWheel}.${repair === undefined ? '' : ` ${repair.message}`}`, {
213
+ supportedReportKitVersion: REPORT_KIT_VERSION,
214
+ actualReportKitVersion: probe.report_kit_version,
215
+ reportKitPackagePath: probe.report_kit_package_path,
216
+ reportKitWheel,
217
+ ...(repair === undefined ? {} : { repairCommands: repair.commands }),
218
+ });
219
+ }
220
+ if (actualPython !== selectedPython) {
221
+ throw new MarivoEnvironmentError('shared-runtime-identity-mismatch', 'Shared Marivo runtime import identity does not match its selected Python', {
222
+ expectedPython: canonical,
223
+ actualPython,
224
+ packagePath: probe.package_path,
225
+ });
226
+ }
227
+ return {
228
+ python_executable: canonical,
229
+ marivo_version: probe.marivo_version,
230
+ package_path: path.resolve(probe.package_path),
231
+ pandas_version: probe.pandas_version,
232
+ pandas_supported: probe.pandas_supported,
233
+ report_kit_version: probe.report_kit_version,
234
+ report_kit_package_path: path.resolve(probe.report_kit_package_path),
235
+ report_kit_public_imports: true,
236
+ };
237
+ }
238
+ async function validateSkills(skillsRoot) {
239
+ for (const skill of SKILL_NAMES) {
240
+ const skillFile = path.join(skillsRoot, skill, 'SKILL.md');
241
+ try {
242
+ if (!(await stat(skillFile)).isFile())
243
+ throw new Error('not a file');
244
+ const lines = (await readFile(skillFile, 'utf8')).split(/\r?\n/);
245
+ if (lines[0] !== '---')
246
+ throw new Error('frontmatter is missing');
247
+ const closing = lines.indexOf('---', 1);
248
+ if (closing < 2)
249
+ throw new Error('frontmatter is not closed');
250
+ const names = lines
251
+ .slice(1, closing)
252
+ .filter((line) => line.startsWith('name:'))
253
+ .map((line) => line.slice('name:'.length).trim());
254
+ if (names.length !== 1 || names[0] !== skill) {
255
+ throw new Error(`frontmatter name must be ${skill}`);
256
+ }
257
+ }
258
+ catch (cause) {
259
+ throw new MarivoEnvironmentError('shared-runtime-skills-invalid', `Shared Marivo skill is invalid: ${skillFile}`, { skillsRoot, skill }, { cause });
260
+ }
261
+ }
262
+ }
263
+ async function syncSkills(runtimeRoot, packagePath) {
264
+ const sourceRoot = path.join(path.dirname(packagePath), 'skills');
265
+ await validateSkills(sourceRoot);
266
+ const skillsRoot = path.join(runtimeRoot, 'skills');
267
+ const staging = path.join(runtimeRoot, `.skills-staging-${randomUUID()}`);
268
+ const previous = path.join(runtimeRoot, `.skills-previous-${randomUUID()}`);
269
+ await mkdir(staging, { recursive: true });
270
+ try {
271
+ for (const skill of SKILL_NAMES) {
272
+ await cp(path.join(sourceRoot, skill), path.join(staging, skill), { recursive: true });
273
+ }
274
+ await validateSkills(staging);
275
+ let hadPrevious = false;
276
+ try {
277
+ await rename(skillsRoot, previous);
278
+ hadPrevious = true;
279
+ }
280
+ catch (error) {
281
+ if (error.code !== 'ENOENT')
282
+ throw error;
283
+ }
284
+ try {
285
+ await rename(staging, skillsRoot);
286
+ }
287
+ catch (error) {
288
+ if (hadPrevious)
289
+ await rename(previous, skillsRoot);
290
+ throw error;
291
+ }
292
+ if (hadPrevious)
293
+ await rm(previous, { recursive: true, force: true });
294
+ return skillsRoot;
295
+ }
296
+ finally {
297
+ await rm(staging, { recursive: true, force: true });
298
+ }
299
+ }
300
+ async function readInstallation(runtimeRoot) {
301
+ try {
302
+ const value = JSON.parse(await readFile(path.join(runtimeRoot, INSTALLATION_FILENAME), 'utf8'));
303
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
304
+ return undefined;
305
+ const record = value;
306
+ const fields = Object.keys(record).sort();
307
+ const expectedFields = [
308
+ 'marivoVersion',
309
+ 'packagePath',
310
+ 'pythonExecutable',
311
+ 'reportAdapterKind',
312
+ 'reportKitPackagePath',
313
+ 'reportKitVersion',
314
+ 'schema',
315
+ 'skillsRoot',
316
+ ];
317
+ if (fields.length !== expectedFields.length ||
318
+ fields.some((field, index) => field !== expectedFields[index]) ||
319
+ record.schema !== INSTALLATION_SCHEMA ||
320
+ typeof record.marivoVersion !== 'string' ||
321
+ record.marivoVersion.length === 0 ||
322
+ typeof record.pythonExecutable !== 'string' ||
323
+ record.pythonExecutable.length === 0 ||
324
+ typeof record.packagePath !== 'string' ||
325
+ record.packagePath.length === 0 ||
326
+ record.reportAdapterKind !== REPORT_ADAPTER_KIND ||
327
+ typeof record.reportKitVersion !== 'string' ||
328
+ record.reportKitVersion.length === 0 ||
329
+ typeof record.reportKitPackagePath !== 'string' ||
330
+ record.reportKitPackagePath.length === 0 ||
331
+ typeof record.skillsRoot !== 'string' ||
332
+ record.skillsRoot.length === 0)
333
+ return undefined;
334
+ return record;
335
+ }
336
+ catch {
337
+ return undefined;
338
+ }
339
+ }
340
+ async function writeInstallation(runtimeRoot, record) {
341
+ const installationPath = path.join(runtimeRoot, INSTALLATION_FILENAME);
342
+ const temporary = path.join(runtimeRoot, `.installation-${randomUUID()}.json`);
343
+ await writeFile(temporary, `${JSON.stringify(record, undefined, 2)}\n`, {
344
+ encoding: 'utf8',
345
+ mode: 0o600,
346
+ });
347
+ if (process.platform !== 'win32')
348
+ await chmod(temporary, 0o600);
349
+ await rename(temporary, installationPath);
350
+ return installationPath;
351
+ }
352
+ async function validatedExisting(runtimeRoot, configuredPython, environment, timeoutMs, reportKitWheel) {
353
+ const record = await readInstallation(runtimeRoot);
354
+ if (record === undefined)
355
+ return undefined;
356
+ if (record.marivoVersion !== PINNED_MARIVO_VERSION)
357
+ return undefined;
358
+ if (record.reportKitVersion !== REPORT_KIT_VERSION)
359
+ return undefined;
360
+ const expectedPython = configuredPython ?? venvPython(runtimeRoot);
361
+ const expectedSkillsRoot = path.join(runtimeRoot, 'skills');
362
+ if (path.normalize(record.skillsRoot) !== path.normalize(expectedSkillsRoot))
363
+ return undefined;
364
+ try {
365
+ const selectedPython = await assertExecutable(expectedPython);
366
+ if (path.normalize(record.pythonExecutable) !== selectedPython)
367
+ return undefined;
368
+ const probe = await probeRuntime(runtimeRoot, selectedPython, environment, timeoutMs, reportKitWheel, configuredPython !== undefined, record.marivoVersion);
369
+ if (path.normalize(probe.package_path) !== path.normalize(record.packagePath))
370
+ return undefined;
371
+ if (path.normalize(probe.report_kit_package_path) !== path.normalize(record.reportKitPackagePath))
372
+ return undefined;
373
+ await validateSkills(record.skillsRoot);
374
+ return {
375
+ runtimeRoot,
376
+ pythonExecutable: probe.python_executable,
377
+ marivoVersion: probe.marivo_version,
378
+ packagePath: probe.package_path,
379
+ reportKitVersion: probe.report_kit_version,
380
+ reportKitPackagePath: probe.report_kit_package_path,
381
+ skillsRoot: record.skillsRoot,
382
+ installationPath: path.join(runtimeRoot, INSTALLATION_FILENAME),
383
+ };
384
+ }
385
+ catch {
386
+ return undefined;
387
+ }
388
+ }
389
+ async function acquireLock(lockPath, timeoutMs, waitIntervalMs) {
390
+ const started = Date.now();
391
+ while (true) {
392
+ try {
393
+ await mkdir(lockPath);
394
+ await writeFile(path.join(lockPath, 'owner.json'), `${JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() })}\n`);
395
+ return async () => {
396
+ await rm(lockPath, { recursive: true, force: true });
397
+ };
398
+ }
399
+ catch (error) {
400
+ if (error.code !== 'EEXIST')
401
+ throw error;
402
+ let age = 0;
403
+ try {
404
+ age = Date.now() - (await stat(lockPath)).mtimeMs;
405
+ }
406
+ catch {
407
+ continue;
408
+ }
409
+ if (age >= timeoutMs && !(await lockOwnerAlive(lockPath))) {
410
+ try {
411
+ await rename(lockPath, `${lockPath}.stale-${Date.now()}-${randomUUID()}`);
412
+ continue;
413
+ }
414
+ catch (renameError) {
415
+ if (renameError.code === 'ENOENT')
416
+ continue;
417
+ }
418
+ }
419
+ if (Date.now() - started >= timeoutMs) {
420
+ throw new MarivoEnvironmentError('shared-runtime-lock-timeout', `Timed out waiting for the shared Marivo runtime installation lock: ${lockPath}`, { lockPath, timeoutMs });
421
+ }
422
+ await new Promise((resolve) => setTimeout(resolve, waitIntervalMs));
423
+ }
424
+ }
425
+ }
426
+ async function lockOwnerAlive(lockPath) {
427
+ try {
428
+ const owner = JSON.parse(await readFile(path.join(lockPath, 'owner.json'), 'utf8'));
429
+ if (!Number.isSafeInteger(owner.pid) || owner.pid < 1)
430
+ return false;
431
+ try {
432
+ process.kill(owner.pid, 0);
433
+ return true;
434
+ }
435
+ catch (error) {
436
+ return error.code !== 'ESRCH';
437
+ }
438
+ }
439
+ catch {
440
+ return false;
441
+ }
442
+ }
443
+ async function backupInvalidRuntime(runtimeRoot) {
444
+ try {
445
+ await stat(runtimeRoot);
446
+ }
447
+ catch (error) {
448
+ if (error.code === 'ENOENT')
449
+ return;
450
+ throw error;
451
+ }
452
+ await rename(runtimeRoot, `${runtimeRoot}.invalid-${Date.now()}-${randomUUID()}`);
453
+ }
454
+ async function installManagedRuntime(runtimeRoot, uvExecutable, reportKitWheel, environment, timeoutMs) {
455
+ const policy = new FixedSubprocessPolicy(runtimeRoot, environment);
456
+ const limits = { timeoutMs, stdoutMaxBytes: 1_048_576, stderrMaxBytes: 1_048_576 };
457
+ requireSuccess('install managed Python', await policy.run({
458
+ executable: uvExecutable,
459
+ args: ['python', 'install', SHARED_PYTHON_SPEC],
460
+ limits,
461
+ }));
462
+ const found = requireSuccess('resolve managed Python', await policy.run({
463
+ executable: uvExecutable,
464
+ args: ['python', 'find', '--managed-python', SHARED_PYTHON_SPEC],
465
+ limits,
466
+ }));
467
+ const managedPython = found.stdout.toString('utf8').trim();
468
+ if (!path.isAbsolute(managedPython)) {
469
+ throw new MarivoEnvironmentError('shared-runtime-install-failed', 'uv returned a non-absolute managed Python path', { managedPython });
470
+ }
471
+ const canonicalManagedPython = await assertExecutable(managedPython);
472
+ const version = parseJsonObject('validate managed Python', await policy.run({
473
+ executable: canonicalManagedPython,
474
+ args: ['-c', PYTHON_VERSION_SCRIPT],
475
+ limits,
476
+ }));
477
+ if (!Array.isArray(version.version) ||
478
+ typeof version.version[0] !== 'number' ||
479
+ typeof version.version[1] !== 'number' ||
480
+ version.version[0] < 3 ||
481
+ (version.version[0] === 3 && version.version[1] < 10)) {
482
+ throw new MarivoEnvironmentError('shared-runtime-install-failed', 'uv managed Python does not satisfy Marivo >=3.10', { version: version.version, pythonExecutable: canonicalManagedPython });
483
+ }
484
+ requireSuccess('create shared virtual environment', await policy.run({
485
+ executable: uvExecutable,
486
+ args: ['venv', '--python', canonicalManagedPython, '--seed', path.join(runtimeRoot, '.venv')],
487
+ limits,
488
+ }));
489
+ const executable = venvPython(runtimeRoot);
490
+ requireSuccess(`install Marivo ${PINNED_MARIVO_VERSION}`, await policy.run({
491
+ executable: uvExecutable,
492
+ args: ['pip', 'install', '--python', executable, '--upgrade', SHARED_MARIVO_PACKAGE_SPEC],
493
+ limits,
494
+ }));
495
+ requireSuccess(`install report kit ${REPORT_KIT_VERSION}`, await policy.run({
496
+ executable: uvExecutable,
497
+ args: ['pip', 'install', '--python', executable, '--no-deps', reportKitWheel],
498
+ limits,
499
+ }));
500
+ return probeRuntime(runtimeRoot, executable, environment, timeoutMs, reportKitWheel, false, PINNED_MARIVO_VERSION);
501
+ }
502
+ /** Ensure and validate the one DSH-home-owned Marivo installation. */
503
+ export async function ensureSharedMarivoRuntime(config = {}, options = {}) {
504
+ const runtimeRoot = config.runtimeRoot === undefined
505
+ ? defaultRuntimeRoot()
506
+ : normalizeAbsolute('runtimeRoot', config.runtimeRoot);
507
+ const configuredPython = config.pythonExecutable === undefined
508
+ ? undefined
509
+ : normalizeAbsolute('pythonExecutable', config.pythonExecutable);
510
+ const uvExecutable = config.uvExecutable === undefined
511
+ ? 'uv'
512
+ : normalizeAbsolute('uvExecutable', config.uvExecutable);
513
+ const timeoutMs = positiveTimeout(config.installTimeoutMs);
514
+ const reportKitWheel = path.normalize(path.resolve(options.reportKitWheelPath ?? bundledReportKitWheel()));
515
+ const existing = await validatedExisting(runtimeRoot, configuredPython, options.environment, timeoutMs, reportKitWheel);
516
+ if (existing !== undefined)
517
+ return existing;
518
+ await mkdir(path.dirname(runtimeRoot), { recursive: true });
519
+ const lockPath = `${runtimeRoot}.install-lock`;
520
+ const release = await acquireLock(lockPath, timeoutMs, options.waitIntervalMs ?? 100);
521
+ try {
522
+ const afterLock = await validatedExisting(runtimeRoot, configuredPython, options.environment, timeoutMs, reportKitWheel);
523
+ if (afterLock !== undefined)
524
+ return afterLock;
525
+ if (configuredPython === undefined)
526
+ await assertReportKitWheel(reportKitWheel);
527
+ await backupInvalidRuntime(runtimeRoot);
528
+ await mkdir(runtimeRoot, { recursive: true });
529
+ const probe = configuredPython === undefined
530
+ ? await installManagedRuntime(runtimeRoot, uvExecutable, reportKitWheel, options.environment, timeoutMs)
531
+ : await probeRuntime(runtimeRoot, configuredPython, options.environment, timeoutMs, reportKitWheel, true, PINNED_MARIVO_VERSION);
532
+ const skillsRoot = await syncSkills(runtimeRoot, probe.package_path);
533
+ const record = {
534
+ schema: INSTALLATION_SCHEMA,
535
+ marivoVersion: probe.marivo_version,
536
+ pythonExecutable: probe.python_executable,
537
+ packagePath: probe.package_path,
538
+ reportAdapterKind: REPORT_ADAPTER_KIND,
539
+ reportKitVersion: probe.report_kit_version,
540
+ reportKitPackagePath: probe.report_kit_package_path,
541
+ skillsRoot,
542
+ };
543
+ const installationPath = await writeInstallation(runtimeRoot, record);
544
+ return {
545
+ runtimeRoot,
546
+ pythonExecutable: probe.python_executable,
547
+ marivoVersion: probe.marivo_version,
548
+ packagePath: probe.package_path,
549
+ reportKitVersion: probe.report_kit_version,
550
+ reportKitPackagePath: probe.report_kit_package_path,
551
+ skillsRoot,
552
+ installationPath,
553
+ };
554
+ }
555
+ finally {
556
+ await release();
557
+ }
558
+ }
@@ -0,0 +1,6 @@
1
+ export function resolveMarivoEnvironmentSource(source) {
2
+ return typeof source === 'function' ? source() : Promise.resolve(source);
3
+ }
4
+ export function resolveMarivoBridgeSource(source) {
5
+ return typeof source === 'function' ? source() : Promise.resolve(source);
6
+ }