@creeperhost/modlens-mcp 1.6.17 → 1.6.19

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 (181) hide show
  1. package/README.md +97 -10
  2. package/TESTING.md +69 -0
  3. package/dist/cli.js +21 -5
  4. package/dist/cli.js.map +1 -1
  5. package/dist/curseforge.d.ts +9 -7
  6. package/dist/curseforge.d.ts.map +1 -1
  7. package/dist/curseforge.js +48 -55
  8. package/dist/curseforge.js.map +1 -1
  9. package/dist/db.d.ts.map +1 -1
  10. package/dist/db.js +34 -3
  11. package/dist/db.js.map +1 -1
  12. package/dist/embeddings.d.ts.map +1 -1
  13. package/dist/embeddings.js +1 -0
  14. package/dist/embeddings.js.map +1 -1
  15. package/dist/feed-the-beast.d.ts +5 -5
  16. package/dist/feed-the-beast.d.ts.map +1 -1
  17. package/dist/feed-the-beast.js +5 -21
  18. package/dist/feed-the-beast.js.map +1 -1
  19. package/dist/generated/sqlite/internal/class.js +2 -2
  20. package/dist/generated/sqlite/internal/class.js.map +1 -1
  21. package/dist/java-tools.d.ts +1 -0
  22. package/dist/java-tools.d.ts.map +1 -1
  23. package/dist/java-tools.js +31 -7
  24. package/dist/java-tools.js.map +1 -1
  25. package/dist/launcher.js +15 -0
  26. package/dist/launcher.js.map +1 -1
  27. package/dist/mappings.d.ts +4 -1
  28. package/dist/mappings.d.ts.map +1 -1
  29. package/dist/mappings.js +120 -52
  30. package/dist/mappings.js.map +1 -1
  31. package/dist/minecraft.d.ts +0 -4
  32. package/dist/minecraft.d.ts.map +1 -1
  33. package/dist/minecraft.js +7 -23
  34. package/dist/minecraft.js.map +1 -1
  35. package/dist/mod-artifacts.d.ts +4 -0
  36. package/dist/mod-artifacts.d.ts.map +1 -0
  37. package/dist/mod-artifacts.js +58 -0
  38. package/dist/mod-artifacts.js.map +1 -0
  39. package/dist/modpacks-ch.d.ts +72 -46
  40. package/dist/modpacks-ch.d.ts.map +1 -1
  41. package/dist/modpacks-ch.js +134 -20
  42. package/dist/modpacks-ch.js.map +1 -1
  43. package/dist/modrinth.d.ts +14 -6
  44. package/dist/modrinth.d.ts.map +1 -1
  45. package/dist/modrinth.js +70 -55
  46. package/dist/modrinth.js.map +1 -1
  47. package/dist/platform-adapter.d.ts +1 -0
  48. package/dist/platform-adapter.d.ts.map +1 -1
  49. package/dist/platform.d.ts.map +1 -1
  50. package/dist/platform.js +20 -44
  51. package/dist/platform.js.map +1 -1
  52. package/dist/processor.d.ts +5 -0
  53. package/dist/processor.d.ts.map +1 -1
  54. package/dist/processor.js +166 -46
  55. package/dist/processor.js.map +1 -1
  56. package/dist/registry-submission.d.ts +34 -0
  57. package/dist/registry-submission.d.ts.map +1 -0
  58. package/dist/registry-submission.js +128 -0
  59. package/dist/registry-submission.js.map +1 -0
  60. package/dist/repositories/array-fields.d.ts +3 -0
  61. package/dist/repositories/array-fields.d.ts.map +1 -0
  62. package/dist/repositories/array-fields.js +8 -0
  63. package/dist/repositories/array-fields.js.map +1 -0
  64. package/dist/repositories/embeddings-sqlite.d.ts +1 -0
  65. package/dist/repositories/embeddings-sqlite.d.ts.map +1 -1
  66. package/dist/repositories/embeddings-sqlite.js +26 -58
  67. package/dist/repositories/embeddings-sqlite.js.map +1 -1
  68. package/dist/repositories/embeddings.d.ts.map +1 -1
  69. package/dist/repositories/embeddings.js +26 -3
  70. package/dist/repositories/embeddings.js.map +1 -1
  71. package/dist/repositories/index.d.ts +1 -1
  72. package/dist/repositories/index.d.ts.map +1 -1
  73. package/dist/repositories/mod.d.ts +8 -5
  74. package/dist/repositories/mod.d.ts.map +1 -1
  75. package/dist/repositories/mod.js +25 -11
  76. package/dist/repositories/mod.js.map +1 -1
  77. package/dist/repositories/packs.d.ts.map +1 -1
  78. package/dist/repositories/packs.js +10 -2
  79. package/dist/repositories/packs.js.map +1 -1
  80. package/dist/search-adapter.d.ts +2 -0
  81. package/dist/search-adapter.d.ts.map +1 -1
  82. package/dist/search-adapter.js +21 -9
  83. package/dist/search-adapter.js.map +1 -1
  84. package/dist/security.d.ts +2 -0
  85. package/dist/security.d.ts.map +1 -1
  86. package/dist/security.js +5 -1
  87. package/dist/security.js.map +1 -1
  88. package/dist/server.js +63 -25
  89. package/dist/server.js.map +1 -1
  90. package/dist/sqlite-json.d.ts +3 -0
  91. package/dist/sqlite-json.d.ts.map +1 -0
  92. package/dist/sqlite-json.js +78 -0
  93. package/dist/sqlite-json.js.map +1 -0
  94. package/dist/sqlite-parameters.d.ts +3 -0
  95. package/dist/sqlite-parameters.d.ts.map +1 -0
  96. package/dist/sqlite-parameters.js +19 -0
  97. package/dist/sqlite-parameters.js.map +1 -0
  98. package/dist/sqlite-schema.d.ts +4 -0
  99. package/dist/sqlite-schema.d.ts.map +1 -0
  100. package/dist/sqlite-schema.js +76 -0
  101. package/dist/sqlite-schema.js.map +1 -0
  102. package/dist/tools/catalog.d.ts +1 -6
  103. package/dist/tools/catalog.d.ts.map +1 -1
  104. package/dist/tools/catalog.js +10 -14
  105. package/dist/tools/catalog.js.map +1 -1
  106. package/dist/tools/compat-check.d.ts.map +1 -1
  107. package/dist/tools/compat-check.js +18 -30
  108. package/dist/tools/compat-check.js.map +1 -1
  109. package/dist/tools/diagnostics.js.map +1 -1
  110. package/dist/tools/docs.d.ts.map +1 -1
  111. package/dist/tools/docs.js +8 -3
  112. package/dist/tools/docs.js.map +1 -1
  113. package/dist/tools/embed-registry.d.ts +20 -4
  114. package/dist/tools/embed-registry.d.ts.map +1 -1
  115. package/dist/tools/embed-registry.js +30 -19
  116. package/dist/tools/embed-registry.js.map +1 -1
  117. package/dist/tools/graphify.d.ts +19 -4
  118. package/dist/tools/graphify.d.ts.map +1 -1
  119. package/dist/tools/graphify.js +61 -13
  120. package/dist/tools/graphify.js.map +1 -1
  121. package/dist/tools/ingest.d.ts.map +1 -1
  122. package/dist/tools/ingest.js +4 -4
  123. package/dist/tools/ingest.js.map +1 -1
  124. package/dist/tools/kubejs.js +1 -1
  125. package/dist/tools/kubejs.js.map +1 -1
  126. package/dist/tools/mc-fts.d.ts.map +1 -1
  127. package/dist/tools/mc-fts.js +9 -4
  128. package/dist/tools/mc-fts.js.map +1 -1
  129. package/dist/tools/mcmeta.d.ts +10 -1
  130. package/dist/tools/mcmeta.d.ts.map +1 -1
  131. package/dist/tools/mcmeta.js +43 -29
  132. package/dist/tools/mcmeta.js.map +1 -1
  133. package/dist/tools/mixin-scan.d.ts +12 -6
  134. package/dist/tools/mixin-scan.d.ts.map +1 -1
  135. package/dist/tools/mixin-scan.js +12 -7
  136. package/dist/tools/mixin-scan.js.map +1 -1
  137. package/dist/tools/mod-data.d.ts.map +1 -1
  138. package/dist/tools/mod-data.js +40 -27
  139. package/dist/tools/mod-data.js.map +1 -1
  140. package/dist/tools/modpacks-ch.d.ts +9 -5
  141. package/dist/tools/modpacks-ch.d.ts.map +1 -1
  142. package/dist/tools/modpacks-ch.js +54 -33
  143. package/dist/tools/modpacks-ch.js.map +1 -1
  144. package/dist/tools/packtools.js +3 -3
  145. package/dist/tools/packtools.js.map +1 -1
  146. package/dist/tools/platform.d.ts +2 -2
  147. package/dist/tools/platform.d.ts.map +1 -1
  148. package/dist/tools/platform.js +33 -23
  149. package/dist/tools/platform.js.map +1 -1
  150. package/dist/tools/project.d.ts +342 -0
  151. package/dist/tools/project.d.ts.map +1 -0
  152. package/dist/tools/project.js +519 -0
  153. package/dist/tools/project.js.map +1 -0
  154. package/dist/tools/reports.d.ts.map +1 -1
  155. package/dist/tools/reports.js +5 -3
  156. package/dist/tools/reports.js.map +1 -1
  157. package/dist/tools/vanilla-data.d.ts.map +1 -1
  158. package/dist/tools/vanilla-data.js +3 -33
  159. package/dist/tools/vanilla-data.js.map +1 -1
  160. package/dist/tools/vanilla.d.ts +3 -1
  161. package/dist/tools/vanilla.d.ts.map +1 -1
  162. package/dist/tools/vanilla.js +26 -12
  163. package/dist/tools/vanilla.js.map +1 -1
  164. package/dist/tools/version-diff.d.ts.map +1 -1
  165. package/dist/tools/version-diff.js +4 -24
  166. package/dist/tools/version-diff.js.map +1 -1
  167. package/dist/version-ranges.d.ts +5 -0
  168. package/dist/version-ranges.d.ts.map +1 -0
  169. package/dist/version-ranges.js +93 -0
  170. package/dist/version-ranges.js.map +1 -0
  171. package/package.json +8 -3
  172. package/prisma/backends/template.db +0 -0
  173. package/scripts/build-template-db.mjs +5 -2
  174. package/scripts/enable-sqlite-vec.mjs +2 -106
  175. package/scripts/gradle/modlens.init.gradle +154 -0
  176. package/scripts/project-upload.mjs +74 -0
  177. package/scripts/test-minecraft-eras.mjs +202 -0
  178. package/scripts/test-package.mjs +217 -0
  179. package/scripts/test-project-gradle.mjs +76 -0
  180. package/scripts/test-project-http.mjs +98 -0
  181. package/scripts/test-setup.mjs +18 -0
