ktx/scripts/package-artifacts.mjs
Andrey Avtomonov 17a2fee69a
fix(cli): remove ktx setup subcommands (#42)
* fix(cli): remove ktx setup subcommands

* test(scripts): update setup-dev status expectation
2026-05-13 00:38:26 +02:00

1198 lines
41 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,
PUBLIC_NPM_PACKAGE_VERSION,
publicNpmPackageTarballName,
} from './build-public-npm-package.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';
const CONNECTOR_PACKAGE_NAMES = INTERNAL_NPM_WORKSPACE_PACKAGES
.map((packageInfo) => packageInfo.name)
.filter((packageName) => packageName.startsWith('@ktx/connector-'));
const NPM_ARTIFACT_BUILD_ORDER = ['@ktx/llm', '@ktx/context', ...CONNECTOR_PACKAGE_NAMES, '@ktx/cli'];
function scriptRootDir() {
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
}
function npmPackageTarballName(packageName) {
if (packageName !== PUBLIC_NPM_PACKAGE_NAME) {
throw new Error(`Unsupported npm artifact package: ${packageName}`);
}
return publicNpmPackageTarballName(PUBLIC_NPM_PACKAGE_VERSION);
}
function npmPackageTarballs(npmDir) {
return Object.fromEntries(
NPM_ARTIFACT_PACKAGES.map((packageInfo) => [packageInfo.name, join(npmDir, npmPackageTarballName(packageInfo.name))]),
);
}
export function packageArtifactLayout(rootDir = scriptRootDir()) {
const artifactDir = join(rootDir, 'dist', 'artifacts');
const npmDir = join(artifactDir, 'npm');
const pythonDir = join(artifactDir, 'python');
const npmTarballs = npmPackageTarballs(npmDir);
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) {
const packagesByName = new Map(INTERNAL_NPM_WORKSPACE_PACKAGES.map((packageInfo) => [packageInfo.name, packageInfo]));
const npmBuildCommands = NPM_ARTIFACT_BUILD_ORDER.map((packageName) => {
const packageInfo = packagesByName.get(packageName);
if (!packageInfo) {
throw new Error(`Unknown npm artifact build package: ${packageName}`);
}
return {
command: 'pnpm',
args: ['--filter', packageInfo.name, 'run', 'build'],
cwd: layout.rootDir,
};
});
const publicPackageCommand = {
command: process.execPath,
args: ['scripts/build-public-npm-package.mjs'],
cwd: layout.rootDir,
};
return [
...npmBuildCommands,
{
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 dev 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) {
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 ? PUBLIC_NPM_PACKAGE_VERSION : packageJson.version,
privatePackage: isPublicKtxPackage ? false : packageJson.private === true,
});
}
export async function packageReleaseMetadata(rootDir = scriptRootDir()) {
const npmPackages = await Promise.all(
NPM_ARTIFACT_PACKAGES.map((packageInfo) => readNpmPackageMetadata(rootDir, packageInfo)),
);
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}`,
},
devDependencies: {
'better-sqlite3': '^12.6.2',
},
pnpm: {
onlyBuiltDependencies: ['better-sqlite3'],
},
};
}
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 Database from 'better-sqlite3';
import { execFile } from 'node:child_process';
import { access, mkdir, mkdtemp, rm, writeFile } 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,
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 requireProjectStderr(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 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 parseJsonResult(label, result) {
requireSuccess(label, result);
return JSON.parse(result.stdout);
}
function parseJsonFailure(label, result) {
assert.equal(result.code, 1, label + ' should fail with exit code 1');
assert.equal(result.stdout, '', label + ' should not write stdout when failing');
return JSON.parse(result.stderr);
}
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(', '));
}
function getRunId(stdout) {
const match = stdout.match(/^Run: (.+)$/m);
assert.ok(match, 'ingest run output did not include a run id');
return match[1];
}
async function writeSqliteWarehouse(projectDir) {
const database = new Database(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 sourceDir = join(root, 'source');
const version = await run('pnpm', ['exec', 'ktx', '--version']);
requireSuccess('ktx public package version', version);
requireOutput('ktx public package version', version, /@kaelio\\/ktx 0\\.1\\.0/);
const runtimeStatusBefore = parseJsonResult(
'ktx dev runtime status missing',
await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'status', '--json']),
);
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 missingProjectDir = join(root, 'missing-project');
await mkdir(missingProjectDir, { recursive: true });
const missingProjectSearch = await run('pnpm', [
'exec',
'ktx',
'agent',
'sl',
'list',
'--json',
'--query',
'revenue',
'--project-dir',
missingProjectDir,
]);
const missingProjectError = parseJsonFailure('ktx agent sl list missing project', missingProjectSearch);
assert.equal(missingProjectError.error.code, 'agent_sl_search_missing_project');
assert.deepEqual(missingProjectError.error.nextSteps, [
'ktx setup --project-dir ' + missingProjectDir,
'ktx status --project-dir ' + missingProjectDir,
'ktx ingest <connection>',
'ktx agent sl list --json --query "revenue" --project-dir ' + missingProjectDir,
]);
process.stdout.write('ktx agent sl list missing project guidance verified\\n');
const init = await run('pnpm', [
'exec',
'ktx',
'setup',
'--project-dir',
projectDir,
'--new',
'--no-input',
'--yes',
'--skip-llm',
'--skip-embeddings',
'--skip-databases',
'--skip-sources',
'--skip-agents',
]);
requireProjectStderr('ktx setup', init, projectDir);
requireOutput('ktx setup', init, /Project: /);
const emptyProjectDir = join(root, 'empty-project');
const emptyInit = await run('pnpm', [
'exec',
'ktx',
'setup',
'--project-dir',
emptyProjectDir,
'--new',
'--no-input',
'--yes',
'--skip-llm',
'--skip-embeddings',
'--skip-databases',
'--skip-sources',
'--skip-agents',
]);
requireProjectStderr('ktx setup empty project', emptyInit, emptyProjectDir);
const emptySearch = await run('pnpm', [
'exec',
'ktx',
'agent',
'sl',
'list',
'--json',
'--query',
'revenue',
'--project-dir',
emptyProjectDir,
]);
const emptySearchError = parseJsonFailure('ktx agent sl list no connections', emptySearch);
assert.equal(emptySearchError.error.code, 'agent_sl_search_no_connections');
assert.deepEqual(emptySearchError.error.nextSteps, [
'ktx setup --project-dir ' + emptyProjectDir,
'ktx status --project-dir ' + emptyProjectDir,
'ktx ingest <connection>',
'ktx agent sl list --json --query "revenue" --project-dir ' + emptyProjectDir,
]);
process.stdout.write('ktx agent sl list no connections guidance verified\\n');
await writeFile(
join(projectDir, 'ktx.yaml'),
[
'project: warehouse',
'connections:',
' warehouse:',
' driver: sqlite',
' path: warehouse.db',
' readonly: true',
'storage:',
' state: sqlite',
' search: sqlite-fts5',
'scan:',
' enrichment:',
' mode: deterministic',
'ingest:',
' adapters:',
' - fake',
' - live-database',
'',
].join('\\n'),
'utf-8',
);
await writeSqliteWarehouse(projectDir);
await mkdir(join(projectDir, 'knowledge', 'global'), { recursive: true });
await writeFile(
join(projectDir, 'knowledge', '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 agentWikiSearch = await run('pnpm', [
'exec',
'ktx',
'agent',
'wiki',
'search',
'revenue',
'--json',
'--limit',
'5',
'--project-dir',
projectDir,
]);
const agentWikiSearchJson = parseJsonResult('ktx agent wiki search', agentWikiSearch);
assert.equal(agentWikiSearchJson.totalFound, 1);
assert.equal(agentWikiSearchJson.results[0].key, 'revenue');
assert.equal(agentWikiSearchJson.results[0].path, 'knowledge/global/revenue.md');
assert.equal(typeof agentWikiSearchJson.results[0].score, 'number');
requireIncludes(agentWikiSearchJson.results[0].matchReasons, 'lexical', 'agent wiki search match reasons');
process.stdout.write('ktx agent wiki search hybrid metadata verified\\n');
await access(join(projectDir, '.ktx', 'db.sqlite'));
process.stdout.write('SQLite knowledge index: ' + join(projectDir, '.ktx', 'db.sqlite') + '\\n');
const noSourceSearch = await run('pnpm', [
'exec',
'ktx',
'agent',
'sl',
'list',
'--json',
'--connection-id',
'warehouse',
'--query',
'revenue',
'--project-dir',
projectDir,
]);
const noSourceSearchError = parseJsonFailure('ktx agent sl list no indexed sources', noSourceSearch);
assert.equal(noSourceSearchError.error.code, 'agent_sl_search_no_indexed_sources');
assert.deepEqual(noSourceSearchError.error.nextSteps, [
'ktx setup --project-dir ' + projectDir,
'ktx status --project-dir ' + projectDir,
'ktx ingest <connection>',
'ktx agent sl list --json --query "revenue" --project-dir ' + projectDir,
]);
process.stdout.write('ktx agent sl list no indexed sources guidance verified\\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 agentSlSearch = await run('pnpm', [
'exec',
'ktx',
'agent',
'sl',
'list',
'--json',
'--connection-id',
'warehouse',
'--query',
'orders',
'--project-dir',
projectDir,
]);
const agentSlSearchJson = parseJsonResult('ktx agent sl list', agentSlSearch);
assert.equal(agentSlSearchJson.totalSources, 1);
assert.equal(agentSlSearchJson.sources[0].connectionId, 'warehouse');
assert.equal(agentSlSearchJson.sources[0].name, 'orders');
assert.equal(typeof agentSlSearchJson.sources[0].score, 'number');
requireIncludes(agentSlSearchJson.sources[0].matchReasons, 'lexical', 'agent sl search match reasons');
process.stdout.write('ktx agent sl list 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 dev runtime status ready',
await run('pnpm', ['exec', 'ktx', 'dev', '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', 'dev', 'runtime', 'doctor']);
requireSuccess('ktx dev runtime doctor', runtimeDoctor);
requireOutput('ktx dev runtime doctor', runtimeDoctor, /PASS uv/);
requireOutput('ktx dev runtime doctor', runtimeDoctor, /PASS Bundled Python wheel/);
requireOutput('ktx dev runtime doctor', runtimeDoctor, /PASS Managed Python runtime/);
process.stdout.write('ktx dev runtime doctor verified\\n');
const runtimeStart = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'start']);
requireSuccess('ktx dev runtime start', runtimeStart);
daemonStarted = true;
requireOutput('ktx dev runtime start', runtimeStart, /Started KTX Python daemon/);
requireOutput('ktx dev runtime start', runtimeStart, /url: http:\\/\\/127\\.0\\.0\\.1:\\d+/);
requireOutput('ktx dev runtime start', runtimeStart, /features: core/);
const runtimeStartReuse = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'start']);
requireSuccess('ktx dev runtime start reuse', runtimeStartReuse);
requireOutput('ktx dev runtime start reuse', runtimeStartReuse, /Using existing KTX Python daemon/);
requireOutput('ktx dev runtime start reuse', runtimeStartReuse, /features: core/);
const runtimeStop = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'stop']);
requireSuccess('ktx dev runtime stop', runtimeStop);
daemonStarted = false;
requireOutput('ktx dev runtime stop', runtimeStop, /Stopped KTX Python daemon/);
process.stdout.write('ktx dev runtime daemon lifecycle verified\\n');
const staleRuntimeDir = join(process.env.KTX_RUNTIME_ROOT, '0.0.0');
await mkdir(staleRuntimeDir, { recursive: true });
const runtimePruneDryRun = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'prune', '--dry-run']);
requireSuccess('ktx dev runtime prune dry run', runtimePruneDryRun);
requireOutput('ktx dev runtime prune dry run', runtimePruneDryRun, /Stale KTX Python runtimes/);
requireOutput('ktx dev runtime prune dry run', runtimePruneDryRun, /0\\.0\\.0/);
await access(staleRuntimeDir);
const runtimePruneNeedsConfirmation = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'prune']);
assert.equal(runtimePruneNeedsConfirmation.code, 1, 'ktx dev runtime prune needs confirmation');
assert.equal(runtimePruneNeedsConfirmation.stdout, '', 'ktx dev runtime prune needs confirmation wrote stdout');
assert.match(runtimePruneNeedsConfirmation.stderr, /Refusing to prune without --yes/);
const runtimePruneConfirmed = await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'prune', '--yes']);
requireSuccess('ktx dev runtime prune confirmed', runtimePruneConfirmed);
requireOutput('ktx dev runtime prune confirmed', runtimePruneConfirmed, /Removed stale KTX Python runtimes/);
requireOutput('ktx dev runtime prune confirmed', runtimePruneConfirmed, /0\\.0\\.0/);
await assert.rejects(() => access(staleRuntimeDir));
process.stdout.write('ktx dev runtime prune verified\\n');
const structuralScan = await run('pnpm', ['exec', 'ktx', 'dev', 'scan', 'warehouse',
'--project-dir',
projectDir,
]);
requireProjectStderr('ktx scan structural', structuralScan, projectDir);
requireOutput('ktx scan structural', structuralScan, /Status: done/);
requireOutput('ktx scan structural', structuralScan, /Mode: structural/);
requireOutput('ktx scan structural', structuralScan, /Needs attention\\s+None/);
const structuralScanRunId = getRunId(structuralScan.stdout);
const scanStatus = await run('pnpm', ['exec', 'ktx', 'dev', 'scan', 'status',
'--project-dir',
projectDir,
structuralScanRunId,
]);
requireProjectStderr('ktx scan status', scanStatus, projectDir);
requireOutput('ktx scan status', scanStatus, new RegExp('Run: ' + structuralScanRunId));
requireOutput('ktx scan status', scanStatus, /Status: done/);
requireOutput('ktx scan status', scanStatus, /Mode: structural/);
const scanReport = await run('pnpm', ['exec', 'ktx', 'dev', 'scan', 'report',
'--project-dir',
projectDir,
'--json',
structuralScanRunId,
]);
requireSuccess('ktx scan report', scanReport);
const scanReportJson = JSON.parse(scanReport.stdout);
assert.equal(scanReportJson.mode, 'structural');
assert.equal(scanReportJson.connectionId, 'warehouse');
assert.equal(scanReportJson.manifestShardsWritten, 1);
assert.deepEqual(scanReportJson.artifactPaths.enrichmentArtifacts, []);
assert.deepEqual(scanReportJson.artifactPaths.manifestShards, ['semantic-layer/warehouse/_schema/public.yaml']);
await access(join(projectDir, 'semantic-layer', 'warehouse', '_schema', 'public.yaml'));
process.stdout.write('ktx scan structural verified: ' + structuralScanRunId + '\\n');
const enrichedScan = await run('pnpm', ['exec', 'ktx', 'dev', 'scan', 'warehouse',
'--project-dir',
projectDir,
'--mode',
'enriched',
]);
requireProjectStderr('ktx scan enriched', enrichedScan, projectDir);
requireOutput('ktx scan enriched', enrichedScan, /Status: done/);
requireOutput('ktx scan enriched', enrichedScan, /Mode: enriched/);
const enrichedScanRunId = getRunId(enrichedScan.stdout);
const enrichedScanReport = await run('pnpm', ['exec', 'ktx', 'dev', 'scan', 'report',
'--project-dir',
projectDir,
'--json',
enrichedScanRunId,
]);
requireSuccess('ktx scan enriched report', enrichedScanReport);
const enrichedScanReportJson = JSON.parse(enrichedScanReport.stdout);
assert.equal(enrichedScanReportJson.mode, 'enriched');
assert.ok(enrichedScanReportJson.artifactPaths.enrichmentArtifacts.length > 0);
assert.deepEqual(enrichedScanReportJson.artifactPaths.manifestShards, ['semantic-layer/warehouse/_schema/public.yaml']);
process.stdout.write('ktx scan enriched verified: ' + enrichedScanRunId + '\\n');
await mkdir(join(sourceDir, 'orders'), { recursive: true });
await writeFile(join(sourceDir, 'orders', 'orders.json'), '{"name":"orders"}\\n', 'utf-8');
const ingestRun = await run('pnpm', ['exec', 'ktx', 'dev', 'ingest', 'run',
'--project-dir',
projectDir,
'--connection-id',
'warehouse',
'--adapter',
'fake',
'--source-dir',
sourceDir,
]);
assert.equal(ingestRun.code, 1, 'ktx dev ingest run without an LLM provider must fail');
assert.match(
ingestRun.stderr,
/ktx dev ingest run requires llm\\.provider\\.backend: anthropic, vertex, or gateway, or an injected agentRunner/,
);
await access(join(projectDir, '.ktx', 'db.sqlite'));
process.stdout.write('ktx dev ingest provider guard verified\\n');
} finally {
if (daemonStarted) {
await run('pnpm', ['exec', 'ktx', 'dev', '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', '--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 setup doctor/);
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 commands = buildArtifactCommands(layout);
const npmBuildCount = NPM_ARTIFACT_BUILD_ORDER.length;
const npmPackStart = commands.length - 1;
for (const command of commands.slice(0, npmBuildCount)) {
await runCommand(command.command, command.args, { cwd: command.cwd });
}
for (const command of commands.slice(npmBuildCount, npmPackStart)) {
await runCommand(command.command, command.args, { cwd: command.cwd });
}
const pythonArtifacts = await findPythonArtifacts(layout.pythonDir);
await copyRuntimeWheelAssets(layout, pythonArtifacts);
for (const command of commands.slice(npmPackStart)) {
await runCommand(command.command, command.args, { cwd: command.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 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, '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, '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 === '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;
}
}