diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index c73cd576..147efe37 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -273,6 +273,14 @@ jobs: if [ ! -f "${target_dir}package.json" ]; then continue fi + if [ ! -f "${target_dir}noop.cjs" ]; then + echo "ERROR: missing ${target_dir}noop.cjs" + exit 1 + fi + if [ ! -f "${target_dir}noop.d.ts" ]; then + echo "ERROR: missing ${target_dir}noop.d.ts" + exit 1 + fi actual_count=$(find "${target_dir}" -maxdepth 1 -type f -name '*.node' | wc -l | tr -d ' ') if [ "$actual_count" -ne "$expected_count" ]; then echo "ERROR: expected ${expected_count} .node files in ${target_dir}, found ${actual_count}" diff --git a/Cargo.lock b/Cargo.lock index 78eb33a6..64f8a95e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2071,7 +2071,7 @@ dependencies = [ [[package]] name = "memory-sync-gui" -version = "2026.10318.11638" +version = "2026.10318.12034" dependencies = [ "dirs", "proptest", @@ -4439,7 +4439,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tnmsc" -version = "2026.10318.11638" +version = "2026.10318.12034" dependencies = [ "clap", "dirs", @@ -4459,7 +4459,7 @@ dependencies = [ [[package]] name = "tnmsc-logger" -version = "2026.10318.11638" +version = "2026.10318.12034" dependencies = [ "chrono", "napi", @@ -4471,7 +4471,7 @@ dependencies = [ [[package]] name = "tnmsc-md-compiler" -version = "2026.10318.11638" +version = "2026.10318.12034" dependencies = [ "markdown", "napi", @@ -4486,7 +4486,7 @@ dependencies = [ [[package]] name = "tnmsc-script-runtime" -version = "2026.10318.11638" +version = "2026.10318.12034" dependencies = [ "napi", "napi-build", diff --git a/Cargo.toml b/Cargo.toml index 108663df..a3301ccb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2026.10318.11638" +version = "2026.10318.12034" edition = "2024" license = "AGPL-3.0-only" authors = ["TrueNine"] diff --git a/cli/npm/.gitignore b/cli/npm/.gitignore index a85693ee..d98927ce 100644 --- a/cli/npm/.gitignore +++ b/cli/npm/.gitignore @@ -2,3 +2,5 @@ !.gitignore !*/ !*/package.json +!*/noop.cjs +!*/noop.d.ts diff --git a/cli/npm/darwin-arm64/noop.cjs b/cli/npm/darwin-arm64/noop.cjs new file mode 100644 index 00000000..84c0933b --- /dev/null +++ b/cli/npm/darwin-arm64/noop.cjs @@ -0,0 +1,28 @@ +'use strict' + +const {readdirSync} = require('node:fs') +const {join} = require('node:path') + +const EXPORT_BINDINGS = [ + ['logger', 'napi-logger.'], + ['mdCompiler', 'napi-md-compiler.'], + ['scriptRuntime', 'napi-script-runtime.'], + ['config', 'napi-memory-sync-cli.'] +] + +const nodeFiles = readdirSync(__dirname).filter(file => file.endsWith('.node')) +const bindings = {} + +for (const [exportName, prefix] of EXPORT_BINDINGS) { + const file = nodeFiles.find(candidate => candidate.startsWith(prefix)) + if (file == null) continue + + Object.defineProperty(bindings, exportName, { + enumerable: true, + get() { + return require(join(__dirname, file)) + } + }) +} + +module.exports = bindings diff --git a/cli/npm/darwin-arm64/noop.d.ts b/cli/npm/darwin-arm64/noop.d.ts new file mode 100644 index 00000000..667d20dc --- /dev/null +++ b/cli/npm/darwin-arm64/noop.d.ts @@ -0,0 +1,8 @@ +declare const bindings: { + readonly logger?: unknown + readonly mdCompiler?: unknown + readonly scriptRuntime?: unknown + readonly config?: unknown +} + +export = bindings diff --git a/cli/npm/darwin-arm64/package.json b/cli/npm/darwin-arm64/package.json index 7323e266..cea43f2d 100644 --- a/cli/npm/darwin-arm64/package.json +++ b/cli/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-cli-darwin-arm64", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "os": [ "darwin" ], diff --git a/cli/npm/darwin-x64/noop.cjs b/cli/npm/darwin-x64/noop.cjs new file mode 100644 index 00000000..84c0933b --- /dev/null +++ b/cli/npm/darwin-x64/noop.cjs @@ -0,0 +1,28 @@ +'use strict' + +const {readdirSync} = require('node:fs') +const {join} = require('node:path') + +const EXPORT_BINDINGS = [ + ['logger', 'napi-logger.'], + ['mdCompiler', 'napi-md-compiler.'], + ['scriptRuntime', 'napi-script-runtime.'], + ['config', 'napi-memory-sync-cli.'] +] + +const nodeFiles = readdirSync(__dirname).filter(file => file.endsWith('.node')) +const bindings = {} + +for (const [exportName, prefix] of EXPORT_BINDINGS) { + const file = nodeFiles.find(candidate => candidate.startsWith(prefix)) + if (file == null) continue + + Object.defineProperty(bindings, exportName, { + enumerable: true, + get() { + return require(join(__dirname, file)) + } + }) +} + +module.exports = bindings diff --git a/cli/npm/darwin-x64/noop.d.ts b/cli/npm/darwin-x64/noop.d.ts new file mode 100644 index 00000000..667d20dc --- /dev/null +++ b/cli/npm/darwin-x64/noop.d.ts @@ -0,0 +1,8 @@ +declare const bindings: { + readonly logger?: unknown + readonly mdCompiler?: unknown + readonly scriptRuntime?: unknown + readonly config?: unknown +} + +export = bindings diff --git a/cli/npm/darwin-x64/package.json b/cli/npm/darwin-x64/package.json index 5134b4e9..f0295e7f 100644 --- a/cli/npm/darwin-x64/package.json +++ b/cli/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-cli-darwin-x64", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "os": [ "darwin" ], diff --git a/cli/npm/linux-arm64-gnu/noop.cjs b/cli/npm/linux-arm64-gnu/noop.cjs new file mode 100644 index 00000000..84c0933b --- /dev/null +++ b/cli/npm/linux-arm64-gnu/noop.cjs @@ -0,0 +1,28 @@ +'use strict' + +const {readdirSync} = require('node:fs') +const {join} = require('node:path') + +const EXPORT_BINDINGS = [ + ['logger', 'napi-logger.'], + ['mdCompiler', 'napi-md-compiler.'], + ['scriptRuntime', 'napi-script-runtime.'], + ['config', 'napi-memory-sync-cli.'] +] + +const nodeFiles = readdirSync(__dirname).filter(file => file.endsWith('.node')) +const bindings = {} + +for (const [exportName, prefix] of EXPORT_BINDINGS) { + const file = nodeFiles.find(candidate => candidate.startsWith(prefix)) + if (file == null) continue + + Object.defineProperty(bindings, exportName, { + enumerable: true, + get() { + return require(join(__dirname, file)) + } + }) +} + +module.exports = bindings diff --git a/cli/npm/linux-arm64-gnu/noop.d.ts b/cli/npm/linux-arm64-gnu/noop.d.ts new file mode 100644 index 00000000..667d20dc --- /dev/null +++ b/cli/npm/linux-arm64-gnu/noop.d.ts @@ -0,0 +1,8 @@ +declare const bindings: { + readonly logger?: unknown + readonly mdCompiler?: unknown + readonly scriptRuntime?: unknown + readonly config?: unknown +} + +export = bindings diff --git a/cli/npm/linux-arm64-gnu/package.json b/cli/npm/linux-arm64-gnu/package.json index 42c155af..76718c77 100644 --- a/cli/npm/linux-arm64-gnu/package.json +++ b/cli/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-cli-linux-arm64-gnu", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "os": [ "linux" ], diff --git a/cli/npm/linux-x64-gnu/noop.cjs b/cli/npm/linux-x64-gnu/noop.cjs new file mode 100644 index 00000000..84c0933b --- /dev/null +++ b/cli/npm/linux-x64-gnu/noop.cjs @@ -0,0 +1,28 @@ +'use strict' + +const {readdirSync} = require('node:fs') +const {join} = require('node:path') + +const EXPORT_BINDINGS = [ + ['logger', 'napi-logger.'], + ['mdCompiler', 'napi-md-compiler.'], + ['scriptRuntime', 'napi-script-runtime.'], + ['config', 'napi-memory-sync-cli.'] +] + +const nodeFiles = readdirSync(__dirname).filter(file => file.endsWith('.node')) +const bindings = {} + +for (const [exportName, prefix] of EXPORT_BINDINGS) { + const file = nodeFiles.find(candidate => candidate.startsWith(prefix)) + if (file == null) continue + + Object.defineProperty(bindings, exportName, { + enumerable: true, + get() { + return require(join(__dirname, file)) + } + }) +} + +module.exports = bindings diff --git a/cli/npm/linux-x64-gnu/noop.d.ts b/cli/npm/linux-x64-gnu/noop.d.ts new file mode 100644 index 00000000..667d20dc --- /dev/null +++ b/cli/npm/linux-x64-gnu/noop.d.ts @@ -0,0 +1,8 @@ +declare const bindings: { + readonly logger?: unknown + readonly mdCompiler?: unknown + readonly scriptRuntime?: unknown + readonly config?: unknown +} + +export = bindings diff --git a/cli/npm/linux-x64-gnu/package.json b/cli/npm/linux-x64-gnu/package.json index 49f72b9e..4d7d4b9b 100644 --- a/cli/npm/linux-x64-gnu/package.json +++ b/cli/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-cli-linux-x64-gnu", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "os": [ "linux" ], diff --git a/cli/npm/win32-x64-msvc/noop.cjs b/cli/npm/win32-x64-msvc/noop.cjs new file mode 100644 index 00000000..84c0933b --- /dev/null +++ b/cli/npm/win32-x64-msvc/noop.cjs @@ -0,0 +1,28 @@ +'use strict' + +const {readdirSync} = require('node:fs') +const {join} = require('node:path') + +const EXPORT_BINDINGS = [ + ['logger', 'napi-logger.'], + ['mdCompiler', 'napi-md-compiler.'], + ['scriptRuntime', 'napi-script-runtime.'], + ['config', 'napi-memory-sync-cli.'] +] + +const nodeFiles = readdirSync(__dirname).filter(file => file.endsWith('.node')) +const bindings = {} + +for (const [exportName, prefix] of EXPORT_BINDINGS) { + const file = nodeFiles.find(candidate => candidate.startsWith(prefix)) + if (file == null) continue + + Object.defineProperty(bindings, exportName, { + enumerable: true, + get() { + return require(join(__dirname, file)) + } + }) +} + +module.exports = bindings diff --git a/cli/npm/win32-x64-msvc/noop.d.ts b/cli/npm/win32-x64-msvc/noop.d.ts new file mode 100644 index 00000000..667d20dc --- /dev/null +++ b/cli/npm/win32-x64-msvc/noop.d.ts @@ -0,0 +1,8 @@ +declare const bindings: { + readonly logger?: unknown + readonly mdCompiler?: unknown + readonly scriptRuntime?: unknown + readonly config?: unknown +} + +export = bindings diff --git a/cli/npm/win32-x64-msvc/package.json b/cli/npm/win32-x64-msvc/package.json index fb0bec76..315f7f9b 100644 --- a/cli/npm/win32-x64-msvc/package.json +++ b/cli/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-cli-win32-x64-msvc", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "os": [ "win32" ], diff --git a/cli/package.json b/cli/package.json index 607118a6..d9b3c7b6 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,7 +1,7 @@ { "name": "@truenine/memory-sync-cli", "type": "module", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "description": "TrueNine Memory Synchronization CLI", "author": "TrueNine", "license": "AGPL-3.0-only", @@ -51,7 +51,7 @@ "registry": "https://registry.npmjs.org/" }, "scripts": { - "build": "run-s build:deps build:napi bundle generate:schema check", + "build": "run-s build:deps build:napi bundle write:main-wrapper generate:schema check", "build:napi": "tsx ../scripts/copy-napi.ts", "build:deps": "pnpm -F @truenine/logger -F @truenine/md-compiler -F @truenine/script-runtime run build", "bundle": "tsx ../scripts/build-quiet.ts", @@ -62,7 +62,8 @@ "test": "run-s build:deps test:run", "test:run": "vitest run", "lintfix": "eslint --fix --cache .", - "typecheck": "tsc --noEmit -p tsconfig.lib.json" + "typecheck": "tsc --noEmit -p tsconfig.lib.json", + "write:main-wrapper": "tsx scripts/write-main-wrapper.ts" }, "dependencies": { "json5": "catalog:", diff --git a/cli/scripts/write-main-wrapper.ts b/cli/scripts/write-main-wrapper.ts new file mode 100644 index 00000000..38748fe6 --- /dev/null +++ b/cli/scripts/write-main-wrapper.ts @@ -0,0 +1,174 @@ +import {spawnSync} from 'node:child_process' +import {chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, writeFileSync} from 'node:fs' +import {tmpdir} from 'node:os' +import {dirname, join, resolve} from 'node:path' +import {fileURLToPath, pathToFileURL} from 'node:url' + +const scriptDir = dirname(fileURLToPath(import.meta.url)) +const cliDir = resolve(scriptDir, '..') +const distDir = resolve(cliDir, 'dist') +const indexEntryPath = resolve(distDir, 'index.mjs') +const mainWrapperPath = resolve(distDir, 'main.mjs') +const bundledJitiBabelRuntimeSourcePath = resolve(cliDir, 'node_modules', 'jiti', 'dist', 'babel.cjs') +const bundledJitiBabelRuntimeTargetPath = resolve(distDir, 'babel.cjs') + +const SHEBANG = '#!/usr/bin/env node' + +const MAIN_WRAPPER_CONTENT = [ + SHEBANG, + "import process from 'node:process'", + "import {runCli} from './index.mjs'", + '', + 'void runCli(process.argv).then(exitCode => process.exit(exitCode))', + '' +].join('\n') + +function ensureIndexBundleExists(): void { + if (existsSync(indexEntryPath)) { + return + } + + throw new Error(`Expected bundled CLI entry at "${indexEntryPath}" before writing main wrapper.`) +} + +function writeMainWrapper(): void { + mkdirSync(distDir, {recursive: true}) + writeFileSync(mainWrapperPath, MAIN_WRAPPER_CONTENT, 'utf8') + + if (process.platform !== 'win32') { + chmodSync(mainWrapperPath, 0o755) + } +} + +function findBundledJitiChunkPath(): string | undefined { + const bundledJitiChunkName = readdirSync(distDir) + .find(fileName => /^jiti-.*\.mjs$/u.test(fileName)) + + return bundledJitiChunkName == null ? void 0 : resolve(distDir, bundledJitiChunkName) +} + +function ensureBundledJitiRuntimeAssets(): string | undefined { + const bundledJitiChunkPath = findBundledJitiChunkPath() + if (bundledJitiChunkPath == null) return void 0 + + if (!existsSync(bundledJitiBabelRuntimeSourcePath)) { + throw new Error( + `Bundled jiti chunk "${bundledJitiChunkPath}" requires "${bundledJitiBabelRuntimeSourcePath}", but it does not exist.` + ) + } + + copyFileSync(bundledJitiBabelRuntimeSourcePath, bundledJitiBabelRuntimeTargetPath) + return bundledJitiChunkPath +} + +function assertMainWrapperSize(): void { + const wrapperStats = statSync(mainWrapperPath) + + if (wrapperStats.size <= `${SHEBANG}\n`.length) { + throw new Error(`Generated "${mainWrapperPath}" is unexpectedly small (${wrapperStats.size} bytes).`) + } +} + +function smokeTestBundledJitiTransform(bundledJitiChunkPath: string | undefined): void { + if (bundledJitiChunkPath == null) return + + const tempDir = mkdtempSync(join(tmpdir(), 'tnmsc-bundled-jiti-')) + const probeModulePath = join(tempDir, 'probe.ts') + const probeRunnerPath = join(tempDir, 'probe-runner.mjs') + + writeFileSync(probeModulePath, 'export default {ok: true}\n', 'utf8') + writeFileSync(probeRunnerPath, [ + "import process from 'node:process'", + "import {pathToFileURL} from 'node:url'", + '', + 'const [, , bundledJitiChunkPathArg, probeModulePathArg] = process.argv', + '', + 'const {createJiti} = await import(pathToFileURL(bundledJitiChunkPathArg).href)', + 'const runtime = createJiti(import.meta.url, {', + ' fsCache: false,', + ' moduleCache: false,', + ' interopDefault: false', + '})', + 'const loaded = await runtime.import(probeModulePathArg)', + '', + 'if (loaded.default?.ok !== true) {', + " throw new Error('Bundled jiti smoke test loaded an unexpected module shape.')", + '}', + '' + ].join('\n'), 'utf8') + + const smokeTest = (() => { + try { + return spawnSync(process.execPath, [probeRunnerPath, bundledJitiChunkPath, probeModulePath], { + cwd: cliDir, + encoding: 'utf8' + }) + } + finally { + rmSync(tempDir, {recursive: true, force: true}) + } + })() + + if (smokeTest.error != null) { + throw smokeTest.error + } + + const combinedOutput = `${smokeTest.stdout ?? ''}${smokeTest.stderr ?? ''}`.trim() + + if (smokeTest.status !== 0) { + throw new Error([ + `Bundled jiti chunk "${pathToFileURL(bundledJitiChunkPath).href}" failed the transform smoke test.`, + combinedOutput.length === 0 ? 'No output captured.' : combinedOutput + ].join('\n')) + } +} + +function smokeTestMainWrapper(): void { + const isolatedHomeDir = mkdtempSync(join(tmpdir(), 'tnmsc-main-wrapper-home-')) + const smokeTest = (() => { + try { + return spawnSync(process.execPath, [mainWrapperPath, '--help'], { + cwd: cliDir, + encoding: 'utf8', + env: { + ...process.env, + HOME: isolatedHomeDir, + USERPROFILE: isolatedHomeDir + } + }) + } + finally { + rmSync(isolatedHomeDir, {recursive: true, force: true}) + } + })() + + if (smokeTest.error != null) { + throw smokeTest.error + } + + const combinedOutput = `${smokeTest.stdout ?? ''}${smokeTest.stderr ?? ''}` + + if (smokeTest.status !== 0) { + throw new Error([ + `Generated "${mainWrapperPath}" failed the CLI smoke test.`, + `Exit code: ${smokeTest.status ?? 'unknown'}`, + combinedOutput.trim().length === 0 ? 'No CLI output captured.' : combinedOutput.trim() + ].join('\n')) + } + + if (!/(?:USAGE:|tnmsc v)/u.test(combinedOutput)) { + throw new Error([ + `Generated "${mainWrapperPath}" did not print CLI help output.`, + combinedOutput.trim().length === 0 ? 'No CLI output captured.' : combinedOutput.trim() + ].join('\n')) + } +} + +ensureIndexBundleExists() +writeMainWrapper() +const bundledJitiChunkPath = ensureBundledJitiRuntimeAssets() +assertMainWrapperSize() +smokeTestBundledJitiTransform(bundledJitiChunkPath) +smokeTestMainWrapper() + +console.log(`Wrote and validated CLI main wrapper at ${mainWrapperPath}`) diff --git a/cli/tsdown.config.ts b/cli/tsdown.config.ts index 01f4eb27..0d880a22 100644 --- a/cli/tsdown.config.ts +++ b/cli/tsdown.config.ts @@ -80,27 +80,6 @@ export default defineConfig([ __KIRO_GLOBAL_POWERS_REGISTRY__: kiroGlobalPowersRegistry } }, - { - entry: ['./src/main.ts'], - platform: 'node', - sourcemap: false, - unbundle: false, - inlineOnly: false, - alias: { - '@': resolve('src'), - ...pluginAliases - }, - noExternal: noExternalDeps, - format: ['esm'], - minify: true, - dts: false, - outputOptions: {banner: '#!/usr/bin/env node'}, - define: { - __CLI_VERSION__: JSON.stringify(pkg.version), - __CLI_PACKAGE_NAME__: JSON.stringify(pkg.name), - __KIRO_GLOBAL_POWERS_REGISTRY__: kiroGlobalPowersRegistry - } - }, { entry: ['./src/plugin-runtime.ts'], platform: 'node', diff --git a/doc/package.json b/doc/package.json index 5481839b..1f6d69ae 100644 --- a/doc/package.json +++ b/doc/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-docs", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "private": true, "description": "Documentation site for @truenine/memory-sync, built with Next.js 16 and MDX.", "engines": { diff --git a/gui/package.json b/gui/package.json index fff69347..a09f0b71 100644 --- a/gui/package.json +++ b/gui/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-gui", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "private": true, "engines": { "node": ">=25.2.1", diff --git a/gui/src-tauri/Cargo.toml b/gui/src-tauri/Cargo.toml index 5d4a32e4..24d92282 100644 --- a/gui/src-tauri/Cargo.toml +++ b/gui/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "memory-sync-gui" -version = "2026.10318.11638" +version = "2026.10318.12034" description = "Memory Sync desktop GUI application" authors.workspace = true edition.workspace = true diff --git a/gui/src-tauri/tauri.conf.json b/gui/src-tauri/tauri.conf.json index 5b7e6cf1..91198f07 100644 --- a/gui/src-tauri/tauri.conf.json +++ b/gui/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "$schema": "https://schema.tauri.app/config/2", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "productName": "Memory Sync", "identifier": "org.truenine.memory-sync", "build": { diff --git a/libraries/logger/package.json b/libraries/logger/package.json index ca945292..018af915 100644 --- a/libraries/logger/package.json +++ b/libraries/logger/package.json @@ -1,7 +1,7 @@ { "name": "@truenine/logger", "type": "module", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "private": true, "description": "Rust-powered structured logger for Node.js via N-API", "license": "AGPL-3.0-only", diff --git a/libraries/logger/src/index.ts b/libraries/logger/src/index.ts index 769d0497..fe400042 100644 --- a/libraries/logger/src/index.ts +++ b/libraries/logger/src/index.ts @@ -1,4 +1,6 @@ +import {readdirSync} from 'node:fs' import {createRequire} from 'node:module' +import {dirname, join} from 'node:path' import process from 'node:process' export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'fatal' | 'silent' @@ -96,6 +98,37 @@ function formatBindingLoadError(localError: unknown, packageError: unknown, suff ) } +function loadBindingFromCliBinaryPackage( + runtimeRequire: ReturnType, + suffix: string +): NapiLoggerModule { + const packageName = `@truenine/memory-sync-cli-${suffix}` + + try { + const cliBinaryPackage = runtimeRequire(packageName) as Record + const loggerModule = cliBinaryPackage['logger'] + + if (isNapiLoggerModule(loggerModule)) return loggerModule + } + catch { + // Fall through to the package-directory probe below. + } + + const packageJsonPath = runtimeRequire.resolve(`${packageName}/package.json`) + const packageDir = dirname(packageJsonPath) + const bindingCandidates = readdirSync(packageDir) + .filter(fileName => fileName.startsWith('napi-logger.') && fileName.endsWith('.node')) + .sort() + + for (const candidateFile of bindingCandidates) { + const bindingModule = runtimeRequire(join(packageDir, candidateFile)) as unknown + + if (isNapiLoggerModule(bindingModule)) return bindingModule + } + + throw new Error(`Package "${packageName}" does not export a logger binding or contain a compatible native module`) +} + function loadNativeBinding(): NapiLoggerModule { const moduleUrl = import.meta.url const runtimeRequire = createRequire(moduleUrl) @@ -106,12 +139,7 @@ function loadNativeBinding(): NapiLoggerModule { } catch (localError) { try { - const cliBinaryPackage = runtimeRequire(`@truenine/memory-sync-cli-${suffix}`) as Record - const loggerModule = cliBinaryPackage['logger'] - - if (isNapiLoggerModule(loggerModule)) return loggerModule - - throw new Error(`Package "@truenine/memory-sync-cli-${suffix}" does not export a logger binding`) + return loadBindingFromCliBinaryPackage(runtimeRequire, suffix) } catch (packageError) { throw formatBindingLoadError(localError, packageError, suffix) diff --git a/libraries/md-compiler/package.json b/libraries/md-compiler/package.json index ca5957fd..06d7a7ae 100644 --- a/libraries/md-compiler/package.json +++ b/libraries/md-compiler/package.json @@ -1,7 +1,7 @@ { "name": "@truenine/md-compiler", "type": "module", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "private": true, "description": "Rust-powered MDX→Markdown compiler for Node.js with pure-TS fallback", "license": "AGPL-3.0-only", diff --git a/libraries/md-compiler/src/markdown/index.ts b/libraries/md-compiler/src/markdown/index.ts index 05dc6aad..48075fbb 100644 --- a/libraries/md-compiler/src/markdown/index.ts +++ b/libraries/md-compiler/src/markdown/index.ts @@ -1,5 +1,7 @@ import type {Root, RootContent} from 'mdast' +import {readdirSync} from 'node:fs' import {createRequire} from 'node:module' +import {dirname, join} from 'node:path' import process from 'node:process' import * as YAML from 'yaml' @@ -18,6 +20,51 @@ interface NapiMdCompilerModule { let napiBinding: NapiMdCompilerModule | null = null +function isNapiMdCompilerModule(value: unknown): value is NapiMdCompilerModule { + if (value == null || typeof value !== 'object') return false + + const candidate = value as Partial + return typeof candidate.buildFrontMatter === 'function' + && typeof candidate.buildMarkdownWithFrontMatter === 'function' + && typeof candidate.parseMarkdown === 'function' + && typeof candidate.transformMdxReferencesToMd === 'function' +} + +function loadBindingFromCliBinaryPackage( + requireFn: ReturnType, + suffix: string +): NapiMdCompilerModule | null { + const packageName = `@truenine/memory-sync-cli-${suffix}` + + try { + const pkg = requireFn(packageName) as Record + const binding = pkg['mdCompiler'] + + if (isNapiMdCompilerModule(binding)) return binding + } + catch { + // Fall through to the package-directory probe below. + } + + try { + const packageJsonPath = requireFn.resolve(`${packageName}/package.json`) + const packageDir = dirname(packageJsonPath) + const bindingCandidates = readdirSync(packageDir) + .filter(fileName => fileName.startsWith('napi-md-compiler.') && fileName.endsWith('.node')) + .sort() + + for (const candidateFile of bindingCandidates) { + const binding = requireFn(join(packageDir, candidateFile)) as unknown + if (isNapiMdCompilerModule(binding)) return binding + } + } + catch { + return null + } + + return null +} + try { const require = createRequire(import.meta.url) const {platform, arch} = process @@ -35,11 +82,7 @@ try { napiBinding = require(`../${local}.node`) as NapiMdCompilerModule } catch { - try { - const pkg = require(`@truenine/memory-sync-cli-${suffix}`) as Record - napiBinding = pkg['mdCompiler'] as NapiMdCompilerModule - } - catch {} + napiBinding = loadBindingFromCliBinaryPackage(require, suffix) } } } diff --git a/libraries/md-compiler/src/mdx-to-md.ts b/libraries/md-compiler/src/mdx-to-md.ts index 0c123f0d..f52cc42a 100644 --- a/libraries/md-compiler/src/mdx-to-md.ts +++ b/libraries/md-compiler/src/mdx-to-md.ts @@ -1,6 +1,8 @@ import type {ExportMetadata, MetadataSource} from './compiler/export-parser' import type {MdxToMdOptions, MdxToMdResult} from './compiler/types' +import {readdirSync} from 'node:fs' import {createRequire} from 'node:module' +import {dirname, join} from 'node:path' import process from 'node:process' import {mdxToMd as fallbackMdxToMd} from './compiler/mdx-to-md' @@ -26,6 +28,48 @@ const RESIDUAL_MODULE_SYNTAX_PATTERNS = [ let napiBinding: NapiMdCompilerModule | null = null +function isNapiMdCompilerModule(value: unknown): value is NapiMdCompilerModule { + if (value == null || typeof value !== 'object') return false + + const candidate = value as Partial + return typeof candidate.compileMdxToMd === 'function' +} + +function loadBindingFromCliBinaryPackage( + requireFn: ReturnType, + suffix: string +): NapiMdCompilerModule | null { + const packageName = `@truenine/memory-sync-cli-${suffix}` + + try { + const pkg = requireFn(packageName) as Record + const binding = pkg['mdCompiler'] + + if (isNapiMdCompilerModule(binding)) return binding + } + catch { + // Fall through to the package-directory probe below. + } + + try { + const packageJsonPath = requireFn.resolve(`${packageName}/package.json`) + const packageDir = dirname(packageJsonPath) + const bindingCandidates = readdirSync(packageDir) + .filter(fileName => fileName.startsWith('napi-md-compiler.') && fileName.endsWith('.node')) + .sort() + + for (const candidateFile of bindingCandidates) { + const binding = requireFn(join(packageDir, candidateFile)) as unknown + if (isNapiMdCompilerModule(binding)) return binding + } + } + catch { + return null + } + + return null +} + try { const require = createRequire(import.meta.url) const {platform, arch} = process @@ -43,11 +87,7 @@ try { napiBinding = require(`./${local}.node`) as NapiMdCompilerModule } catch { - try { - const pkg = require(`@truenine/memory-sync-cli-${suffix}`) as Record - napiBinding = pkg['mdCompiler'] as NapiMdCompilerModule - } - catch {} + napiBinding = loadBindingFromCliBinaryPackage(require, suffix) } } } diff --git a/libraries/script-runtime/package.json b/libraries/script-runtime/package.json index a2316e0f..286d332d 100644 --- a/libraries/script-runtime/package.json +++ b/libraries/script-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@truenine/script-runtime", "type": "module", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "private": true, "description": "Rust-backed TypeScript proxy runtime for tnmsc", "license": "AGPL-3.0-only", diff --git a/libraries/script-runtime/src/index.ts b/libraries/script-runtime/src/index.ts index e54f5c8a..f1a08f60 100644 --- a/libraries/script-runtime/src/index.ts +++ b/libraries/script-runtime/src/index.ts @@ -9,6 +9,7 @@ import type { import * as fs from 'node:fs' import {createRequire} from 'node:module' +import {dirname, join} from 'node:path' import process from 'node:process' import {fileURLToPath} from 'node:url' import { @@ -81,6 +82,37 @@ function formatBindingLoadError(localError: unknown, packageError: unknown, suff ) } +function loadBindingFromCliBinaryPackage( + runtimeRequire: ReturnType, + suffix: string +): ScriptRuntimeBinding { + const packageName = `@truenine/memory-sync-cli-${suffix}` + + try { + const cliBinaryPackage = runtimeRequire(packageName) as Record + const runtimeBinding = cliBinaryPackage['scriptRuntime'] + + if (isScriptRuntimeBinding(runtimeBinding)) return runtimeBinding + } + catch { + // Fall through to the package-directory probe below. + } + + const packageJsonPath = runtimeRequire.resolve(`${packageName}/package.json`) + const packageDir = dirname(packageJsonPath) + const bindingCandidates = fs.readdirSync(packageDir) + .filter(fileName => fileName.startsWith('napi-script-runtime.') && fileName.endsWith('.node')) + .sort() + + for (const candidateFile of bindingCandidates) { + const bindingModule = runtimeRequire(join(packageDir, candidateFile)) as unknown + + if (isScriptRuntimeBinding(bindingModule)) return bindingModule + } + + throw new Error(`Package "${packageName}" does not export a scriptRuntime binding or contain a compatible native module`) +} + function loadNativeBinding(): ScriptRuntimeBinding { const runtimeRequire = createRequire(import.meta.url) const {local, suffix} = getPlatformBinding() @@ -90,12 +122,7 @@ function loadNativeBinding(): ScriptRuntimeBinding { } catch (localError) { try { - const cliBinaryPackage = runtimeRequire(`@truenine/memory-sync-cli-${suffix}`) as Record - const runtimeBinding = cliBinaryPackage['scriptRuntime'] - - if (isScriptRuntimeBinding(runtimeBinding)) return runtimeBinding - - throw new Error(`Package "@truenine/memory-sync-cli-${suffix}" does not export a scriptRuntime binding`) + return loadBindingFromCliBinaryPackage(runtimeRequire, suffix) } catch (packageError) { throw formatBindingLoadError(localError, packageError, suffix) diff --git a/mcp/package.json b/mcp/package.json index bd011642..49901b17 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -1,7 +1,7 @@ { "name": "@truenine/memory-sync-mcp", "type": "module", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "description": "MCP stdio server for managing memory-sync prompt sources and translation artifacts", "author": "TrueNine", "license": "AGPL-3.0-only", diff --git a/package.json b/package.json index 5d117321..6644c25b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync", - "version": "2026.10318.11638", + "version": "2026.10318.12034", "description": "Cross-AI-tool prompt synchronisation toolkit (CLI + Tauri desktop GUI) — one ruleset, multi-target adaptation. Monorepo powered by pnpm + Turbo.", "license": "AGPL-3.0-only", "keywords": [