ktx/scripts/package-artifacts.mjs
Andrey Avtomonov a11b9e9757
refactor(release): drop release-policy.json runtime dep and next branch (#180)
* chore: standardize daemon naming on "KTX daemon"

Replace inconsistent names ("KTX Python daemon", "KTX local embeddings
daemon", "KTX managed daemon", "Python daemon") with the single name
"KTX daemon" in CLI output, errors, command descriptions, test
assertions, smoke scripts, docs, AGENTS.md, issue templates, and
codecov flags. The daemon is a portable compute server with endpoints
for SQL analysis, semantic layer, LookML, database introspection, and
embeddings; the previous labels misrepresented it as embeddings-only or
exposed implementation details ("Python", "managed").

The "KTX Python runtime" concept (installed interpreter + packages) is
deliberately left as-is — it is a separate concept from the daemon
process.

* refactor(release): drop release-policy.json runtime dep and next branch

Strips the release-policy.json fallback from release-version.ts so the CLI
reads its version straight from packages/cli/package.json. dev → 0.0.0-private,
installed @kaelio/ktx → the real semver baked into the published package.json.
KtxCliPackageInfo collapses to { name, version, contextPackageName }; /health
no longer depends on version files surviving past a CI run.

Replaces the dual-branch (main + next) semantic-release model with a single-
branch model on main. rcs and stables interleave on the same branch via
{ name: 'main', prerelease: 'rc', channel: 'next' } / ['main']. Drops
@semantic-release/git and @semantic-release/changelog (nothing is committed
back to the repo on any channel) and the workflow's "Prepare next prerelease
branch" step plus the KTX_PRERELEASE_BRANCH plumbing. The git tag plus the
published npm artifact carry the version forward.

Updates docs/release.md, removes the two now-unused devDeps, regenerates
pnpm-lock.yaml. 611/611 @ktx/cli tests, 173/173 script tests, type-check,
biome, knip all clean.

* fix(release): don't throw on non-main branches at config-load time

knip loads .releaserc.cjs on every PR run, where GITHUB_REF_NAME is the
merge ref (e.g. 180/merge). The previous version of releaseBranches threw
immediately when the branch wasn't main, which made knip fail to evaluate
the config and then mis-flag @semantic-release/exec as an unused dep.

semantic-release already refuses to publish when the current branch doesn't
match a configured release branch, so the explicit throw was redundant.
Drop it (and the unused currentBranch helper) and replace the
"rejects releases from non-main" assertion with one that exercises a CI-
shaped GITHUB_REF_NAME and confirms the config loads.
2026-05-20 13:53:14 +02:00

1063 lines
35 KiB
JavaScript

#!/usr/bin/env node
import { createHash } from 'node:crypto';
import { execFile } from 'node:child_process';
import { access, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import {
RUNTIME_WHEEL_DISTRIBUTION_NAME,
RUNTIME_WHEEL_NORMALIZED_NAME,
RUNTIME_WHEEL_PACKAGE_VERSION,
} from './build-python-runtime-wheel.mjs';
import {
PUBLIC_NPM_PACKAGE_NAME,
publicNpmPackageTarballName,
} from './build-public-npm-package.mjs';
import { publicNpmPackageVersion } from './public-npm-release-metadata.mjs';
export {
RUNTIME_WHEEL_DISTRIBUTION_NAME,
RUNTIME_WHEEL_NORMALIZED_NAME,
RUNTIME_WHEEL_PACKAGE_VERSION,
};
export const INTERNAL_NPM_WORKSPACE_PACKAGES = [
{ name: '@ktx/context', packageRoot: 'packages/context' },
{ name: '@ktx/llm', packageRoot: 'packages/llm' },
{ name: '@ktx/connector-bigquery', packageRoot: 'packages/connector-bigquery' },
{ name: '@ktx/connector-clickhouse', packageRoot: 'packages/connector-clickhouse' },
{ name: '@ktx/connector-mysql', packageRoot: 'packages/connector-mysql' },
{ name: '@ktx/connector-postgres', packageRoot: 'packages/connector-postgres' },
{ name: '@ktx/connector-snowflake', packageRoot: 'packages/connector-snowflake' },
{ name: '@ktx/connector-sqlite', packageRoot: 'packages/connector-sqlite' },
{ name: '@ktx/connector-sqlserver', packageRoot: 'packages/connector-sqlserver' },
{ name: '@ktx/cli', packageRoot: 'packages/cli' },
];
export const NPM_ARTIFACT_PACKAGES = [{ name: PUBLIC_NPM_PACKAGE_NAME, packageRoot: 'packages/cli' }];
export const CLI_PYTHON_ASSET_MANIFEST = 'manifest.json';
function scriptRootDir() {
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
}
function npmPackageTarballName(packageName, version) {
if (packageName !== PUBLIC_NPM_PACKAGE_NAME) {
throw new Error(`Unsupported npm artifact package: ${packageName}`);
}
return publicNpmPackageTarballName(version);
}
function npmPackageTarballs(npmDir, version) {
return Object.fromEntries(
NPM_ARTIFACT_PACKAGES.map((packageInfo) => [
packageInfo.name,
join(npmDir, npmPackageTarballName(packageInfo.name, version)),
]),
);
}
export function packageArtifactLayout(rootDir = scriptRootDir(), version = publicNpmPackageVersion(rootDir)) {
const artifactDir = join(rootDir, 'dist', 'artifacts');
const npmDir = join(artifactDir, 'npm');
const pythonDir = join(artifactDir, 'python');
const npmTarballs = npmPackageTarballs(npmDir, version);
return {
rootDir,
artifactDir,
npmDir,
pythonDir,
npmTarballs,
contextTarball: npmTarballs[PUBLIC_NPM_PACKAGE_NAME],
cliTarball: npmTarballs[PUBLIC_NPM_PACKAGE_NAME],
connectorTarballs: {},
manifestPath: join(artifactDir, 'manifest.json'),
};
}
export function buildArtifactCommands(layout) {
// One recursive pnpm invocation; topology comes from workspace deps in
// each package.json, parallelism from --workspace-concurrency.
const npmBuildCommand = {
command: 'pnpm',
args: [
'--filter',
'./packages/*',
'--workspace-concurrency=10',
'run',
'build',
],
cwd: layout.rootDir,
};
const publicPackageCommand = {
command: process.execPath,
args: ['scripts/build-public-npm-package.mjs'],
cwd: layout.rootDir,
};
return [
npmBuildCommand,
{
command: process.execPath,
args: ['scripts/build-python-runtime-wheel.mjs'],
cwd: layout.rootDir,
},
publicPackageCommand,
];
}
async function pathExists(path) {
try {
await access(path);
return true;
} catch {
return false;
}
}
async function assertPathExists(path, label) {
if (!(await pathExists(path))) {
throw new Error(`Missing ${label}: ${path}`);
}
}
function normalizePythonDistributionName(name) {
return name.replaceAll('-', '_');
}
function findOne(files, distributionName, suffix, label, pythonDir, version) {
const normalized = normalizePythonDistributionName(distributionName);
const found = files.find((file) => file.startsWith(`${normalized}-${version}`) && file.endsWith(suffix));
if (!found) {
throw new Error(`Missing Python artifact: ${label}`);
}
return join(pythonDir, found);
}
export async function findPythonArtifacts(pythonDir) {
const files = await readdir(pythonDir);
return {
runtimeWheel: findOne(
files,
RUNTIME_WHEEL_DISTRIBUTION_NAME,
'.whl',
'kaelio-ktx runtime wheel',
pythonDir,
RUNTIME_WHEEL_PACKAGE_VERSION,
),
};
}
export function artifactManifestPath(layout) {
return layout.manifestPath ?? join(layout.artifactDir, 'manifest.json');
}
async function readJson(path) {
return JSON.parse(await readFile(path, 'utf-8'));
}
function releaseMetadataEntry({ ecosystem, packageName, packageRoot, packageVersion, privatePackage }) {
return {
ecosystem,
packageName,
packageRoot,
packageVersion,
private: privatePackage,
releaseMode: 'ci-artifact-only',
};
}
async function readNpmPackageMetadata(rootDir, packageInfo, version) {
const packageJson = await readJson(join(rootDir, packageInfo.packageRoot, 'package.json'));
const expectedSourceName = packageInfo.name === PUBLIC_NPM_PACKAGE_NAME ? '@ktx/cli' : packageInfo.name;
if (packageJson.name !== expectedSourceName) {
throw new Error(
`Unexpected package name in ${packageInfo.packageRoot}/package.json: expected ${expectedSourceName}, got ${packageJson.name}`,
);
}
const isPublicKtxPackage = packageInfo.name === PUBLIC_NPM_PACKAGE_NAME;
return releaseMetadataEntry({
ecosystem: 'npm',
packageName: packageInfo.name,
packageRoot: packageInfo.packageRoot,
packageVersion: isPublicKtxPackage ? version : packageJson.version,
privatePackage: isPublicKtxPackage ? false : packageJson.private === true,
});
}
export async function packageReleaseMetadata(rootDir = scriptRootDir(), version = publicNpmPackageVersion(rootDir)) {
const npmPackages = await Promise.all(
NPM_ARTIFACT_PACKAGES.map((packageInfo) => readNpmPackageMetadata(rootDir, packageInfo, version)),
);
return [
...npmPackages,
releaseMetadataEntry({
ecosystem: 'python',
packageName: RUNTIME_WHEEL_DISTRIBUTION_NAME,
packageRoot: 'python/runtime-wheel',
packageVersion: RUNTIME_WHEEL_PACKAGE_VERSION,
privatePackage: false,
}),
];
}
function packageMetadataByName(packages) {
return new Map(packages.map((metadata) => [metadata.packageName, metadata]));
}
function requirePackageMetadata(packagesByName, packageName) {
const metadata = packagesByName.get(packageName);
if (!metadata) {
throw new Error(`Missing package release metadata for ${packageName}`);
}
return metadata;
}
function artifactPackageRecords(layout, pythonArtifacts, packages) {
const packagesByName = packageMetadataByName(packages);
const npmRecords = NPM_ARTIFACT_PACKAGES.map((packageInfo) => ({
artifactKind: 'tarball',
artifactPath: layout.npmTarballs[packageInfo.name],
metadata: requirePackageMetadata(packagesByName, packageInfo.name),
}));
return [
...npmRecords,
{
artifactKind: 'wheel',
artifactPath: pythonArtifacts.runtimeWheel,
metadata: requirePackageMetadata(packagesByName, RUNTIME_WHEEL_DISTRIBUTION_NAME),
},
];
}
function artifactRelativePath(layout, artifactPath) {
return relative(layout.artifactDir, artifactPath).split(sep).join('/');
}
function formatJson(value) {
return JSON.stringify(value, null, 2);
}
function assertJsonEqual(actual, expected, label) {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(`${label} do not match\nExpected:\n${formatJson(expected)}\nActual:\n${formatJson(actual)}`);
}
}
function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function assertString(value, label) {
if (typeof value !== 'string') {
throw new Error(`${label} must be a string`);
}
}
function artifactPathFromManifest(layout, manifestPath) {
assertString(manifestPath, 'Artifact manifest file path');
if (
manifestPath.length === 0 ||
manifestPath.startsWith('/') ||
manifestPath.includes('\\') ||
manifestPath.split('/').some((part) => part.length === 0 || part === '..')
) {
throw new Error(`Unsafe artifact manifest path: ${manifestPath}`);
}
const resolvedPath = resolve(layout.artifactDir, manifestPath);
const relativePath = relative(layout.artifactDir, resolvedPath);
if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
throw new Error(`Unsafe artifact manifest path: ${manifestPath}`);
}
return resolvedPath;
}
function sortedManifestFiles(files) {
return [...files].sort((a, b) => a.path.localeCompare(b.path));
}
function assertManifestShape(manifest) {
if (!isPlainObject(manifest)) {
throw new Error('Artifact manifest must be a JSON object');
}
if (manifest.schemaVersion !== 2) {
throw new Error(`Unsupported artifact manifest schemaVersion: ${manifest.schemaVersion}`);
}
assertString(manifest.generatedAt, 'Artifact manifest generatedAt');
if (Number.isNaN(Date.parse(manifest.generatedAt))) {
throw new Error(`Artifact manifest generatedAt is not an ISO timestamp: ${manifest.generatedAt}`);
}
if (manifest.sourceRevision !== null && typeof manifest.sourceRevision !== 'string') {
throw new Error('Artifact manifest sourceRevision must be a string or null');
}
if (!Array.isArray(manifest.packages)) {
throw new Error('Artifact manifest packages must be an array');
}
if (!Array.isArray(manifest.files)) {
throw new Error('Artifact manifest files must be an array');
}
}
async function artifactManifestEntry(layout, record) {
const contents = await readFile(record.artifactPath);
return {
path: artifactRelativePath(layout, record.artifactPath),
ecosystem: record.metadata.ecosystem,
artifactKind: record.artifactKind,
packageName: record.metadata.packageName,
packageVersion: record.metadata.packageVersion,
bytes: contents.byteLength,
sha256: createHash('sha256').update(contents).digest('hex'),
};
}
export async function buildArtifactManifest(layout, generatedAt = new Date(), options = {}) {
const pythonArtifacts = await findPythonArtifacts(layout.pythonDir);
const packages = await packageReleaseMetadata(layout.rootDir);
const artifactRecords = artifactPackageRecords(layout, pythonArtifacts, packages);
const files = await Promise.all(artifactRecords.map((record) => artifactManifestEntry(layout, record)));
return {
schemaVersion: 2,
generatedAt: generatedAt.toISOString(),
sourceRevision: options.sourceRevision ?? process.env.GITHUB_SHA ?? null,
packages,
files: files.sort((a, b) => a.path.localeCompare(b.path)),
};
}
export async function writeArtifactManifest(layout, generatedAt = new Date(), options = {}) {
const manifest = await buildArtifactManifest(layout, generatedAt, options);
await writeFile(artifactManifestPath(layout), `${JSON.stringify(manifest, null, 2)}\n`);
return manifest;
}
export async function verifyArtifactManifest(layout, options = {}) {
const manifest = await readJson(artifactManifestPath(layout));
assertManifestShape(manifest);
const expectedSourceRevision = options.expectedSourceRevision ?? process.env.KTX_EXPECTED_SOURCE_REVISION;
if (expectedSourceRevision !== undefined && manifest.sourceRevision !== expectedSourceRevision) {
throw new Error(
`Artifact manifest sourceRevision mismatch: expected ${expectedSourceRevision}, got ${manifest.sourceRevision}`,
);
}
const expectedPackages = await packageReleaseMetadata(layout.rootDir);
assertJsonEqual(manifest.packages, expectedPackages, 'Artifact manifest packages');
for (const file of manifest.files) {
if (!isPlainObject(file)) {
throw new Error('Artifact manifest file entries must be JSON objects');
}
artifactPathFromManifest(layout, file.path);
}
const pythonArtifacts = await findPythonArtifacts(layout.pythonDir);
const expectedFiles = await Promise.all(
artifactPackageRecords(layout, pythonArtifacts, expectedPackages).map((record) => artifactManifestEntry(layout, record)),
);
assertJsonEqual(
sortedManifestFiles(manifest.files),
sortedManifestFiles(expectedFiles),
'Artifact manifest files do not match artifact contents',
);
return manifest;
}
function runtimeWheelAssetName(runtimeWheelPath) {
return runtimeWheelPath.split(sep).at(-1);
}
export async function copyRuntimeWheelAssets(layout, pythonArtifacts) {
const assetDir = join(layout.rootDir, 'packages', 'cli', 'assets', 'python');
const wheelFile = runtimeWheelAssetName(pythonArtifacts.runtimeWheel);
if (!wheelFile) {
throw new Error(`Unable to determine runtime wheel filename: ${pythonArtifacts.runtimeWheel}`);
}
const wheelContents = await readFile(pythonArtifacts.runtimeWheel);
await rm(assetDir, { recursive: true, force: true });
await mkdir(assetDir, { recursive: true });
const wheelPath = join(assetDir, wheelFile);
const manifestPath = join(assetDir, CLI_PYTHON_ASSET_MANIFEST);
await writeFile(wheelPath, wheelContents);
await writeFile(
manifestPath,
`${JSON.stringify(
{
schemaVersion: 1,
distributionName: RUNTIME_WHEEL_DISTRIBUTION_NAME,
normalizedName: RUNTIME_WHEEL_NORMALIZED_NAME,
version: RUNTIME_WHEEL_PACKAGE_VERSION,
wheel: {
file: wheelFile,
sha256: createHash('sha256').update(wheelContents).digest('hex'),
bytes: wheelContents.byteLength,
},
},
null,
2,
)}\n`,
);
return { assetDir, wheelPath, manifestPath };
}
function runCommand(command, args, options = {}) {
const cwd = options.cwd ?? process.cwd();
process.stdout.write(`$ ${command} ${args.join(' ')}\n`);
return new Promise((resolvePromise, reject) => {
const child = execFile(
command,
args,
{
cwd,
env: { ...process.env, ...options.env },
maxBuffer: 1024 * 1024 * 20,
},
(error, stdout, stderr) => {
if (stdout) {
process.stdout.write(stdout);
}
if (stderr) {
process.stderr.write(stderr);
}
if (error) {
reject(error);
return;
}
resolvePromise({ stdout, stderr });
},
);
if (options.input !== undefined) {
child.stdin?.end(options.input);
}
});
}
export function npmSmokePackageJson(layout) {
return {
name: 'ktx-artifact-npm-smoke',
version: '0.0.0',
private: true,
type: 'module',
dependencies: {
'@kaelio/ktx': `file:${layout.cliTarball}`,
},
};
}
export function npmSmokePnpmWorkspaceYaml() {
return ['packages:', ' - "."', 'allowBuilds:', ' better-sqlite3: true', ''].join('\n');
}
export function npmVerifySource() {
return `
const cli = await import('@kaelio/ktx');
if (cli.getKtxCliPackageInfo().name !== '@kaelio/ktx') {
throw new Error('Unexpected @kaelio/ktx package info');
}
if (typeof cli.runKtxCli !== 'function') {
throw new Error('Missing runKtxCli export');
}
`;
}
export function npmRuntimeSmokeSource() {
return `
import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { access, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
async function run(command, args, options = {}) {
process.stdout.write('$ ' + command + ' ' + args.join(' ') + '\\n');
try {
const result = await execFileAsync(command, args, {
cwd: options.cwd,
encoding: 'utf8',
timeout: 30_000,
});
return { code: 0, stdout: result.stdout, stderr: result.stderr };
} catch (error) {
return {
code: typeof error.code === 'number' ? error.code : 1,
stdout: error.stdout ?? '',
stderr: error.stderr ?? error.message,
};
}
}
function requireSuccess(label, result) {
assert.equal(
result.code,
0,
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
);
assert.equal(result.stderr, '', label + ' wrote unexpected stderr');
}
function requireSuccessWithProjectStderr(label, result, projectDir) {
assert.equal(
result.code,
0,
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
);
assert.equal(result.stderr, 'Project: ' + projectDir + '\\n', label + ' wrote unexpected stderr');
}
function requireExitCodeWithProjectStderr(label, result, projectDir, expectedCode) {
assert.equal(
result.code,
expectedCode,
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
);
assert.equal(result.stderr, 'Project: ' + projectDir + '\\n', label + ' wrote unexpected stderr');
}
function requireSuccessWithStderr(label, result, stderrPattern) {
assert.equal(
result.code,
0,
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
);
assert.match(result.stderr, stderrPattern, label + ' stderr did not match ' + stderrPattern);
}
function requireOutput(label, result, text) {
assert.match(result.stdout, text, label + ' output did not match ' + text);
}
function escapeRegExp(value) {
return value.replace(/[|\\\\{}()[\\]^$+*?.]/g, '\\\\$&');
}
async function installedPackageVersionPattern() {
const packageJson = JSON.parse(await readFile(require.resolve('@kaelio/ktx/package.json'), 'utf8'));
return new RegExp('^' + escapeRegExp(packageJson.name) + ' ' + escapeRegExp(packageJson.version) + '$', 'm');
}
function parseJsonResult(label, result) {
requireSuccess(label, result);
return JSON.parse(result.stdout);
}
function parseJsonResultWithExitCode(label, result, expectedCode) {
assert.equal(
result.code,
expectedCode,
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
);
return JSON.parse(result.stdout);
}
function requireIncludes(values, expected, label) {
assert.ok(Array.isArray(values), label + ' must be an array');
assert.ok(values.includes(expected), label + ' did not include ' + expected + ': ' + values.join(', '));
}
async function writeSqliteWarehouse(projectDir) {
const database = new DatabaseSync(join(projectDir, 'warehouse.db'));
try {
database.exec(\`
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
status TEXT NOT NULL,
amount INTEGER NOT NULL
);
INSERT INTO orders (status, amount) VALUES ('paid', 20), ('paid', 30), ('open', 10);
\`);
} finally {
database.close();
}
}
const root = await mkdtemp(join(tmpdir(), 'ktx-installed-cli-smoke-'));
const previousRuntimeRoot = process.env.KTX_RUNTIME_ROOT;
process.env.KTX_RUNTIME_ROOT = join(root, 'managed-runtime');
let daemonStarted = false;
try {
const projectDir = join(root, 'project');
const version = await run('pnpm', ['exec', 'ktx', '--version']);
requireSuccess('ktx public package version', version);
requireOutput('ktx public package version', version, await installedPackageVersionPattern());
const runtimeStatusBefore = parseJsonResultWithExitCode(
'ktx admin runtime status missing',
await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'status', '--json']),
1,
);
assert.equal(runtimeStatusBefore.kind, 'missing');
assert.equal(runtimeStatusBefore.layout.runtimeRoot, process.env.KTX_RUNTIME_ROOT);
process.stdout.write('ktx managed runtime starts missing in isolated root\\n');
const init = await run('pnpm', [
'exec',
'ktx',
'setup',
'--project-dir',
projectDir,
'--no-input',
'--yes',
'--skip-llm',
'--skip-embeddings',
'--skip-databases',
'--skip-sources',
'--skip-agents',
]);
requireSuccess('ktx setup', init);
const emptyProjectDir = join(root, 'empty-project');
const emptyInit = await run('pnpm', [
'exec',
'ktx',
'setup',
'--project-dir',
emptyProjectDir,
'--no-input',
'--yes',
'--skip-llm',
'--skip-embeddings',
'--skip-databases',
'--skip-sources',
'--skip-agents',
]);
requireSuccess('ktx setup empty project', emptyInit);
await writeFile(
join(projectDir, 'ktx.yaml'),
[
'connections:',
' warehouse:',
' driver: sqlite',
' path: warehouse.db',
'storage:',
' state: sqlite',
' search: sqlite-fts5',
'scan:',
' enrichment:',
' mode: deterministic',
'',
].join('\\n'),
'utf-8',
);
await writeSqliteWarehouse(projectDir);
await mkdir(join(projectDir, 'wiki', 'global'), { recursive: true });
await writeFile(
join(projectDir, 'wiki', 'global', 'revenue.md'),
[
'---',
'summary: Paid order value',
'tags:',
' - finance',
'refs: []',
'sl_refs: []',
'usage_mode: auto',
'---',
'',
'Revenue is the sum of paid order amounts.',
'',
].join('\\n'),
'utf-8',
);
const wikiSearch = await run('pnpm', [
'exec',
'ktx',
'wiki',
'revenue',
'--json',
'--limit',
'5',
'--project-dir',
projectDir,
]);
const wikiSearchJson = parseJsonResult('ktx wiki search', wikiSearch);
assert.equal(wikiSearchJson.kind, 'list');
assert.equal(wikiSearchJson.data.items.length, 1);
assert.equal(wikiSearchJson.data.items[0].key, 'revenue');
assert.equal(wikiSearchJson.data.items[0].path, 'wiki/global/revenue.md');
assert.equal(typeof wikiSearchJson.data.items[0].score, 'number');
requireIncludes(wikiSearchJson.data.items[0].matchReasons, 'lexical', 'wiki search match reasons');
process.stdout.write('ktx wiki search hybrid metadata verified\\n');
await access(join(projectDir, '.ktx', 'db.sqlite'));
process.stdout.write('SQLite wiki index: ' + join(projectDir, '.ktx', 'db.sqlite') + '\\n');
const slYaml = [
'name: orders',
'table: orders',
'grain:',
' - id',
'columns:',
' - name: id',
' type: number',
' - name: amount',
' type: number',
'measures:',
' - name: order_count',
' expr: count(*)',
'joins: []',
'',
].join('\\n');
await mkdir(join(projectDir, 'semantic-layer', 'warehouse'), { recursive: true });
await writeFile(join(projectDir, 'semantic-layer', 'warehouse', 'orders.yaml'), slYaml, 'utf-8');
const slSearch = await run('pnpm', [
'exec',
'ktx',
'sl',
'orders',
'--json',
'--connection-id',
'warehouse',
'--project-dir',
projectDir,
]);
const slSearchJson = parseJsonResult('ktx sl search', slSearch);
assert.equal(slSearchJson.kind, 'list');
assert.equal(slSearchJson.data.items.length, 1);
assert.equal(slSearchJson.data.items[0].connectionId, 'warehouse');
assert.equal(slSearchJson.data.items[0].name, 'orders');
assert.equal(typeof slSearchJson.data.items[0].score, 'number');
requireIncludes(slSearchJson.data.items[0].matchReasons, 'lexical', 'sl search match reasons');
process.stdout.write('ktx sl search hybrid metadata verified\\n');
const slQuery = await run('pnpm', ['exec', 'ktx', 'sl', 'query',
'--connection-id',
'warehouse',
'--measure',
'orders.order_count',
'--format',
'json',
'--yes',
'--project-dir',
projectDir,
]);
requireSuccessWithStderr(
'ktx sl query first managed runtime install',
slQuery,
/Installing KTX Python runtime \\(core\\) with uv[\\s\\S]*KTX Python runtime ready:/,
);
requireOutput('ktx sl query first managed runtime install', slQuery, /"mode": "compile_only"/);
requireOutput('ktx sl query first managed runtime install', slQuery, /orders/);
const runtimeStatusAfter = parseJsonResult(
'ktx admin runtime status ready',
await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'status', '--json']),
);
assert.equal(runtimeStatusAfter.kind, 'ready');
assert.deepEqual(runtimeStatusAfter.manifest.features, ['core']);
assert.equal(runtimeStatusAfter.layout.runtimeRoot, process.env.KTX_RUNTIME_ROOT);
process.stdout.write('ktx managed runtime lazy install verified\\n');
const sqliteSlQuery = await run('pnpm', ['exec', 'ktx', 'sl', 'query',
'--connection-id',
'warehouse',
'--measure',
'orders.order_count',
'--format',
'json',
'--execute',
'--max-rows',
'100',
'--yes',
'--project-dir',
projectDir,
]);
requireSuccess('ktx sl query sqlite execute', sqliteSlQuery);
requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"dialect": "sqlite"/);
requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"mode": "executed"/);
requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"driver": "sqlite"/);
requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"rows": \\[\\s*\\[\\s*3\\s*\\]\\s*\\]/);
process.stdout.write('ktx sl query sqlite execute verified\\n');
const runtimeDoctor = await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'status']);
requireSuccess('ktx admin runtime status', runtimeDoctor);
requireOutput('ktx admin runtime status', runtimeDoctor, /KTX Python runtime/);
requireOutput('ktx admin runtime status', runtimeDoctor, /status: ready/);
process.stdout.write('ktx admin runtime status verified\\n');
const runtimeStart = await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'start']);
requireSuccess('ktx admin runtime start', runtimeStart);
daemonStarted = true;
requireOutput('ktx admin runtime start', runtimeStart, /Started KTX daemon/);
requireOutput('ktx admin runtime start', runtimeStart, /url: http:\\/\\/127\\.0\\.0\\.1:\\d+/);
requireOutput('ktx admin runtime start', runtimeStart, /features: core/);
const runtimeStartReuse = await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'start']);
requireSuccess('ktx admin runtime start reuse', runtimeStartReuse);
requireOutput('ktx admin runtime start reuse', runtimeStartReuse, /Using existing KTX daemon/);
requireOutput('ktx admin runtime start reuse', runtimeStartReuse, /features: core/);
const runtimeStop = await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'stop']);
requireSuccess('ktx admin runtime stop', runtimeStop);
daemonStarted = false;
requireOutput('ktx admin runtime stop', runtimeStop, /Stopped KTX daemon/);
process.stdout.write('ktx admin runtime daemon lifecycle verified\\n');
const structuralScan = await run('pnpm', ['exec', 'ktx', 'ingest', 'warehouse',
'--project-dir',
projectDir,
'--fast',
'--no-input',
]);
requireSuccessWithProjectStderr('ktx ingest fast', structuralScan, projectDir);
requireOutput('ktx ingest fast', structuralScan, /Ingest finished/);
requireOutput('ktx ingest fast', structuralScan, /Database schema/);
requireOutput('ktx ingest fast', structuralScan, /warehouse\\s+done/);
await access(join(projectDir, 'semantic-layer', 'warehouse', '_schema', 'public.yaml'));
process.stdout.write('ktx ingest fast verified\\n');
const enrichedScan = await run('pnpm', ['exec', 'ktx', 'ingest', 'warehouse',
'--project-dir',
projectDir,
'--deep',
'--no-input',
]);
requireExitCodeWithProjectStderr('ktx ingest deep readiness guard', enrichedScan, projectDir, 1);
requireOutput('ktx ingest deep readiness guard', enrichedScan, /Ingest finished with partial failures/);
requireOutput('ktx ingest deep readiness guard', enrichedScan, /requires deep ingest readiness/);
process.stdout.write('ktx ingest deep readiness guard verified\\n');
await access(join(projectDir, '.ktx', 'db.sqlite'));
process.stdout.write('ktx ingest state verified\\n');
} finally {
if (daemonStarted) {
await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'stop']);
}
if (previousRuntimeRoot === undefined) {
delete process.env.KTX_RUNTIME_ROOT;
} else {
process.env.KTX_RUNTIME_ROOT = previousRuntimeRoot;
}
await rm(root, { recursive: true, force: true });
}
`;
}
export function npmCliSmokeSource() {
return `
import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
async function run(command, args, options = {}) {
process.stdout.write('$ ' + command + ' ' + args.join(' ') + '\\n');
try {
const result = await execFileAsync(command, args, {
cwd: options.cwd,
env: options.env ?? process.env,
encoding: 'utf8',
timeout: 45_000,
});
return { code: 0, stdout: result.stdout, stderr: result.stderr };
} catch (error) {
return {
code: typeof error.code === 'number' ? error.code : 1,
stdout: error.stdout ?? '',
stderr: error.stderr ?? error.message,
};
}
}
function requireSuccess(label, result) {
assert.equal(
result.code,
0,
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
);
}
function requireStdout(label, result, pattern) {
assert.match(result.stdout, pattern, label + ' stdout did not match ' + pattern);
}
const root = await mkdtemp(join(tmpdir(), 'ktx-cli-smoke-'));
try {
const packageJson = JSON.parse(await readFile(join(process.cwd(), 'package.json'), 'utf8'));
assert.deepEqual(Object.keys(packageJson.dependencies), ['@kaelio/ktx']);
const help = await run('pnpm', ['exec', 'ktx', '--help']);
requireSuccess('ktx --help', help);
requireStdout('ktx --help', help, /Usage: ktx/);
requireStdout('ktx --help', help, /setup/);
const setupHelp = await run('pnpm', ['exec', 'ktx', 'setup', '--help']);
requireSuccess('ktx setup --help', setupHelp);
requireStdout('ktx setup --help', setupHelp, /Usage: ktx setup/);
requireStdout('ktx setup --help', setupHelp, /--no-input/);
const doctor = await run('pnpm', ['exec', 'ktx', 'status', '--verbose', '--no-input']);
assert.ok([0, 1].includes(doctor.code), 'ktx status setup exit code must be 0 or 1');
requireStdout('ktx status setup', doctor, /KTX status/);
requireStdout('ktx status setup', doctor, /No project here yet\\./);
requireStdout('ktx status setup', doctor, /ktx setup/);
requireStdout('ktx status setup', doctor, /Node 22\\+/);
assert.equal(doctor.stderr, '', 'ktx status setup wrote unexpected stderr');
} finally {
await rm(root, { recursive: true, force: true });
}
`;
}
async function buildArtifacts(layout) {
await rm(layout.artifactDir, { recursive: true, force: true });
await mkdir(layout.npmDir, { recursive: true });
await mkdir(layout.pythonDir, { recursive: true });
const [npmBuildCommand, wheelCommand, publicPackageCommand] = buildArtifactCommands(layout);
await runCommand(npmBuildCommand.command, npmBuildCommand.args, { cwd: npmBuildCommand.cwd });
await runCommand(wheelCommand.command, wheelCommand.args, { cwd: wheelCommand.cwd });
const pythonArtifacts = await findPythonArtifacts(layout.pythonDir);
await copyRuntimeWheelAssets(layout, pythonArtifacts);
await runCommand(publicPackageCommand.command, publicPackageCommand.args, { cwd: publicPackageCommand.cwd });
for (const packageInfo of NPM_ARTIFACT_PACKAGES) {
await assertPathExists(layout.npmTarballs[packageInfo.name], `${packageInfo.name} tarball`);
}
await writeArtifactManifest(layout);
await assertPathExists(artifactManifestPath(layout), 'artifact manifest');
}
async function buildRuntimeWheelAssets(layout) {
await rm(layout.pythonDir, { recursive: true, force: true });
await mkdir(layout.pythonDir, { recursive: true });
const [, wheelCommand] = buildArtifactCommands(layout);
await runCommand(wheelCommand.command, wheelCommand.args, { cwd: wheelCommand.cwd });
const pythonArtifacts = await findPythonArtifacts(layout.pythonDir);
await copyRuntimeWheelAssets(layout, pythonArtifacts);
}
async function verifyNpmArtifacts(layout, tmpRoot) {
for (const packageInfo of NPM_ARTIFACT_PACKAGES) {
await assertPathExists(layout.npmTarballs[packageInfo.name], `${packageInfo.name} tarball`);
}
const projectDir = join(tmpRoot, 'npm-clean-install');
await mkdir(projectDir, { recursive: true });
await writeFile(
join(projectDir, 'package.json'),
`${JSON.stringify(npmSmokePackageJson(layout), null, 2)}\n`,
);
await writeFile(join(projectDir, 'pnpm-workspace.yaml'), npmSmokePnpmWorkspaceYaml());
await writeFile(join(projectDir, 'verify-npm.mjs'), npmVerifySource());
await writeFile(join(projectDir, 'verify-installed-cli.mjs'), npmRuntimeSmokeSource());
await writeFile(join(projectDir, 'verify-installed-cli-commands.mjs'), npmCliSmokeSource());
await runCommand('pnpm', ['install'], { cwd: projectDir });
await runCommand('pnpm', ['rebuild', 'better-sqlite3'], { cwd: projectDir });
await runCommand('node', ['verify-npm.mjs'], { cwd: projectDir });
await runCommand('pnpm', ['exec', 'ktx', '--version'], { cwd: projectDir });
await runCommand('node', ['verify-installed-cli.mjs'], { cwd: projectDir });
await runCommand('node', ['verify-installed-cli-commands.mjs'], { cwd: projectDir });
}
async function verifyNpmCliArtifacts(layout, tmpRoot) {
for (const packageInfo of NPM_ARTIFACT_PACKAGES) {
await assertPathExists(layout.npmTarballs[packageInfo.name], `${packageInfo.name} tarball`);
}
const projectDir = join(tmpRoot, 'npm-cli-clean-install');
await mkdir(projectDir, { recursive: true });
await writeFile(join(projectDir, 'package.json'), `${JSON.stringify(npmSmokePackageJson(layout), null, 2)}\n`);
await writeFile(join(projectDir, 'pnpm-workspace.yaml'), npmSmokePnpmWorkspaceYaml());
await writeFile(join(projectDir, 'verify-installed-cli-commands.mjs'), npmCliSmokeSource());
await runCommand('pnpm', ['install'], { cwd: projectDir });
await runCommand('node', ['verify-installed-cli-commands.mjs'], { cwd: projectDir });
}
async function verifyArtifacts(layout) {
await verifyArtifactManifest(layout);
const tmpRoot = await mkdtemp(join(tmpdir(), 'ktx-artifacts-'));
try {
await verifyNpmArtifacts(layout, tmpRoot);
} finally {
await rm(tmpRoot, { recursive: true, force: true });
}
}
async function verifyCliArtifacts(layout) {
await verifyArtifactManifest(layout);
const tmpRoot = await mkdtemp(join(tmpdir(), 'ktx-cli-artifacts-'));
try {
await verifyNpmCliArtifacts(layout, tmpRoot);
} finally {
await rm(tmpRoot, { recursive: true, force: true });
}
}
async function main() {
const command = process.argv[2] ?? 'check';
const layout = packageArtifactLayout();
if (command === 'build') {
await buildArtifacts(layout);
return;
}
if (command === 'build-runtime') {
await buildRuntimeWheelAssets(layout);
return;
}
if (command === 'verify') {
await verifyArtifacts(layout);
return;
}
if (command === 'verify-demo') {
await verifyCliArtifacts(layout);
return;
}
if (command === 'verify-manifest') {
await verifyArtifactManifest(layout);
return;
}
if (command === 'check') {
await buildArtifacts(layout);
await verifyArtifacts(layout);
return;
}
throw new Error(`Unknown package artifact command: ${command}`);
}
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;
}
}