@@ -0,0 +1,154 @@
1
+ // Apply without modifying a project: ./gradlew -I /path/to/modlens.init.gradle :modlensExport
2
+ // ModDevGradle 2.x adapter plus a generic Java source-set compile-classpath exporter.
3
+ import groovy.json.JsonOutput
4
+ import java.security.MessageDigest
5
+ import java.security.SecureRandom
6
+ import java.nio.file.Files
7
+ import java.nio.file.StandardCopyOption
8
+ import java.nio.file.StandardOpenOption
9
+ import java.nio.file.FileAlreadyExistsException
10
+ import java.util.zip.ZipEntry
11
+ import java.util.zip.ZipOutputStream
12
+ import org.gradle.api.GradleException
13
+ import org.gradle.api.plugins.JavaPluginExtension
14
+ import org.gradle.api.provider.Provider
15
+
16
+ def sha256 = { File file ->
17
+ def md = MessageDigest.getInstance('SHA-256')
18
+ file.withInputStream { input ->
19
+ byte[] buffer = new byte[65536]
20
+ int count
21
+ while ((count = input.read(buffer)) != -1) md.update(buffer, 0, count)
22
+ }
23
+ md.digest().encodeHex().toString()
24
+ }
25
+
26
+ gradle.projectsEvaluated {
27
+ allprojects { p ->
28
+ def javaExtension = p.extensions.findByType(JavaPluginExtension)
29
+ if (javaExtension == null) return
30
+ String selected = p.findProperty('modlensSourceSet')?.toString() ?: 'main'
31
+ def sourceSet = javaExtension.sourceSets.findByName(selected)
32
+ if (sourceSet == null) {
33
+ // A sibling module may not have the source set selected for the requested module.
34
+ p.tasks.register('modlensExport') {
35
+ doLast { throw new GradleException("Unknown ModLens source set '${selected}' in ${p.path}") }
36
+ }
37
+ return
38
+ }
39
+ boolean moddev = p.plugins.hasPlugin('net.neoforged.moddev') || p.plugins.hasPlugin('net.neoforged.moddev.legacyforge')
40
+ def artifactsTask = moddev ? p.tasks.findByName('createMinecraftArtifacts') : null
41
+ if (moddev && artifactsTask == null) throw new GradleException('ModLens requires a ModDevGradle version with createMinecraftArtifacts (2.x).')
42
+
43
+ p.tasks.register('modlensExport') { task ->
44
+ group = 'modlens'
45
+ description = 'Export the resolved compile environment for local or remote ModLens.'
46
+ // Gradle resolves/builds the actual compilation inputs, including AT processing.
47
+ dependsOn(sourceSet.compileClasspath)
48
+ if (artifactsTask != null) dependsOn(artifactsTask)
49
+ // This task accesses the evaluated model and is intentionally not configuration-cache compatible.
50
+ notCompatibleWithConfigurationCache('ModLens exports the evaluated compile classpath and modding task outputs.')
51
+ doLast {
52
+ File output = p.layout.buildDirectory.dir("modlens/${selected}").get().asFile
53
+ output.mkdirs()
54
+ def artifacts = []
55
+ def files = []
56
+ def byFile = [:]
57
+ def addArtifact = { File file, String kind, String sourceFor ->
58
+ if (!file.isFile()) throw new GradleException("Missing ModLens artifact: ${file.name}")
59
+ String path = "artifacts/${artifacts.size()}.jar"
60
+ def info = [path: path, name: file.name, sha256: sha256(file), size: file.length(), kind: kind]
61
+ if (sourceFor != null) info.sourceFor = sourceFor
62
+ artifacts.add(info)
63
+ files.add(file)
64
+ if (kind == 'classpath') byFile[file.canonicalPath] = path
65
+ path
66
+ }
67
+ sourceSet.compileClasspath.files.eachWithIndex { File file, int index ->
68
+ if (!file.exists()) throw new GradleException("Compile classpath entry does not exist: ${file.name}")
69
+ if (file.isDirectory()) {
70
+ // Project dependencies often expose class directories instead of JARs.
71
+ File jar = new File(output, "directory-${index}.jar")
72
+ def entries = p.fileTree(file).matching { include '**/*.class'; include '**/*.java' }.files.sort { a, b ->
73
+ file.toPath().relativize(a.toPath()).toString() <=> file.toPath().relativize(b.toPath()).toString()
74
+ }
75
+ jar.withOutputStream { stream ->
76
+ def zip = new ZipOutputStream(stream)
77
+ entries.each { entry ->
78
+ def name = file.toPath().relativize(entry.toPath()).toString().replace('\\', '/')
79
+ def ze = new ZipEntry(name); ze.time = 0
80
+ zip.putNextEntry(ze); entry.withInputStream { zip << it }; zip.closeEntry()
81
+ }
82
+ zip.finish()
83
+ }
84
+ addArtifact(jar, 'classpath', null)
85
+ } else if (file.name.endsWith('.jar')) {
86
+ addArtifact(file, 'classpath', null)
87
+ } else {
88
+ throw new GradleException("Unsupported compile classpath entry: ${file.name}")
89
+ }
90
+ }
91
+ if (artifacts.empty) throw new GradleException('Compile classpath is empty; there is no dependency environment to export.')
92
+
93
+ def transforms = []
94
+ if (artifactsTask != null) {
95
+ // Bind sources to the binary actually selected by the compile classpath, not a guessed cache path.
96
+ def binaryPaths = ['gameJarArtifact', 'gameJarWithSourcesArtifact'].collect { property ->
97
+ artifactsTask.hasProperty(property) ? artifactsTask.property(property).orNull?.asFile?.canonicalPath : null
98
+ }.findAll { it != null && byFile.containsKey(it) }
99
+ if (binaryPaths.empty) throw new GradleException('ModLens cannot locate ModDevGradle Minecraft output on this source set compile classpath. Select a mod-enabled source set.')
100
+ def sourceFile = artifactsTask.hasProperty('gameSourcesArtifact') ? artifactsTask.gameSourcesArtifact.orNull?.asFile : null
101
+ if (sourceFile?.isFile()) binaryPaths.each { path -> addArtifact(sourceFile, 'sources', byFile[path]) }
102
+ if (artifactsTask.hasProperty('accessTransformers')) {
103
+ artifactsTask.accessTransformers.files.each { File file -> transforms.add([name: file.name, sha256: sha256(file)]) }
104
+ }
105
+ }
106
+ def neo = p.extensions.findByName('neoForge') ?: p.extensions.findByName('legacyForge')
107
+ def propertyText = { String name -> p.findProperty(name)?.toString() ?: '' }
108
+ String loaderVersion = propertyText('neo_version') ?: propertyText('forge_version')
109
+ if (neo != null && neo.hasProperty('version')) {
110
+ def version = neo.version
111
+ loaderVersion = (version instanceof Provider ? version.orNull : version)?.toString() ?: loaderVersion
112
+ }
113
+ String mappings = propertyText('modlensMappings')
114
+ if (!mappings && neo != null && neo.hasProperty('parchment') && neo.parchment.enabled.getOrElse(false)) {
115
+ mappings = "parchment:${neo.parchment.minecraftVersion.orNull ?: ''}:${neo.parchment.mappingsVersion.orNull ?: ''}"
116
+ }
117
+ if (!mappings) mappings = moddev ? 'toolchain-prepared (official names where applicable)' : 'compile-classpath'
118
+ def manifest = [format: 'modlens-project-v1', project: "${p.rootProject.name}${p.path}", sourceSet: selected,
119
+ toolchain: moddev ? 'moddevgradle' : 'gradle-compile-classpath',
120
+ minecraftVersion: propertyText('minecraft_version') ?: propertyText('modlensMinecraftVersion'),
121
+ loaderVersion: loaderVersion, mappings: mappings,
122
+ javaVersion: p.tasks.named(sourceSet.compileJavaTaskName).get().options.release.orNull ?: javaExtension.toolchain.languageVersion.orNull?.asInt() ?: Integer.parseInt(org.gradle.api.JavaVersion.current().majorVersion),
123
+ transformations: transforms, artifacts: artifacts]
124
+ File temporary = new File(output, 'environment.zip.tmp')
125
+ temporary.withOutputStream { stream ->
126
+ def zip = new ZipOutputStream(stream)
127
+ def metadata = new ZipEntry('manifest.json'); metadata.time = 0
128
+ zip.putNextEntry(metadata); zip.write(JsonOutput.toJson(manifest).getBytes('UTF-8')); zip.closeEntry()
129
+ artifacts.eachWithIndex { artifact, index ->
130
+ def entry = new ZipEntry(artifact.path); entry.time = 0
131
+ zip.putNextEntry(entry); files[index].withInputStream { zip << it }; zip.closeEntry()
132
+ }
133
+ zip.finish()
134
+ }
135
+ File destination = new File(output, 'environment.zip')
136
+ Files.move(temporary.toPath(), destination.toPath(), StandardCopyOption.REPLACE_EXISTING)
137
+ File keyFile = new File(p.projectDir, '.gradle/modlens/project-key.txt')
138
+ keyFile.parentFile.mkdirs()
139
+ if (!keyFile.exists()) {
140
+ byte[] key = new byte[32]; new SecureRandom().nextBytes(key)
141
+ // CREATE_NEW prevents concurrent exports from silently replacing a project key.
142
+ try { Files.write(keyFile.toPath(), (key.encodeHex().toString() + '\n').getBytes('UTF-8'), StandardOpenOption.CREATE_NEW) }
143
+ catch (FileAlreadyExistsException ignored) { }
144
+ keyFile.setReadable(false, false); keyFile.setWritable(false, false)
145
+ keyFile.setReadable(true, true); keyFile.setWritable(true, true)
146
+ }
147
+ if (!(keyFile.text.trim() ==~ /[a-f0-9]{64}/)) throw new GradleException('Invalid existing ModLens project key; expected 64 lowercase hex characters.')
148
+ logger.lifecycle("ModLens bundle: ${destination}")
149
+ logger.lifecycle("ModLens private key file: ${keyFile}")
150
+ logger.lifecycle('Re-export and import after changing dependencies, mappings or access transformers.')
151
+ }
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ // Transfer a Gradle export over MCP without exposing a local filesystem path to the server.
3
+ import { createHash } from 'node:crypto';
4
+ import { createReadStream } from 'node:fs';
5
+ import { readFile, open, stat } from 'node:fs/promises';
6
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
7
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
8
+
9
+ const [endpoint, bundlePath, keyFile] = process.argv.slice(2);
10
+ if (!endpoint || !bundlePath || !keyFile || process.argv.length !== 5) {
11
+ console.error('Usage: node scripts/project-upload.mjs <https://host/mcp> <environment.zip> <project-key.txt>');
12
+ process.exit(1);
13
+ }
14
+ const url = new URL(endpoint);
15
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) {
16
+ throw new Error('Use HTTPS for remote project uploads (HTTP is allowed only on loopback).');
17
+ }
18
+ if (url.username || url.password || url.search || url.hash) throw new Error('Endpoint must not contain credentials, query parameters or a fragment. Use MODLENS_AUTH_TOKEN for proxy authentication.');
19
+ const projectKey = (await readFile(keyFile, 'utf8')).trim();
20
+ if (!/^[a-f0-9]{64}$/.test(projectKey)) throw new Error('Invalid project key file');
21
+ const size = (await stat(bundlePath)).size;
22
+ if (!size || size > 512 * 1024 * 1024) throw new Error('Bundle must contain 1 byte to 512 MiB');
23
+ const hash = createHash('sha256');
24
+ for await (const chunk of createReadStream(bundlePath)) hash.update(chunk);
25
+ const client = new Client({ name: 'modlens-project-upload', version: '1.0.0' });
26
+ const transport = new StreamableHTTPClientTransport(url, { requestInit: {
27
+ // Never follow redirects carrying the private project capability in the request body.
28
+ redirect: 'error',
29
+ headers: process.env.MODLENS_AUTH_TOKEN ? { Authorization: `Bearer ${process.env.MODLENS_AUTH_TOKEN}` } : {},
30
+ } });
31
+ let uploadId;
32
+ try {
33
+ await client.connect(transport);
34
+ const call = async (args) => {
35
+ const response = await client.callTool({ name: 'project', arguments: { ...args, projectKey } }, undefined, { timeout: 300_000 });
36
+ const text = response.content?.filter(c => c.type === 'text').map(c => c.text).join('\n');
37
+ if (response.isError) throw new Error(text || 'Project upload failed');
38
+ return JSON.parse(text);
39
+ };
40
+ const begin = await call({ action: 'upload_begin', size, sha256: hash.digest('hex') });
41
+ uploadId = begin.uploadId;
42
+ const file = await open(bundlePath, 'r');
43
+ try {
44
+ let offset = 0;
45
+ while (offset < size) {
46
+ const bytes = Buffer.alloc(Math.min(begin.chunkBytes, size - offset));
47
+ const { bytesRead } = await file.read(bytes, 0, bytes.length, offset);
48
+ if (bytesRead !== bytes.length) throw new Error('Bundle changed during upload; re-export it.');
49
+ // Retrying an acknowledged range is checked byte-for-byte by the server.
50
+ let result;
51
+ for (let attempt = 0; attempt < 3; attempt++) {
52
+ try { result = await call({ action: 'upload_chunk', uploadId, offset, data: bytes.toString('base64') }); break; }
53
+ catch (error) { if (attempt === 2) throw error; }
54
+ }
55
+ if (result.offset !== offset + bytes.length) throw new Error('Unexpected upload offset');
56
+ offset = result.offset;
57
+ }
58
+ } finally { await file.close(); }
59
+ let result;
60
+ for (let attempt = 0; attempt < 3; attempt++) {
61
+ try { result = await call({ action: 'upload_finish', uploadId }); break; }
62
+ catch (error) { if (attempt === 2) throw error; }
63
+ }
64
+ console.log(JSON.stringify(result, null, 2));
65
+ await call({ action: 'upload_abort', uploadId }); // receipt no longer needed
66
+ uploadId = undefined;
67
+ } catch (error) {
68
+ if (uploadId) {
69
+ try { await client.callTool({ name: 'project', arguments: { action: 'upload_abort', projectKey, uploadId } }); } catch { }
70
+ }
71
+ // Do not print transport objects or request bodies, which contain the project capability.
72
+ console.error(`Project upload failed: ${String(error.message).split(projectKey).join('[redacted]')}`);
73
+ process.exitCode = 1;
74
+ } finally { await client.close(); }
@@ -0,0 +1,202 @@
1
+ /** Live MCP compatibility checks. Uses public downloads and an isolated database/cache. */
2
+ import assert from "node:assert/strict";
3
+ import { appendFile, copyFile, cp, mkdir, mkdtemp, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
9
+
10
+ const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
11
+ const arguments_ = process.argv.slice(2);
12
+ const runMods = !arguments_.length || arguments_.includes("--mods");
13
+ const selectedVersions = arguments_.filter(arg => arg !== "--mods");
14
+ const versions = selectedVersions.length ? selectedVersions : runMods && arguments_.length ? [] : [
15
+ "b1.7.3", "1.2.5", "1.5.2", "1.6.4", "1.7.10", "1.8.9", "1.12.2",
16
+ "1.13.2", "1.16.5", "1.18.2", "1.20.1", "1.20.6", "1.21.1", "26.1.2",
17
+ ];
18
+ const root = await mkdtemp(join(tmpdir(), "modlens-minecraft-eras-"));
19
+ const cache = join(root, "cache");
20
+ await mkdir(cache, { recursive: true });
21
+ if (process.env.MODLENS_TEST_ARTIFACT_CACHE) {
22
+ for (const directory of ["mc-jars", "mappings", "tools", "jdk"]) {
23
+ await cp(join(process.env.MODLENS_TEST_ARTIFACT_CACHE, directory), join(cache, directory), { recursive: true }).catch(error => {
24
+ if (error.code !== "ENOENT") throw error;
25
+ });
26
+ }
27
+ }
28
+ await copyFile(join(repository, "prisma/backends/template.db"), join(root, "database.db"));
29
+
30
+ // An explicit read-only seed can avoid re-downloading the analysis tools.
31
+ if (process.env.MODLENS_TEST_TOOL_CACHE) {
32
+ await mkdir(join(cache, "tools"), { recursive: true });
33
+ for (const name of ["mcsrc-indexer.jar", "vineflower.jar", "tiny-remapper.jar", "SpecialSource.jar"]) {
34
+ await copyFile(join(process.env.MODLENS_TEST_TOOL_CACHE, name), join(cache, "tools", name)).catch(error => {
35
+ if (error.code !== "ENOENT") throw error;
36
+ });
37
+ }
38
+ }
39
+ const environment = Object.fromEntries(Object.entries(process.env).filter(([key, value]) =>
40
+ typeof value === "string" && !/TOKEN|API_KEY|SECRET|^(MCP_|DATABASE_URL$|MODLENS_|OLLAMA_|AUTO_)/i.test(key),
41
+ ));
42
+ Object.assign(environment, {
43
+ DATABASE_URL: `file:${join(root, "database.db").replaceAll("\\", "/")}`,
44
+ MODLENS_HOME: join(root, "home"), MODLENS_CACHE_ROOT: cache,
45
+ MODLENS_AUTO_EMBED: "0", MODLENS_AUTO_GRAPH: "0", OLLAMA_URL: "http://127.0.0.1:1",
46
+ });
47
+ const client = new Client({ name: "minecraft-era-validation", version: "1.0" });
48
+ const transport = new StdioClientTransport({
49
+ command: process.execPath, args: [join(repository, "dist/server.js")],
50
+ cwd: root, env: environment, stderr: "pipe",
51
+ });
52
+ const errors = [];
53
+ const results = [];
54
+ let serverLog = "";
55
+ transport.stderr?.on("data", data => { serverLog += data; });
56
+ console.log(`Evidence: ${root}`);
57
+
58
+ async function call(name, args, validate, expectError = false) {
59
+ const start = Date.now();
60
+ const result = { name, args };
61
+ try {
62
+ const response = await client.callTool({ name, arguments: args }, undefined, { timeout: 300_000 });
63
+ const text = response.content.filter(item => item.type === "text").map(item => item.text).join("\n");
64
+ let data;
65
+ try { data = JSON.parse(text); } catch { data = text; }
66
+ if (args.action === "search_class" && typeof data === "string") data = data.split("\n").filter(Boolean);
67
+ assert.equal(response.isError === true, expectError, text);
68
+ validate?.(data);
69
+ result.passed = true;
70
+ return data;
71
+ } catch (error) {
72
+ result.passed = false;
73
+ result.error = error.message;
74
+ throw error;
75
+ } finally {
76
+ result.elapsedMs = Date.now() - start;
77
+ results.push(result);
78
+ await appendFile(join(root, "results.jsonl"), JSON.stringify(result) + "\n");
79
+ console.log(`${result.passed ? "PASS" : "FAIL"} ${args.version ?? ""} ${name}.${args.action} (${result.elapsedMs}ms)${result.error ? `: ${result.error.slice(0, 220)}` : ""}`);
80
+ }
81
+ }
82
+
83
+ try {
84
+ await client.connect(transport);
85
+ await call("mc_versions", { action: "list_mc", type: "all" }, rows => {
86
+ for (const version of versions) assert.ok(rows.some(row => row.id === version), `Version list omits ${version}`);
87
+ }).catch(error => errors.push({ version: "catalog", error: error.message }));
88
+ for (const version of versions) {
89
+ try {
90
+ const classes = await call("mc_source", { action: "search_class", version, query: "Minecraft" }, rows => {
91
+ assert.ok(rows.some(name => /(?:^|\/)Minecraft$/.test(name)), "No named Minecraft class found");
92
+ });
93
+ const className = classes.find(name => /(?:^|\/)Minecraft$/.test(name));
94
+ await call("mc_source", { action: "get_source", version, className, maxLines: 20 }, text => {
95
+ assert.equal(typeof text, "string");
96
+ assert.match(text, /(?:package|import) net\.minecraft\./);
97
+ });
98
+ await call("mc_source", { action: "class_members", version, className }, data => {
99
+ assert.ok(data.methods.length > 5, "Missing class methods");
100
+ assert.ok(data.fields.length > 5, "Missing class fields");
101
+ });
102
+ await call("mc_source", { action: "bytecode", version, className }, text => {
103
+ assert.equal(typeof text, "string");
104
+ assert.match(text, /Minecraft/);
105
+ assert.match(text, /descriptor:/);
106
+ });
107
+ await call("mc_source", { action: "inheritance", version, className }, data => {
108
+ assert.equal(data.className, className);
109
+ assert.ok(data.superClass);
110
+ });
111
+ if (version === "1.5.2") {
112
+ for (const [className, method] of [
113
+ ["net/minecraft/server/MinecraftServer", "tick"],
114
+ ["net/minecraft/src/IntegratedServer", "tickIntegrated"],
115
+ ]) await call("mc_source", { action: "class_members", version, className }, data => {
116
+ assert.ok(data.methods.some(entry => entry.name === method), `Lost explicit mapping for ${className}.${method}`);
117
+ });
118
+ }
119
+ } catch (error) {
120
+ errors.push({ version, error: error.message });
121
+ }
122
+ }
123
+ if (runMods) {
124
+ await call("mod", { action: "get" }, text => assert.match(text, /modId or dbId is required/), true);
125
+ const mods = [
126
+ { project: 73488, version: "1.6.4", loader: "forge", name: "Waila" },
127
+ { project: 73488, version: "1.7.10", loader: "forge", name: "Waila" },
128
+ { project: 238222, version: "1.8.9", loader: "forge", name: "JEI" },
129
+ { project: 238222, version: "1.12.2", loader: "forge", name: "JEI" },
130
+ { project: 238222, version: "1.16.5", loader: "forge", name: "JEI" },
131
+ { project: "AANobbMI", version: "1.16.5", loader: "fabric", name: "Sodium" },
132
+ { project: 238222, version: "1.20.1", loader: "forge", name: "JEI" },
133
+ { project: 238222, version: "1.21.1", loader: "neoforge", name: "JEI" },
134
+ ];
135
+ for (const mod of mods) {
136
+ try {
137
+ const infoArgs = { modId: mod.project, mcVersionFilter: mod.version, loader: mod.loader };
138
+ const before = await call("mod", { action: "stats" });
139
+ const info = await call("modpacks_ch", { action: "mod_info", ...infoArgs }, data => {
140
+ assert.ok(data.versions.length, `No ${mod.name} versions for ${mod.version}/${mod.loader}`);
141
+ assert.equal(data.versionLimit, 20);
142
+ assert.ok(data.versions.length <= data.versionLimit);
143
+ assert.ok(data.versions.every(version => version.loaders.includes(mod.loader)), "Accepted an unverified loader");
144
+ });
145
+ const compatibilityAlias = await call("modpacks_ch", { action: "ftb_mod_info", ...infoArgs });
146
+ assert.deepEqual(compatibilityAlias, info);
147
+ if (mod.name === "Waila" && mod.version === "1.7.10") {
148
+ await call("platform", { action: "search", query: "Waila", mcVersion: mod.version, loader: "forge", limit: 5 }, data => {
149
+ assert.ok(data.results.some(hit => String(hit.projectId) === String(mod.project)), "Loader-filtered search omitted Waila");
150
+ assert.ok(data.results.every(hit => hit.loaders.includes("forge")), "Search returned an unverified loader");
151
+ });
152
+ await call("modpacks_ch", { action: "mod_info", ...infoArgs, loader: "fabric", limit: 1 }, data => {
153
+ assert.deepEqual(data.versions, [], "Accepted a Forge-only release for Fabric");
154
+ });
155
+ }
156
+ await call("mod", { action: "stats" }, data => assert.equal(data.total, before.total, "Metadata inspection ingested candidate files"));
157
+ const downloaded = await call("modpacks_ch", { action: "download_mod", ...infoArgs }, data => {
158
+ assert.ok(data.modId > 0, "Download did not produce an ingested mod");
159
+ });
160
+ const dbId = downloaded.modId;
161
+ await call("mod", { action: "get", dbId }, data => {
162
+ assert.equal(data.id, dbId, "Returned the wrong database record");
163
+ assert.equal(data.loader, mod.loader);
164
+ assert.ok(data.modId && data.modId !== "unknown");
165
+ assert.ok(data.version && !data.version.includes("${"), "Unresolved version placeholder");
166
+ });
167
+ const files = await call("mod_jar", { action: "list_files", modId: dbId });
168
+ const englishPath = files.entries.find(path => /(?:^|\/)lang\/en_us\.(json|lang)$/i.test(path));
169
+ const languageFile = englishPath ? await call("mod_jar", { action: "get_file", modId: dbId, path: englishPath }) : undefined;
170
+ const emptyLanguage = languageFile && (languageFile.data
171
+ ? Object.keys(languageFile.data).length === 0 : !languageFile.raw?.trim());
172
+ await call("mod_jar", { action: "lang", modId: dbId }, data => {
173
+ if (emptyLanguage) assert.equal(data.total, 0, "Invented translations for an empty language file");
174
+ else if (englishPath) assert.ok(data.total > 0, "Existing language file was not read");
175
+ else assert.equal(data.found, false, "Reported language entries for a JAR without English translations");
176
+ });
177
+ await call("mod", { action: "reindex", dbId });
178
+ const query = mod.name === "JEI" ? "JustEnoughItems" : mod.name;
179
+ const classes = await call("mod_bytecode", { action: "search_class", dbId, query });
180
+ const className = classes.find(name => !name.includes("$"));
181
+ assert.ok(className, "No matching mod class found");
182
+ await call("mod_bytecode", { action: "class_members", dbId, className }, data => {
183
+ assert.equal(data.className, className);
184
+ assert.ok(data.methods.length, "No methods returned for mod class");
185
+ });
186
+ await call("mod_bytecode", { action: "bytecode", dbId, className }, text => assert.match(text, /descriptor:/));
187
+ await call("mod", { action: "decompile_class", dbId, className }, text => {
188
+ assert.match(text, /package /);
189
+ assert.ok(text.includes(className.split("/").at(-1)), "Decompiled the wrong class");
190
+ });
191
+ } catch (error) {
192
+ errors.push({ version: `${mod.name} ${mod.version}/${mod.loader}`, error: error.message });
193
+ }
194
+ }
195
+ }
196
+ } finally {
197
+ await client.close();
198
+ await writeFile(join(root, "server.log"), serverLog);
199
+ await writeFile(join(root, "summary.json"), JSON.stringify({ versions, results, errors }, null, 2));
200
+ }
201
+ console.log(`${results.filter(row => row.passed).length}/${results.length} checks passed; ${errors.length} failing versions/catalogs. Evidence: ${root}`);
202
+ if (errors.length) process.exitCode = 1;
@@ -0,0 +1,217 @@
1
+ // Exercise the packed CLI as an npx consumer, outside the source checkout.
2
+ // Run after `npm run build`, or pass a tarball / release-artifact directory.
3
+ import assert from "node:assert/strict";
4
+ import { spawn, execFileSync } from "node:child_process";
5
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
6
+ import { createRequire } from "node:module";
7
+ import { tmpdir } from "node:os";
8
+ import { dirname, join, resolve, delimiter } from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+
11
+ const root = mkdtempSync(join(tmpdir(), "modlens-package-test-"));
12
+ const consumer = join(root, "consumer");
13
+ mkdirSync(consumer);
14
+ writeFileSync(join(consumer, "package.json"), JSON.stringify({ name: "package-smoke-consumer", private: true }));
15
+ writeFileSync(join(root, "npmrc"), "");
16
+ const npm = process.env.npm_execpath ?? (process.platform === "win32"
17
+ ? join(dirname(process.execPath), "node_modules/npm/bin/npm-cli.js")
18
+ : realpathSync(join(dirname(process.execPath), "npm")));
19
+ assert.ok(existsSync(npm), "Run this test via npm run test:package");
20
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) =>
21
+ !/^(DATABASE_URL|MODLENS_.*|MCP_PORT|DEBUG|NODE_OPTIONS|NODE_PATH|PATH)$/i.test(key)));
22
+ Object.assign(env, {
23
+ PATH: `${dirname(process.execPath)}${delimiter}${process.env.PATH ?? process.env.Path ?? ""}`,
24
+ npm_config_cache: join(root, "cache"),
25
+ npm_config_userconfig: join(root, "npmrc"),
26
+ npm_config_update_notifier: "false",
27
+ npm_config_audit: "false",
28
+ npm_config_fund: "false",
29
+ MODLENS_AUTO_EMBED: "0",
30
+ MODLENS_CACHE_ROOT: join(root, "modlens-cache"),
31
+ });
32
+
33
+ function start(args, home) {
34
+ const child = spawn(process.execPath, [npm, ...args], {
35
+ cwd: consumer, env: { ...env, MODLENS_HOME: home }, stdio: ["pipe", "pipe", "pipe"],
36
+ detached: process.platform !== "win32",
37
+ });
38
+ let output = "";
39
+ child.stdout.on("data", data => { output += data; });
40
+ child.stderr.on("data", data => { output += data; });
41
+ const done = new Promise((resolveDone, reject) => {
42
+ child.on("error", reject);
43
+ child.on("close", (code, signal) => resolveDone({ code, signal }));
44
+ });
45
+ return { child, done, output: () => output };
46
+ }
47
+
48
+ async function stop(run) {
49
+ if (run.child.exitCode !== null || run.child.signalCode !== null) return;
50
+ if (process.platform === "win32") {
51
+ // Only terminate the process tree created by this test.
52
+ try { execFileSync("taskkill.exe", ["/pid", String(run.child.pid), "/T", "/F"], { stdio: "ignore" }); } catch {}
53
+ } else {
54
+ try { process.kill(-run.child.pid, "SIGTERM"); } catch (error) {
55
+ if (error.code !== "ESRCH") throw error;
56
+ }
57
+ }
58
+ await deadline(run.done, 10_000, "test process shutdown");
59
+ }
60
+
61
+ async function deadline(promise, ms, label) {
62
+ let timer;
63
+ try {
64
+ return await Promise.race([promise, new Promise((_, reject) => {
65
+ timer = setTimeout(() => reject(new Error(`Timed out: ${label}`)), ms);
66
+ })]);
67
+ } finally { clearTimeout(timer); }
68
+ }
69
+
70
+ let passed = false;
71
+ try {
72
+ let tarball;
73
+ if (process.argv[2]) {
74
+ tarball = resolve(process.argv[2]);
75
+ if (statSync(tarball).isDirectory()) {
76
+ const files = readdirSync(tarball).filter(file => file.endsWith(".tgz"));
77
+ assert.equal(files.length, 1, "Expected one package tarball");
78
+ tarball = join(tarball, files[0]);
79
+ }
80
+ } else {
81
+ const pack = JSON.parse(execFileSync(process.execPath, [npm, "pack", "--json", "--pack-destination", root], {
82
+ env, encoding: "utf8", timeout: 60_000,
83
+ }));
84
+ tarball = join(root, pack[0].filename);
85
+ }
86
+ console.log(`Testing ${tarball} on ${process.version} (${process.platform}/${process.arch})`);
87
+ const args = ["exec", "--yes", "--foreground-scripts", "--package", tarball, "--", "modlens-mcp"];
88
+
89
+ const setup = start([...args, "--setup"], join(root, "setup-home"));
90
+ try {
91
+ await deadline(new Promise((resolvePrompt, reject) => {
92
+ const inspect = () => {
93
+ if (setup.output().includes("Choose a setup profile")) resolvePrompt();
94
+ };
95
+ setup.child.stdout.on("data", inspect);
96
+ setup.child.stderr.on("data", inspect);
97
+ setup.done.then(result => reject(new Error(`Setup exited before prompting: ${JSON.stringify(result)}\n${setup.output()}`)), reject);
98
+ }), 180_000, "fresh npx install and setup prompt");
99
+ setup.child.stdin.write("\x03"); // Cancel the wizard before saving configuration.
100
+ assert.equal((await deadline(setup.done, 15_000, "setup cancellation")).code, 0, setup.output());
101
+ console.log("PASS: fresh npx install and interactive setup prompt");
102
+ } catch (error) {
103
+ writeFileSync(join(root, "setup.log"), setup.output());
104
+ throw error;
105
+ } finally { await stop(setup); }
106
+
107
+ const installs = readdirSync(join(root, "cache/_npx"))
108
+ .map(dir => join(root, "cache/_npx", dir, "node_modules/@creeperhost/modlens-mcp"))
109
+ .filter(dir => existsSync(join(dir, "package.json")));
110
+ assert.equal(installs.length, 1);
111
+ const installed = installs[0];
112
+ const initScript = execFileSync(process.execPath, [join(installed, "dist/launcher.js"), "--gradle-init-script"], { env, encoding: "utf8" }).trim();
113
+ assert.equal(initScript, join(installed, "scripts/gradle/modlens.init.gradle"));
114
+ assert.ok(statSync(initScript).size > 0, "Gradle exporter must ship in the npm package");
115
+ assert.ok(statSync(join(installed, "scripts/project-upload.mjs")).size > 0, "Remote uploader must ship in the npm package");
116
+ const keyFile = join(root, "project-key.txt");
117
+ writeFileSync(keyFile, "a".repeat(64));
118
+ assert.deepEqual(JSON.parse(execFileSync(process.execPath, [join(installed, "dist/launcher.js"), "--project", "list", `--key-file=${keyFile}`], { env, encoding: "utf8" })), []);
119
+ const require = createRequire(join(installed, "package.json"));
120
+ const adapterRequire = createRequire(require.resolve("@prisma/adapter-better-sqlite3"));
121
+ assert.equal(require.resolve("better-sqlite3"), adapterRequire.resolve("better-sqlite3"), "Prisma must share the v12 driver");
122
+ assert.match(require("better-sqlite3/package.json").version, /^12\./);
123
+
124
+ const dataHome = join(root, "server-home");
125
+ async function checkServer() {
126
+ const run = start(args, dataHome);
127
+ const pending = new Map();
128
+ let buffer = "";
129
+ let id = 0;
130
+ run.child.stdout.on("data", data => {
131
+ buffer += data;
132
+ while (buffer.includes("\n")) {
133
+ const newline = buffer.indexOf("\n");
134
+ const line = buffer.slice(0, newline).trim();
135
+ buffer = buffer.slice(newline + 1);
136
+ if (!line) continue;
137
+ try {
138
+ const message = JSON.parse(line);
139
+ const waiter = pending.get(message.id);
140
+ if (waiter) {
141
+ pending.delete(message.id);
142
+ if (message.error) waiter.reject(new Error(JSON.stringify(message.error)));
143
+ else waiter.resolve(message.result);
144
+ }
145
+ } catch (error) { for (const waiter of pending.values()) waiter.reject(error); }
146
+ }
147
+ });
148
+ run.done.then(result => {
149
+ for (const waiter of pending.values()) waiter.reject(new Error(`MCP exited: ${JSON.stringify(result)}\n${run.output()}`));
150
+ });
151
+ function send(message) { run.child.stdin.write(JSON.stringify({ jsonrpc: "2.0", ...message }) + "\n"); }
152
+ function request(method, params) {
153
+ return deadline(new Promise((resolveRequest, reject) => {
154
+ const requestId = ++id;
155
+ pending.set(requestId, { resolve: resolveRequest, reject });
156
+ send({ id: requestId, method, params });
157
+ }), 30_000, method);
158
+ }
159
+ try {
160
+ const initialized = await request("initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "package-smoke", version: "1.0.0" } });
161
+ assert.ok(initialized.serverInfo);
162
+ send({ method: "notifications/initialized" });
163
+ const list = await request("tools/list", {});
164
+ assert.ok(list.tools.some(tool => tool.name === "mod"));
165
+ assert.ok(list.tools.some(tool => tool.name === "project"));
166
+ const stats = await request("tools/call", { name: "mod", arguments: { action: "stats" } });
167
+ assert.ok(!stats.isError, JSON.stringify(stats));
168
+ assert.equal(JSON.parse(stats.content.find(item => item.type === "text").text).total, 0);
169
+ return run.output();
170
+ } finally { await stop(run); }
171
+ }
172
+
173
+ const first = await checkServer();
174
+ assert.match(first, /First run/);
175
+ const database = join(dataHome, "data/modlens.db");
176
+ assert.ok(statSync(database).size > 0);
177
+ console.log("PASS: first-run SQLite bootstrap, MCP handshake, tools/list, database stats");
178
+
179
+ // Check the actual installed client/adapter with dates, BLOBs, and transactions.
180
+ // Use a child so Windows releases the native .node file before cleanup.
181
+ function checkDatabase(mode) {
182
+ execFileSync(process.execPath, ["--input-type=module", "-e", `
183
+ import assert from 'node:assert/strict';
184
+ const { getDb, disconnect } = await import(process.argv[1]);
185
+ const db = await getDb();
186
+ try {
187
+ const bytes = new Uint8Array([0, 1, 127, 128, 255]);
188
+ if (process.argv[2] === 'create') {
189
+ const doc = await db.docEntry.create({ data: { title: 'Package smoke', url: 'https://example.invalid', embedding: bytes } });
190
+ assert.deepEqual(doc.embedding, bytes);
191
+ assert.ok(doc.createdAt instanceof Date);
192
+ await db.$transaction(async tx => { await tx.docEntry.update({ where: { id: doc.id }, data: { summary: 'committed' } }); });
193
+ await assert.rejects(db.$transaction(async tx => {
194
+ await tx.docEntry.update({ where: { id: doc.id }, data: { summary: 'rolled back' } });
195
+ throw new Error('rollback sentinel');
196
+ }), /rollback sentinel/);
197
+ }
198
+ const saved = await db.docEntry.findFirstOrThrow({ where: { title: 'Package smoke' } });
199
+ assert.equal(saved.summary, 'committed');
200
+ assert.deepEqual(saved.embedding, bytes);
201
+ } finally { await disconnect(); }
202
+ `, pathToFileURL(join(installed, "dist/db.js")).href, mode], {
203
+ cwd: consumer, env: { ...env, DATABASE_URL: `file:${database}` }, timeout: 30_000, stdio: "pipe",
204
+ });
205
+ }
206
+ checkDatabase("create");
207
+ console.log("PASS: installed Prisma client/adapter CRUD, binary data, dates, commit and rollback");
208
+
209
+ const second = await checkServer();
210
+ assert.doesNotMatch(second, /First run/);
211
+ checkDatabase("read");
212
+ console.log("PASS: second npx run preserves existing database contents");
213
+ passed = true;
214
+ } finally {
215
+ if (passed) rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 300 });
216
+ else console.error(`Package test artifacts retained at ${root}`);
217
+ }