mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-13 08:15:14 +02:00
feat: assemble public kaelio ktx npm package
This commit is contained in:
parent
271c05ac99
commit
24dbbe2a06
2 changed files with 522 additions and 0 deletions
258
scripts/build-public-npm-package.mjs
Normal file
258
scripts/build-public-npm-package.mjs
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { execFile } from 'node:child_process';
|
||||
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const PUBLIC_NPM_PACKAGE_NAME = '@kaelio/ktx';
|
||||
export const PUBLIC_NPM_PACKAGE_VERSION = '0.0.0-private';
|
||||
export const PUBLIC_NPM_PACKAGE_TARBALL = 'kaelio-ktx-0.0.0-private.tgz';
|
||||
|
||||
export const PUBLIC_BUNDLED_WORKSPACE_PACKAGES = [
|
||||
'@ktx/llm',
|
||||
'@ktx/context',
|
||||
'@ktx/connector-bigquery',
|
||||
'@ktx/connector-clickhouse',
|
||||
'@ktx/connector-mysql',
|
||||
'@ktx/connector-postgres',
|
||||
'@ktx/connector-posthog',
|
||||
'@ktx/connector-snowflake',
|
||||
'@ktx/connector-sqlite',
|
||||
'@ktx/connector-sqlserver',
|
||||
];
|
||||
|
||||
export const PUBLIC_BUNDLED_WORKSPACE_PACKAGE_ROOTS = {
|
||||
'@ktx/llm': 'packages/llm',
|
||||
'@ktx/context': 'packages/context',
|
||||
'@ktx/connector-bigquery': 'packages/connector-bigquery',
|
||||
'@ktx/connector-clickhouse': 'packages/connector-clickhouse',
|
||||
'@ktx/connector-mysql': 'packages/connector-mysql',
|
||||
'@ktx/connector-postgres': 'packages/connector-postgres',
|
||||
'@ktx/connector-posthog': 'packages/connector-posthog',
|
||||
'@ktx/connector-snowflake': 'packages/connector-snowflake',
|
||||
'@ktx/connector-sqlite': 'packages/connector-sqlite',
|
||||
'@ktx/connector-sqlserver': 'packages/connector-sqlserver',
|
||||
};
|
||||
|
||||
function scriptRootDir() {
|
||||
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
}
|
||||
|
||||
export function publicNpmPackageLayout(rootDir = scriptRootDir()) {
|
||||
return {
|
||||
rootDir,
|
||||
cliPackageRoot: join(rootDir, 'packages', 'cli'),
|
||||
packRoot: join(rootDir, 'dist', 'public-npm-package'),
|
||||
npmDir: join(rootDir, 'dist', 'artifacts', 'npm'),
|
||||
tarballPath: join(rootDir, 'dist', 'artifacts', 'npm', PUBLIC_NPM_PACKAGE_TARBALL),
|
||||
};
|
||||
}
|
||||
|
||||
async function readJson(path) {
|
||||
return JSON.parse(await readFile(path, 'utf8'));
|
||||
}
|
||||
|
||||
async function writeJson(path, value) {
|
||||
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function sortedObject(entries) {
|
||||
return Object.fromEntries([...entries].sort(([left], [right]) => left.localeCompare(right)));
|
||||
}
|
||||
|
||||
function isWorkspacePackageName(name) {
|
||||
return name.startsWith('@ktx/');
|
||||
}
|
||||
|
||||
function parseCaretVersion(value) {
|
||||
const match = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(value);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
major: Number(match[1]),
|
||||
minor: Number(match[2]),
|
||||
patch: Number(match[3]),
|
||||
};
|
||||
}
|
||||
|
||||
function compareParsedVersions(left, right) {
|
||||
return left.major - right.major || left.minor - right.minor || left.patch - right.patch;
|
||||
}
|
||||
|
||||
function mergeDependencyVersion(name, previous, next) {
|
||||
if (previous === next) {
|
||||
return previous;
|
||||
}
|
||||
|
||||
const previousCaret = parseCaretVersion(previous);
|
||||
const nextCaret = parseCaretVersion(next);
|
||||
if (previousCaret && nextCaret && previousCaret.major === nextCaret.major) {
|
||||
return compareParsedVersions(previousCaret, nextCaret) >= 0 ? previous : next;
|
||||
}
|
||||
|
||||
throw new Error(`Incompatible dependency versions for ${name}: ${previous} and ${next}`);
|
||||
}
|
||||
|
||||
export function collectPublicDependencies(packageJsons) {
|
||||
const dependencies = new Map();
|
||||
|
||||
for (const packageJson of packageJsons) {
|
||||
for (const [name, version] of Object.entries(packageJson.dependencies ?? {})) {
|
||||
if (isWorkspacePackageName(name)) {
|
||||
continue;
|
||||
}
|
||||
const previous = dependencies.get(name);
|
||||
dependencies.set(name, previous ? mergeDependencyVersion(name, previous, version) : version);
|
||||
}
|
||||
}
|
||||
|
||||
return sortedObject(dependencies);
|
||||
}
|
||||
|
||||
export function publicNpmPackageJson(cliPackageJson, dependencies) {
|
||||
return {
|
||||
name: PUBLIC_NPM_PACKAGE_NAME,
|
||||
version: cliPackageJson.version ?? PUBLIC_NPM_PACKAGE_VERSION,
|
||||
description: 'Standalone KTX context layer for database agents',
|
||||
private: false,
|
||||
type: 'module',
|
||||
engines: cliPackageJson.engines ?? { node: '>=22.0.0' },
|
||||
bin: { ktx: './dist/bin.js' },
|
||||
main: cliPackageJson.main ?? 'dist/index.js',
|
||||
types: cliPackageJson.types ?? 'dist/index.d.ts',
|
||||
exports: cliPackageJson.exports ?? {
|
||||
'.': {
|
||||
types: './dist/index.d.ts',
|
||||
import: './dist/index.js',
|
||||
default: './dist/index.js',
|
||||
},
|
||||
'./package.json': './package.json',
|
||||
},
|
||||
files: ['dist', 'assets'],
|
||||
dependencies,
|
||||
bundledDependencies: PUBLIC_BUNDLED_WORKSPACE_PACKAGES,
|
||||
license: cliPackageJson.license ?? 'Apache-2.0',
|
||||
repository: {
|
||||
type: 'git',
|
||||
url: 'git+https://github.com/kaelio/ktx.git',
|
||||
},
|
||||
bugs: {
|
||||
url: 'https://github.com/kaelio/ktx/issues',
|
||||
},
|
||||
homepage: 'https://github.com/kaelio/ktx#readme',
|
||||
};
|
||||
}
|
||||
|
||||
function bundledWorkspacePackageJson(packageJson) {
|
||||
const dependencies = Object.fromEntries(
|
||||
Object.entries(packageJson.dependencies ?? {}).filter(([name]) => !isWorkspacePackageName(name)),
|
||||
);
|
||||
|
||||
return {
|
||||
name: packageJson.name,
|
||||
version: packageJson.version ?? PUBLIC_NPM_PACKAGE_VERSION,
|
||||
private: true,
|
||||
type: packageJson.type ?? 'module',
|
||||
main: packageJson.main,
|
||||
types: packageJson.types,
|
||||
exports: packageJson.exports,
|
||||
files: packageJson.files,
|
||||
dependencies: sortedObject(Object.entries(dependencies)),
|
||||
license: packageJson.license ?? 'Apache-2.0',
|
||||
};
|
||||
}
|
||||
|
||||
async function copyPackageFileEntries(sourceRoot, targetRoot, packageJson) {
|
||||
for (const entry of packageJson.files ?? ['dist']) {
|
||||
await cp(join(sourceRoot, entry), join(targetRoot, entry), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function copyCliPackage(layout, cliPackageJson, dependencies) {
|
||||
await copyPackageFileEntries(layout.cliPackageRoot, layout.packRoot, cliPackageJson);
|
||||
await writeJson(join(layout.packRoot, 'package.json'), publicNpmPackageJson(cliPackageJson, dependencies));
|
||||
}
|
||||
|
||||
async function copyBundledWorkspacePackage(rootDir, packageName, packageJson) {
|
||||
const packageRoot = PUBLIC_BUNDLED_WORKSPACE_PACKAGE_ROOTS[packageName];
|
||||
if (!packageRoot) {
|
||||
throw new Error(`Missing bundled workspace package root for ${packageName}`);
|
||||
}
|
||||
|
||||
const sourceRoot = join(rootDir, packageRoot);
|
||||
const targetRoot = join(rootDir, 'dist', 'public-npm-package', 'node_modules', ...packageName.split('/'));
|
||||
await mkdir(targetRoot, { recursive: true });
|
||||
await copyPackageFileEntries(sourceRoot, targetRoot, packageJson);
|
||||
await writeJson(join(targetRoot, 'package.json'), bundledWorkspacePackageJson(packageJson));
|
||||
}
|
||||
|
||||
export async function createPublicNpmPackageTree(layout = publicNpmPackageLayout()) {
|
||||
const cliPackageJson = await readJson(join(layout.cliPackageRoot, 'package.json'));
|
||||
const bundledPackageJsons = await Promise.all(
|
||||
PUBLIC_BUNDLED_WORKSPACE_PACKAGES.map(async (packageName) => {
|
||||
const packageRoot = PUBLIC_BUNDLED_WORKSPACE_PACKAGE_ROOTS[packageName];
|
||||
const packageJson = await readJson(join(layout.rootDir, packageRoot, 'package.json'));
|
||||
if (packageJson.name !== packageName) {
|
||||
throw new Error(`Unexpected package name in ${packageRoot}/package.json: ${packageJson.name}`);
|
||||
}
|
||||
return packageJson;
|
||||
}),
|
||||
);
|
||||
const dependencies = collectPublicDependencies([cliPackageJson, ...bundledPackageJsons]);
|
||||
|
||||
await rm(layout.packRoot, { recursive: true, force: true });
|
||||
await mkdir(layout.packRoot, { recursive: true });
|
||||
await mkdir(layout.npmDir, { recursive: true });
|
||||
await copyCliPackage(layout, cliPackageJson, dependencies);
|
||||
|
||||
for (const packageJson of bundledPackageJsons) {
|
||||
await copyBundledWorkspacePackage(layout.rootDir, packageJson.name, packageJson);
|
||||
}
|
||||
|
||||
return {
|
||||
layout,
|
||||
packageJson: publicNpmPackageJson(cliPackageJson, dependencies),
|
||||
bundledPackages: PUBLIC_BUNDLED_WORKSPACE_PACKAGES,
|
||||
};
|
||||
}
|
||||
|
||||
export function publicNpmPackCommand(layout = publicNpmPackageLayout()) {
|
||||
return {
|
||||
command: 'pnpm',
|
||||
args: ['pack', '--out', layout.tarballPath],
|
||||
cwd: layout.packRoot,
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildPublicNpmPackage(layout = publicNpmPackageLayout()) {
|
||||
await createPublicNpmPackageTree(layout);
|
||||
const pack = publicNpmPackCommand(layout);
|
||||
await execFileAsync(pack.command, pack.args, {
|
||||
cwd: pack.cwd,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
return layout.tarballPath;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const tarball = await buildPublicNpmPackage();
|
||||
process.stdout.write(`Built ${PUBLIC_NPM_PACKAGE_NAME} package: ${tarball}\n`);
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
||||
try {
|
||||
await main();
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
264
scripts/build-public-npm-package.test.mjs
Normal file
264
scripts/build-public-npm-package.test.mjs
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
PUBLIC_BUNDLED_WORKSPACE_PACKAGES,
|
||||
PUBLIC_NPM_PACKAGE_NAME,
|
||||
collectPublicDependencies,
|
||||
createPublicNpmPackageTree,
|
||||
publicNpmPackageJson,
|
||||
publicNpmPackageLayout,
|
||||
publicNpmPackCommand,
|
||||
} from './build-public-npm-package.mjs';
|
||||
|
||||
async function writeJson(path, value) {
|
||||
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
async function writePackage(root, packageRoot, packageJson, files = {}) {
|
||||
const absoluteRoot = join(root, packageRoot);
|
||||
await mkdir(absoluteRoot, { recursive: true });
|
||||
await writeJson(join(absoluteRoot, 'package.json'), packageJson);
|
||||
|
||||
for (const [relativePath, contents] of Object.entries(files)) {
|
||||
const target = join(absoluteRoot, relativePath);
|
||||
await mkdir(join(target, '..'), { recursive: true });
|
||||
await writeFile(target, contents);
|
||||
}
|
||||
}
|
||||
|
||||
async function writeWorkspaceFixture(root) {
|
||||
await writePackage(
|
||||
root,
|
||||
'packages/cli',
|
||||
{
|
||||
name: '@ktx/cli',
|
||||
version: '0.0.0-private',
|
||||
description: 'CLI wrapper for KTX',
|
||||
type: 'module',
|
||||
engines: { node: '>=22.0.0' },
|
||||
bin: { ktx: './dist/bin.js' },
|
||||
main: 'dist/index.js',
|
||||
types: 'dist/index.d.ts',
|
||||
exports: {
|
||||
'.': {
|
||||
types: './dist/index.d.ts',
|
||||
import: './dist/index.js',
|
||||
default: './dist/index.js',
|
||||
},
|
||||
'./package.json': './package.json',
|
||||
},
|
||||
files: ['dist', 'assets'],
|
||||
dependencies: {
|
||||
'@clack/prompts': '1.3.0',
|
||||
'@ktx/context': 'workspace:*',
|
||||
commander: '14.0.3',
|
||||
},
|
||||
license: 'Apache-2.0',
|
||||
repository: {
|
||||
type: 'git',
|
||||
url: 'git+https://github.com/kaelio/ktx.git',
|
||||
directory: 'packages/cli',
|
||||
},
|
||||
},
|
||||
{
|
||||
'dist/bin.js': '#!/usr/bin/env node\n',
|
||||
'dist/index.js': 'export const cli = true;\n',
|
||||
'dist/index.d.ts': 'export declare const cli: true;\n',
|
||||
'assets/python/manifest.json': '{"schemaVersion":1}\n',
|
||||
},
|
||||
);
|
||||
|
||||
await writePackage(
|
||||
root,
|
||||
'packages/context',
|
||||
{
|
||||
name: '@ktx/context',
|
||||
version: '0.0.0-private',
|
||||
type: 'module',
|
||||
main: 'dist/index.js',
|
||||
exports: { '.': './dist/index.js' },
|
||||
files: ['dist', 'prompts', 'skills'],
|
||||
dependencies: {
|
||||
'@ktx/llm': 'workspace:*',
|
||||
yaml: '^2.8.2',
|
||||
},
|
||||
},
|
||||
{
|
||||
'dist/index.js': 'export const context = true;\n',
|
||||
'prompts/system.md': 'prompt\n',
|
||||
'skills/sl/SKILL.md': 'skill\n',
|
||||
},
|
||||
);
|
||||
|
||||
await writePackage(
|
||||
root,
|
||||
'packages/llm',
|
||||
{
|
||||
name: '@ktx/llm',
|
||||
version: '0.0.0-private',
|
||||
type: 'module',
|
||||
main: 'dist/index.js',
|
||||
exports: { '.': './dist/index.js' },
|
||||
files: ['dist'],
|
||||
dependencies: {
|
||||
ai: '^6.0.168',
|
||||
},
|
||||
},
|
||||
{
|
||||
'dist/index.js': 'export const llm = true;\n',
|
||||
},
|
||||
);
|
||||
|
||||
for (const packageName of PUBLIC_BUNDLED_WORKSPACE_PACKAGES.filter((name) => name.startsWith('@ktx/connector-'))) {
|
||||
const directory = packageName.replace('@ktx/', '');
|
||||
await writePackage(
|
||||
root,
|
||||
`packages/${directory}`,
|
||||
{
|
||||
name: packageName,
|
||||
version: '0.0.0-private',
|
||||
type: 'module',
|
||||
main: 'dist/index.js',
|
||||
exports: { '.': './dist/index.js' },
|
||||
files: ['dist'],
|
||||
dependencies: {
|
||||
'@ktx/context': 'workspace:*',
|
||||
},
|
||||
},
|
||||
{
|
||||
'dist/index.js': `export const name = ${JSON.stringify(packageName)};\n`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
describe('publicNpmPackageLayout', () => {
|
||||
it('uses stable public package build and tarball paths', () => {
|
||||
const layout = publicNpmPackageLayout('/repo/ktx');
|
||||
|
||||
assert.equal(layout.rootDir, '/repo/ktx');
|
||||
assert.equal(layout.packRoot, '/repo/ktx/dist/public-npm-package');
|
||||
assert.equal(layout.npmDir, '/repo/ktx/dist/artifacts/npm');
|
||||
assert.equal(layout.tarballPath, '/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.0.0-private.tgz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('collectPublicDependencies', () => {
|
||||
it('unions external runtime dependencies and omits workspace packages', () => {
|
||||
assert.deepEqual(
|
||||
collectPublicDependencies([
|
||||
{
|
||||
name: '@ktx/cli',
|
||||
dependencies: {
|
||||
'@ktx/context': 'workspace:*',
|
||||
commander: '14.0.3',
|
||||
zod: '^4.4.3',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@ktx/context',
|
||||
dependencies: {
|
||||
'@ktx/llm': 'workspace:*',
|
||||
commander: '14.0.3',
|
||||
yaml: '^2.8.2',
|
||||
zod: '^4.1.13',
|
||||
},
|
||||
},
|
||||
]),
|
||||
{
|
||||
commander: '14.0.3',
|
||||
yaml: '^2.8.2',
|
||||
zod: '^4.4.3',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('fails on incompatible external dependency ranges', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
collectPublicDependencies([
|
||||
{ name: '@ktx/cli', dependencies: { zod: '^4.4.3' } },
|
||||
{ name: '@ktx/context', dependencies: { zod: '^3.25.0' } },
|
||||
]),
|
||||
/Incompatible dependency versions for zod/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('publicNpmPackageJson', () => {
|
||||
it('describes the public @kaelio/ktx binary package', () => {
|
||||
const packageJson = publicNpmPackageJson(
|
||||
{
|
||||
name: '@ktx/cli',
|
||||
version: '0.0.0-private',
|
||||
engines: { node: '>=22.0.0' },
|
||||
bin: { ktx: './dist/bin.js' },
|
||||
main: 'dist/index.js',
|
||||
types: 'dist/index.d.ts',
|
||||
exports: { '.': './dist/index.js', './package.json': './package.json' },
|
||||
license: 'Apache-2.0',
|
||||
},
|
||||
{ commander: '14.0.3' },
|
||||
);
|
||||
|
||||
assert.equal(packageJson.name, PUBLIC_NPM_PACKAGE_NAME);
|
||||
assert.equal(packageJson.private, false);
|
||||
assert.deepEqual(packageJson.bin, { ktx: './dist/bin.js' });
|
||||
assert.deepEqual(packageJson.dependencies, { commander: '14.0.3' });
|
||||
assert.deepEqual(packageJson.bundledDependencies, PUBLIC_BUNDLED_WORKSPACE_PACKAGES);
|
||||
assert.deepEqual(packageJson.files, ['dist', 'assets']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPublicNpmPackageTree', () => {
|
||||
it('copies CLI files, assets, and bundled internal workspace packages', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'ktx-public-npm-test-'));
|
||||
try {
|
||||
await writeWorkspaceFixture(root);
|
||||
const layout = publicNpmPackageLayout(root);
|
||||
|
||||
const result = await createPublicNpmPackageTree(layout);
|
||||
|
||||
assert.equal(result.packageJson.name, '@kaelio/ktx');
|
||||
assert.equal(result.packageJson.dependencies.commander, '14.0.3');
|
||||
assert.equal(result.packageJson.dependencies.yaml, '^2.8.2');
|
||||
assert.equal(result.packageJson.dependencies.ai, '^6.0.168');
|
||||
assert.equal(
|
||||
await readFile(join(layout.packRoot, 'assets', 'python', 'manifest.json'), 'utf8'),
|
||||
'{"schemaVersion":1}\n',
|
||||
);
|
||||
assert.equal(
|
||||
await readFile(join(layout.packRoot, 'node_modules', '@ktx', 'context', 'dist', 'index.js'), 'utf8'),
|
||||
'export const context = true;\n',
|
||||
);
|
||||
assert.equal(
|
||||
await readFile(join(layout.packRoot, 'node_modules', '@ktx', 'context', 'prompts', 'system.md'), 'utf8'),
|
||||
'prompt\n',
|
||||
);
|
||||
|
||||
const bundledContextJson = JSON.parse(
|
||||
await readFile(join(layout.packRoot, 'node_modules', '@ktx', 'context', 'package.json'), 'utf8'),
|
||||
);
|
||||
assert.equal(bundledContextJson.private, true);
|
||||
assert.deepEqual(bundledContextJson.dependencies, { yaml: '^2.8.2' });
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('publicNpmPackCommand', () => {
|
||||
it('packs the assembled public package with pnpm', () => {
|
||||
const layout = publicNpmPackageLayout('/repo/ktx');
|
||||
|
||||
assert.deepEqual(publicNpmPackCommand(layout), {
|
||||
command: 'pnpm',
|
||||
args: ['pack', '--out', '/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.0.0-private.tgz'],
|
||||
cwd: '/repo/ktx/dist/public-npm-package',
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue