mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
fix: improve setup wizard behavior (#127)
* fix: improve setup wizard behavior * fix: derive runtime versions from release metadata * test: validate metabase source mapping requirements * Fix boundary check release identifiers
This commit is contained in:
parent
33a142f769
commit
d1c84e5564
35 changed files with 671 additions and 90 deletions
|
|
@ -31,6 +31,9 @@ ktx dev <subcommand> [options]
|
||||||
|
|
||||||
## `dev schema`
|
## `dev schema`
|
||||||
|
|
||||||
|
`ktx dev schema` does not require a `ktx.yaml` file or a configured project
|
||||||
|
directory. Use it from any directory to generate editor or agent schema files.
|
||||||
|
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------|-------------|---------|
|
|------|-------------|---------|
|
||||||
| `--output <file>` | Write the schema to a file instead of stdout | — |
|
| `--output <file>` | Write the schema to a file instead of stdout | — |
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,9 @@ Setup supports three LLM provider paths:
|
||||||
|
|
||||||
| Provider | Use when | Credential model |
|
| Provider | Use when | Credential model |
|
||||||
|----------|----------|------------------|
|
|----------|----------|------------------|
|
||||||
| Anthropic API | You have an Anthropic API key | `ANTHROPIC_API_KEY` or a local `file:` secret |
|
| Claude subscription (Pro/Max) | You want KTX to use your local Claude Code session | Claude Code local authentication |
|
||||||
|
| Anthropic API key | You have an Anthropic API key | `ANTHROPIC_API_KEY` or a local `file:` secret |
|
||||||
| Google Vertex AI for Anthropic Claude | Your organization runs Claude through Google Cloud | Application Default Credentials plus Vertex project and location |
|
| Google Vertex AI for Anthropic Claude | Your organization runs Claude through Google Cloud | Application Default Credentials plus Vertex project and location |
|
||||||
| Claude Code | You want KTX to use your local Claude Code session | Claude Code local authentication |
|
|
||||||
|
|
||||||
For Anthropic API, setup can read the key from the environment or save a pasted
|
For Anthropic API, setup can read the key from the environment or save a pasted
|
||||||
key to `.ktx/secrets/anthropic-api-key`. `ktx.yaml` stores an `env:` or `file:`
|
key to `.ktx/secrets/anthropic-api-key`. `ktx.yaml` stores an `env:` or `file:`
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,12 @@ The artifact packaging and readiness scripts read `publicNpmPackageVersion`
|
||||||
from `release-policy.json`, so manual version edits in build scripts aren't
|
from `release-policy.json`, so manual version edits in build scripts aren't
|
||||||
needed for rc releases.
|
needed for rc releases.
|
||||||
|
|
||||||
|
The bundled Python runtime wheel also derives its version from
|
||||||
|
`publicNpmPackageVersion`. Stable npm versions are reused as-is, and rc
|
||||||
|
versions are normalized to Python's version format. For example,
|
||||||
|
`0.1.0-rc.2` becomes `0.1.0rc2` in the `kaelio-ktx` wheel filename and wheel
|
||||||
|
metadata.
|
||||||
|
|
||||||
## npm authentication
|
## npm authentication
|
||||||
|
|
||||||
The release workflow publishes through npm Trusted Publishing. It doesn't use
|
The release workflow publishes through npm Trusted Publishing. It doesn't use
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { cancel, confirm, isCancel, log, spinner } from '@clack/prompts';
|
import { cancel, confirm, isCancel, log, spinner } from '@clack/prompts';
|
||||||
|
|
||||||
|
const ESC = String.fromCharCode(0x1b);
|
||||||
|
|
||||||
export interface KtxCliSpinner {
|
export interface KtxCliSpinner {
|
||||||
start(message: string): void;
|
start(message: string): void;
|
||||||
message(message: string): void;
|
message(message: string): void;
|
||||||
|
|
@ -7,6 +9,10 @@ export interface KtxCliSpinner {
|
||||||
error(message: string): void;
|
error(message: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KtxCliSpinnerIo {
|
||||||
|
stderr: { write(chunk: string): void };
|
||||||
|
}
|
||||||
|
|
||||||
export interface KtxCliPromptAdapter {
|
export interface KtxCliPromptAdapter {
|
||||||
confirm(options: { message: string; initialValue?: boolean }): Promise<boolean>;
|
confirm(options: { message: string; initialValue?: boolean }): Promise<boolean>;
|
||||||
cancel(message: string): void;
|
cancel(message: string): void;
|
||||||
|
|
@ -31,6 +37,31 @@ export function createClackSpinner(): KtxCliSpinner {
|
||||||
return spinner();
|
return spinner();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function magenta(text: string): string {
|
||||||
|
return `${ESC}[35m${text}${ESC}[39m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function red(text: string): string {
|
||||||
|
return `${ESC}[31m${text}${ESC}[39m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createStaticCliSpinner(io: KtxCliSpinnerIo): KtxCliSpinner {
|
||||||
|
return {
|
||||||
|
start(message) {
|
||||||
|
io.stderr.write(`${magenta('◐')} ${message}\n`);
|
||||||
|
},
|
||||||
|
message(message) {
|
||||||
|
io.stderr.write(`${magenta('│')} ${message}\n`);
|
||||||
|
},
|
||||||
|
stop(message) {
|
||||||
|
io.stderr.write(`${magenta('◇')} ${message}\n`);
|
||||||
|
},
|
||||||
|
error(message) {
|
||||||
|
io.stderr.write(`${red('■')} ${message}\n`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createClackPromptAdapter(): KtxCliPromptAdapter {
|
export function createClackPromptAdapter(): KtxCliPromptAdapter {
|
||||||
return {
|
return {
|
||||||
async confirm(options) {
|
async confirm(options) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,13 @@ function stubIo(): KtxCliIo {
|
||||||
}
|
}
|
||||||
|
|
||||||
function stubPackageInfo(): KtxCliPackageInfo {
|
function stubPackageInfo(): KtxCliPackageInfo {
|
||||||
return { name: '@ktx/cli', version: '0.0.0-test', contextPackageName: '@ktx/context' };
|
return {
|
||||||
|
name: '@ktx/cli',
|
||||||
|
version: '0.0.0-test',
|
||||||
|
packageVersion: '0.0.0-private',
|
||||||
|
runtimeVersion: '0.0.0-test',
|
||||||
|
contextPackageName: '@ktx/context',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('buildKtxProgram', () => {
|
describe('buildKtxProgram', () => {
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ type CommandPathNode = CommandWithGlobalOptions & {
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROJECT_AWARE_ROOT_COMMANDS = new Set(['setup', 'connection', 'ingest', 'wiki', 'sl', 'sql', 'status', 'mcp']);
|
const PROJECT_AWARE_ROOT_COMMANDS = new Set(['setup', 'connection', 'ingest', 'wiki', 'sl', 'sql', 'status', 'mcp']);
|
||||||
|
const PROJECT_INDEPENDENT_DEV_COMMANDS = new Set(['runtime', 'schema']);
|
||||||
const COMMANDS_THAT_CREATE_PROJECT = new Set(['setup', 'ktx dev init']);
|
const COMMANDS_THAT_CREATE_PROJECT = new Set(['setup', 'ktx dev init']);
|
||||||
const COMMANDS_WITH_OWN_MISSING_PROJECT_HANDLING = new Set(['status']);
|
const COMMANDS_WITH_OWN_MISSING_PROJECT_HANDLING = new Set(['status']);
|
||||||
const GLOBAL_OPTIONS_WITH_VALUE = new Set(['--project-dir']);
|
const GLOBAL_OPTIONS_WITH_VALUE = new Set(['--project-dir']);
|
||||||
|
|
@ -172,7 +173,7 @@ function isProjectAwareCommand(path: string[]): boolean {
|
||||||
|
|
||||||
const rootCommand = path[1];
|
const rootCommand = path[1];
|
||||||
if (rootCommand === 'dev') {
|
if (rootCommand === 'dev') {
|
||||||
return path[2] !== undefined && path[2] !== 'runtime';
|
return path[2] !== undefined && !PROJECT_INDEPENDENT_DEV_COMMANDS.has(path[2]);
|
||||||
}
|
}
|
||||||
return rootCommand !== undefined && PROJECT_AWARE_ROOT_COMMANDS.has(rootCommand);
|
return rootCommand !== undefined && PROJECT_AWARE_ROOT_COMMANDS.has(rootCommand);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import type { KtxSlArgs } from './sl.js';
|
||||||
import type { KtxSqlArgs } from './sql.js';
|
import type { KtxSqlArgs } from './sql.js';
|
||||||
import { profileMark, profileSpan } from './startup-profile.js';
|
import { profileMark, profileSpan } from './startup-profile.js';
|
||||||
import type { KtxTextIngestArgs } from './text-ingest.js';
|
import type { KtxTextIngestArgs } from './text-ingest.js';
|
||||||
|
import { resolveKtxRuntimeVersion } from './release-version.js';
|
||||||
|
|
||||||
profileMark('module:cli-runtime');
|
profileMark('module:cli-runtime');
|
||||||
|
|
||||||
|
|
@ -18,6 +19,8 @@ const requirePackageJson = createRequire(import.meta.url);
|
||||||
export interface KtxCliPackageInfo {
|
export interface KtxCliPackageInfo {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
packageVersion: string;
|
||||||
|
runtimeVersion: string;
|
||||||
contextPackageName: '@ktx/context';
|
contextPackageName: '@ktx/context';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,9 +64,16 @@ export function packageInfoFromJson(packageJson: unknown): KtxCliPackageInfo {
|
||||||
throw new Error('Invalid KTX CLI package metadata');
|
throw new Error('Invalid KTX CLI package metadata');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runtimeVersion = resolveKtxRuntimeVersion({
|
||||||
|
packageName: packageJson.name,
|
||||||
|
packageVersion: packageJson.version,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: packageJson.name,
|
name: packageJson.name,
|
||||||
version: packageJson.version,
|
version: runtimeVersion,
|
||||||
|
packageVersion: packageJson.version,
|
||||||
|
runtimeVersion,
|
||||||
contextPackageName: '@ktx/context',
|
contextPackageName: '@ktx/context',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,35 @@ describe('dev Commander tree', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prints config schema without requiring a KTX project directory', async () => {
|
||||||
|
const { mkdtemp, rm } = await import('node:fs/promises');
|
||||||
|
const { tmpdir } = await import('node:os');
|
||||||
|
const { join } = await import('node:path');
|
||||||
|
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-dev-schema-'));
|
||||||
|
const missingProjectDir = join(tempDir, 'missing-project');
|
||||||
|
const originalProjectDir = process.env.KTX_PROJECT_DIR;
|
||||||
|
const testIo = makeIo();
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.env.KTX_PROJECT_DIR = missingProjectDir;
|
||||||
|
|
||||||
|
await expect(runKtxCli(['dev', 'schema'], testIo.io)).resolves.toBe(0);
|
||||||
|
|
||||||
|
expect(JSON.parse(testIo.stdout())).toMatchObject({
|
||||||
|
title: 'ktx.yaml',
|
||||||
|
type: 'object',
|
||||||
|
});
|
||||||
|
expect(testIo.stderr()).toBe('');
|
||||||
|
} finally {
|
||||||
|
if (originalProjectDir === undefined) {
|
||||||
|
delete process.env.KTX_PROJECT_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.KTX_PROJECT_DIR = originalProjectDir;
|
||||||
|
}
|
||||||
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects removed dev command groups', async () => {
|
it('rejects removed dev command groups', async () => {
|
||||||
for (const argv of [
|
for (const argv of [
|
||||||
['dev', 'doctor', 'setup'],
|
['dev', 'doctor', 'setup'],
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,9 @@ describe('getKtxCliPackageInfo', () => {
|
||||||
it('identifies the CLI package and its context dependency', () => {
|
it('identifies the CLI package and its context dependency', () => {
|
||||||
expect(getKtxCliPackageInfo()).toEqual({
|
expect(getKtxCliPackageInfo()).toEqual({
|
||||||
name: '@ktx/cli',
|
name: '@ktx/cli',
|
||||||
version: '0.0.0-private',
|
version: '0.1.0-rc.1',
|
||||||
|
packageVersion: '0.0.0-private',
|
||||||
|
runtimeVersion: '0.1.0-rc.1',
|
||||||
contextPackageName: '@ktx/context',
|
contextPackageName: '@ktx/context',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -68,6 +70,8 @@ describe('getKtxCliPackageInfo', () => {
|
||||||
).toEqual({
|
).toEqual({
|
||||||
name: '@kaelio/ktx',
|
name: '@kaelio/ktx',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
|
packageVersion: '0.1.0',
|
||||||
|
runtimeVersion: '0.1.0',
|
||||||
contextPackageName: '@ktx/context',
|
contextPackageName: '@ktx/context',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -114,7 +118,7 @@ describe('runKtxCli', () => {
|
||||||
|
|
||||||
await expect(runKtxCli(['--version'], testIo.io)).resolves.toBe(0);
|
await expect(runKtxCli(['--version'], testIo.io)).resolves.toBe(0);
|
||||||
|
|
||||||
expect(testIo.stdout()).toBe('@ktx/cli 0.0.0-private\n');
|
expect(testIo.stdout()).toBe('@ktx/cli 0.1.0-rc.1\n');
|
||||||
expect(testIo.stderr()).toBe('');
|
expect(testIo.stderr()).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -252,7 +256,7 @@ describe('runKtxCli', () => {
|
||||||
expect(listIo.stderr()).toContain("unknown option '--query'");
|
expect(listIo.stderr()).toContain("unknown option '--query'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('routes runtime management commands with the CLI package version', async () => {
|
it('routes runtime management commands with the release runtime version', async () => {
|
||||||
const runtime = vi.fn(async () => 0);
|
const runtime = vi.fn(async () => 0);
|
||||||
const installIo = makeIo();
|
const installIo = makeIo();
|
||||||
const startIo = makeIo();
|
const startIo = makeIo();
|
||||||
|
|
@ -278,7 +282,7 @@ describe('runKtxCli', () => {
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
command: 'install',
|
command: 'install',
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
feature: 'local-embeddings',
|
feature: 'local-embeddings',
|
||||||
force: true,
|
force: true,
|
||||||
},
|
},
|
||||||
|
|
@ -288,7 +292,7 @@ describe('runKtxCli', () => {
|
||||||
2,
|
2,
|
||||||
{
|
{
|
||||||
command: 'start',
|
command: 'start',
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
projectDir: expect.any(String),
|
projectDir: expect.any(String),
|
||||||
feature: 'local-embeddings',
|
feature: 'local-embeddings',
|
||||||
force: true,
|
force: true,
|
||||||
|
|
@ -299,7 +303,7 @@ describe('runKtxCli', () => {
|
||||||
3,
|
3,
|
||||||
{
|
{
|
||||||
command: 'stop',
|
command: 'stop',
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
projectDir: expect.any(String),
|
projectDir: expect.any(String),
|
||||||
all: false,
|
all: false,
|
||||||
},
|
},
|
||||||
|
|
@ -309,7 +313,7 @@ describe('runKtxCli', () => {
|
||||||
4,
|
4,
|
||||||
{
|
{
|
||||||
command: 'stop',
|
command: 'stop',
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
projectDir: expect.any(String),
|
projectDir: expect.any(String),
|
||||||
all: true,
|
all: true,
|
||||||
},
|
},
|
||||||
|
|
@ -319,7 +323,7 @@ describe('runKtxCli', () => {
|
||||||
5,
|
5,
|
||||||
{
|
{
|
||||||
command: 'status',
|
command: 'status',
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
json: true,
|
json: true,
|
||||||
},
|
},
|
||||||
statusIo.io,
|
statusIo.io,
|
||||||
|
|
@ -392,7 +396,7 @@ describe('runKtxCli', () => {
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
command: 'query',
|
command: 'query',
|
||||||
projectDir: tempDir,
|
projectDir: tempDir,
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
runtimeInstallPolicy: 'prompt',
|
runtimeInstallPolicy: 'prompt',
|
||||||
query: expect.objectContaining({ measures: ['orders.order_count'], dimensions: [] }),
|
query: expect.objectContaining({ measures: ['orders.order_count'], dimensions: [] }),
|
||||||
}),
|
}),
|
||||||
|
|
@ -407,7 +411,7 @@ describe('runKtxCli', () => {
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
expect(sl).toHaveBeenLastCalledWith(
|
expect(sl).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
runtimeInstallPolicy: 'auto',
|
runtimeInstallPolicy: 'auto',
|
||||||
}),
|
}),
|
||||||
autoIo.io,
|
autoIo.io,
|
||||||
|
|
@ -423,7 +427,7 @@ describe('runKtxCli', () => {
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
expect(sl).toHaveBeenLastCalledWith(
|
expect(sl).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
runtimeInstallPolicy: 'never',
|
runtimeInstallPolicy: 'never',
|
||||||
}),
|
}),
|
||||||
noInputIo.io,
|
noInputIo.io,
|
||||||
|
|
@ -562,7 +566,7 @@ describe('runKtxCli', () => {
|
||||||
skipAgents: false,
|
skipAgents: false,
|
||||||
inputMode: 'auto',
|
inputMode: 'auto',
|
||||||
yes: false,
|
yes: false,
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
skipLlm: false,
|
skipLlm: false,
|
||||||
skipEmbeddings: false,
|
skipEmbeddings: false,
|
||||||
databaseSchemas: [],
|
databaseSchemas: [],
|
||||||
|
|
@ -692,7 +696,7 @@ describe('runKtxCli', () => {
|
||||||
inputMode: 'disabled',
|
inputMode: 'disabled',
|
||||||
depth: 'fast',
|
depth: 'fast',
|
||||||
queryHistory: 'default',
|
queryHistory: 'default',
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
runtimeInstallPolicy: 'never',
|
runtimeInstallPolicy: 'never',
|
||||||
},
|
},
|
||||||
testIo.io,
|
testIo.io,
|
||||||
|
|
@ -719,7 +723,7 @@ describe('runKtxCli', () => {
|
||||||
inputMode: 'auto',
|
inputMode: 'auto',
|
||||||
depth: 'deep',
|
depth: 'deep',
|
||||||
queryHistory: 'default',
|
queryHistory: 'default',
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
runtimeInstallPolicy: 'prompt',
|
runtimeInstallPolicy: 'prompt',
|
||||||
},
|
},
|
||||||
testIo.io,
|
testIo.io,
|
||||||
|
|
@ -796,7 +800,7 @@ describe('runKtxCli', () => {
|
||||||
json: false,
|
json: false,
|
||||||
inputMode: 'disabled',
|
inputMode: 'disabled',
|
||||||
queryHistory: 'default',
|
queryHistory: 'default',
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
runtimeInstallPolicy: 'never',
|
runtimeInstallPolicy: 'never',
|
||||||
},
|
},
|
||||||
testIo.io,
|
testIo.io,
|
||||||
|
|
@ -1074,7 +1078,7 @@ describe('runKtxCli', () => {
|
||||||
command: 'run',
|
command: 'run',
|
||||||
projectDir: tempDir,
|
projectDir: tempDir,
|
||||||
inputMode: 'disabled',
|
inputMode: 'disabled',
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
|
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
|
||||||
anthropicModel: 'claude-sonnet-4-6',
|
anthropicModel: 'claude-sonnet-4-6',
|
||||||
skipLlm: false,
|
skipLlm: false,
|
||||||
|
|
@ -1113,7 +1117,7 @@ describe('runKtxCli', () => {
|
||||||
command: 'run',
|
command: 'run',
|
||||||
projectDir: tempDir,
|
projectDir: tempDir,
|
||||||
inputMode: 'disabled',
|
inputMode: 'disabled',
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
llmBackend: 'vertex',
|
llmBackend: 'vertex',
|
||||||
vertexProject: 'local-gcp-project',
|
vertexProject: 'local-gcp-project',
|
||||||
vertexLocation: 'us-east5',
|
vertexLocation: 'us-east5',
|
||||||
|
|
@ -1150,7 +1154,7 @@ describe('runKtxCli', () => {
|
||||||
command: 'run',
|
command: 'run',
|
||||||
projectDir: tempDir,
|
projectDir: tempDir,
|
||||||
inputMode: 'disabled',
|
inputMode: 'disabled',
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
llmBackend: 'claude-code',
|
llmBackend: 'claude-code',
|
||||||
llmModel: 'opus',
|
llmModel: 'opus',
|
||||||
skipLlm: false,
|
skipLlm: false,
|
||||||
|
|
@ -1258,7 +1262,7 @@ describe('runKtxCli', () => {
|
||||||
projectDir: '/tmp/project',
|
projectDir: '/tmp/project',
|
||||||
inputMode: 'disabled',
|
inputMode: 'disabled',
|
||||||
yes: true,
|
yes: true,
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
skipLlm: true,
|
skipLlm: true,
|
||||||
skipEmbeddings: true,
|
skipEmbeddings: true,
|
||||||
databaseDrivers: ['postgres'],
|
databaseDrivers: ['postgres'],
|
||||||
|
|
@ -1576,7 +1580,7 @@ describe('runKtxCli', () => {
|
||||||
queryFile: '/tmp/query.json',
|
queryFile: '/tmp/query.json',
|
||||||
execute: false,
|
execute: false,
|
||||||
format: 'json',
|
format: 'json',
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
runtimeInstallPolicy: 'auto',
|
runtimeInstallPolicy: 'auto',
|
||||||
},
|
},
|
||||||
autoIo.io,
|
autoIo.io,
|
||||||
|
|
@ -1590,7 +1594,7 @@ describe('runKtxCli', () => {
|
||||||
queryFile: '/tmp/query.json',
|
queryFile: '/tmp/query.json',
|
||||||
execute: false,
|
execute: false,
|
||||||
format: 'json',
|
format: 'json',
|
||||||
cliVersion: '0.0.0-private',
|
cliVersion: '0.1.0-rc.1',
|
||||||
runtimeInstallPolicy: 'never',
|
runtimeInstallPolicy: 'never',
|
||||||
},
|
},
|
||||||
neverIo.io,
|
neverIo.io,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import {
|
import {
|
||||||
createManagedPythonSemanticLayerComputePort,
|
createManagedPythonSemanticLayerComputePort,
|
||||||
|
ensureManagedPythonCommandRuntime,
|
||||||
managedRuntimeInstallCommand,
|
managedRuntimeInstallCommand,
|
||||||
runtimeInstallPolicyFromFlags,
|
runtimeInstallPolicyFromFlags,
|
||||||
} from './managed-python-command.js';
|
} from './managed-python-command.js';
|
||||||
|
|
@ -103,6 +104,17 @@ function installResult(features: KtxRuntimeFeature[] = ['core']): ManagedPythonR
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeSpinnerEvents() {
|
||||||
|
const events: string[] = [];
|
||||||
|
const spinner = vi.fn(() => ({
|
||||||
|
start: (msg: string) => events.push(`start:${msg}`),
|
||||||
|
message: (msg: string) => events.push(`message:${msg}`),
|
||||||
|
stop: (msg: string) => events.push(`stop:${msg}`),
|
||||||
|
error: (msg: string) => events.push(`error:${msg}`),
|
||||||
|
}));
|
||||||
|
return { events, spinner };
|
||||||
|
}
|
||||||
|
|
||||||
describe('managedRuntimeInstallCommand', () => {
|
describe('managedRuntimeInstallCommand', () => {
|
||||||
it('prints the exact command for each managed runtime feature', () => {
|
it('prints the exact command for each managed runtime feature', () => {
|
||||||
expect(managedRuntimeInstallCommand('core')).toBe('ktx dev runtime install --yes');
|
expect(managedRuntimeInstallCommand('core')).toBe('ktx dev runtime install --yes');
|
||||||
|
|
@ -128,6 +140,51 @@ describe('runtimeInstallPolicyFromFlags', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createManagedPythonSemanticLayerComputePort', () => {
|
describe('createManagedPythonSemanticLayerComputePort', () => {
|
||||||
|
it('uses non-animated runtime setup status by default', async () => {
|
||||||
|
const io = makeIo();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ensureManagedPythonCommandRuntime({
|
||||||
|
cliVersion: '0.2.0',
|
||||||
|
installPolicy: 'auto',
|
||||||
|
io: io.io,
|
||||||
|
readStatus: vi.fn(async () => missingStatus()),
|
||||||
|
installRuntime: vi.fn(async () => installResult(['local-embeddings'])),
|
||||||
|
feature: 'local-embeddings',
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
layout: { versionDir: '/runtime/0.2.0' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(io.stderr()).toContain('Installing KTX Python runtime (local-embeddings) with uv...');
|
||||||
|
expect(io.stderr()).toContain('KTX Python runtime ready: /runtime/0.2.0');
|
||||||
|
expect(io.stderr().match(/Installing KTX Python runtime/g)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows runtime installation progress with the CLI spinner', async () => {
|
||||||
|
const io = makeIo();
|
||||||
|
const { events, spinner } = makeSpinnerEvents();
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
cliVersion: '0.2.0',
|
||||||
|
installPolicy: 'auto' as const,
|
||||||
|
io: io.io,
|
||||||
|
readStatus: vi.fn(async () => missingStatus()),
|
||||||
|
installRuntime: vi.fn(async () => installResult(['local-embeddings'])),
|
||||||
|
feature: 'local-embeddings' as const,
|
||||||
|
spinner,
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(ensureManagedPythonCommandRuntime(options)).resolves.toMatchObject({
|
||||||
|
layout: { versionDir: '/runtime/0.2.0' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(events).toEqual([
|
||||||
|
'start:Installing KTX Python runtime (local-embeddings) with uv...',
|
||||||
|
'stop:KTX Python runtime ready: /runtime/0.2.0',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('uses the managed ktx-daemon executable when the runtime is ready', async () => {
|
it('uses the managed ktx-daemon executable when the runtime is ready', async () => {
|
||||||
const io = makeIo();
|
const io = makeIo();
|
||||||
const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
|
const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
|
||||||
|
|
@ -170,6 +227,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
||||||
|
|
||||||
it('installs the core runtime without prompting when policy is auto', async () => {
|
it('installs the core runtime without prompting when policy is auto', async () => {
|
||||||
const io = makeIo();
|
const io = makeIo();
|
||||||
|
const { events, spinner } = makeSpinnerEvents();
|
||||||
const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
|
const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
|
||||||
const createPythonCompute = vi.fn(() => compute);
|
const createPythonCompute = vi.fn(() => compute);
|
||||||
const installRuntime = vi.fn(async () => installResult());
|
const installRuntime = vi.fn(async () => installResult());
|
||||||
|
|
@ -182,6 +240,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
||||||
readStatus: vi.fn(async () => missingStatus()),
|
readStatus: vi.fn(async () => missingStatus()),
|
||||||
installRuntime,
|
installRuntime,
|
||||||
createPythonCompute,
|
createPythonCompute,
|
||||||
|
spinner,
|
||||||
}),
|
}),
|
||||||
).resolves.toBe(compute);
|
).resolves.toBe(compute);
|
||||||
|
|
||||||
|
|
@ -190,12 +249,15 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
||||||
features: ['core'],
|
features: ['core'],
|
||||||
force: false,
|
force: false,
|
||||||
});
|
});
|
||||||
expect(io.stderr()).toContain('Installing KTX Python runtime (core) with uv');
|
expect(events).toEqual([
|
||||||
expect(io.stderr()).toContain('KTX Python runtime ready: /runtime/0.2.0');
|
'start:Installing KTX Python runtime (core) with uv...',
|
||||||
|
'stop:KTX Python runtime ready: /runtime/0.2.0',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prompts before installing when policy is prompt', async () => {
|
it('prompts before installing when policy is prompt', async () => {
|
||||||
const io = makeIo();
|
const io = makeIo();
|
||||||
|
const { events, spinner } = makeSpinnerEvents();
|
||||||
const confirmInstall = vi.fn(async () => true);
|
const confirmInstall = vi.fn(async () => true);
|
||||||
const installRuntime = vi.fn(async () => installResult());
|
const installRuntime = vi.fn(async () => installResult());
|
||||||
|
|
||||||
|
|
@ -207,6 +269,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
||||||
installRuntime,
|
installRuntime,
|
||||||
createPythonCompute: vi.fn(() => ({ query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() })),
|
createPythonCompute: vi.fn(() => ({ query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() })),
|
||||||
confirmInstall,
|
confirmInstall,
|
||||||
|
spinner,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(confirmInstall).toHaveBeenCalledWith(
|
expect(confirmInstall).toHaveBeenCalledWith(
|
||||||
|
|
@ -218,10 +281,12 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
||||||
features: ['core'],
|
features: ['core'],
|
||||||
force: false,
|
force: false,
|
||||||
});
|
});
|
||||||
|
expect(events).toContainEqual('start:Installing KTX Python runtime (core) with uv...');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses injected runtime confirmation instead of reading process TTY directly', async () => {
|
it('uses injected runtime confirmation instead of reading process TTY directly', async () => {
|
||||||
const io = makeIo();
|
const io = makeIo();
|
||||||
|
const { events, spinner } = makeSpinnerEvents();
|
||||||
const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
|
const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
|
||||||
const installRuntime = vi.fn(async (): Promise<ManagedPythonRuntimeInstallResult> => installResult());
|
const installRuntime = vi.fn(async (): Promise<ManagedPythonRuntimeInstallResult> => installResult());
|
||||||
const confirmInstall = vi.fn(async () => true);
|
const confirmInstall = vi.fn(async () => true);
|
||||||
|
|
@ -235,6 +300,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
||||||
installRuntime,
|
installRuntime,
|
||||||
confirmInstall,
|
confirmInstall,
|
||||||
createPythonCompute: () => compute,
|
createPythonCompute: () => compute,
|
||||||
|
spinner,
|
||||||
}),
|
}),
|
||||||
).resolves.toBe(compute);
|
).resolves.toBe(compute);
|
||||||
|
|
||||||
|
|
@ -242,7 +308,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
||||||
'KTX needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?',
|
'KTX needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?',
|
||||||
io.io,
|
io.io,
|
||||||
);
|
);
|
||||||
expect(io.stderr()).toContain('Installing KTX Python runtime (core) with uv...');
|
expect(events).toContainEqual('start:Installing KTX Python runtime (core) with uv...');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can decide default runtime prompting from injected io capabilities', async () => {
|
it('can decide default runtime prompting from injected io capabilities', async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createPythonSemanticLayerComputePort, type KtxSemanticLayerComputePort } from '@ktx/context/daemon';
|
import { createPythonSemanticLayerComputePort, type KtxSemanticLayerComputePort } from '@ktx/context/daemon';
|
||||||
import type { KtxCliIo } from './cli-runtime.js';
|
import type { KtxCliIo } from './cli-runtime.js';
|
||||||
import { createClackPromptAdapter } from './clack.js';
|
import { createClackPromptAdapter, createStaticCliSpinner, type KtxCliSpinner } from './clack.js';
|
||||||
import {
|
import {
|
||||||
installManagedPythonRuntime,
|
installManagedPythonRuntime,
|
||||||
readManagedPythonRuntimeStatus,
|
readManagedPythonRuntimeStatus,
|
||||||
|
|
@ -37,6 +37,7 @@ export interface ManagedPythonCommandDeps {
|
||||||
readStatus?: (options: ManagedPythonRuntimeLayoutOptions) => Promise<ManagedPythonRuntimeStatus>;
|
readStatus?: (options: ManagedPythonRuntimeLayoutOptions) => Promise<ManagedPythonRuntimeStatus>;
|
||||||
installRuntime?: (options: ManagedPythonRuntimeInstallOptions) => Promise<ManagedPythonRuntimeInstallResult>;
|
installRuntime?: (options: ManagedPythonRuntimeInstallOptions) => Promise<ManagedPythonRuntimeInstallResult>;
|
||||||
confirmInstall?: (message: string, io: KtxCliIo) => Promise<boolean>;
|
confirmInstall?: (message: string, io: KtxCliIo) => Promise<boolean>;
|
||||||
|
spinner?: () => KtxCliSpinner;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ManagedPythonCommandOptions extends ManagedPythonCommandDeps {
|
export interface ManagedPythonCommandOptions extends ManagedPythonCommandDeps {
|
||||||
|
|
@ -101,14 +102,20 @@ export async function ensureManagedPythonCommandRuntime(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
options.io.stderr.write(`Installing KTX Python runtime (${feature}) with uv...\n`);
|
const progress = (options.spinner ?? (() => createStaticCliSpinner(options.io)))();
|
||||||
const installed = await installRuntime({
|
progress.start(`Installing KTX Python runtime (${feature}) with uv...`);
|
||||||
cliVersion: options.cliVersion,
|
try {
|
||||||
features: [feature],
|
const installed = await installRuntime({
|
||||||
force: false,
|
cliVersion: options.cliVersion,
|
||||||
});
|
features: [feature],
|
||||||
options.io.stderr.write(`KTX Python runtime ready: ${installed.layout.versionDir}\n`);
|
force: false,
|
||||||
return { layout: installed.layout, manifest: installed.manifest };
|
});
|
||||||
|
progress.stop(`KTX Python runtime ready: ${installed.layout.versionDir}`);
|
||||||
|
return { layout: installed.layout, manifest: installed.manifest };
|
||||||
|
} catch (error) {
|
||||||
|
progress.error(`KTX Python runtime install failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createManagedPythonSemanticLayerComputePort(
|
export async function createManagedPythonSemanticLayerComputePort(
|
||||||
|
|
@ -122,6 +129,7 @@ export async function createManagedPythonSemanticLayerComputePort(
|
||||||
...(options.readStatus ? { readStatus: options.readStatus } : {}),
|
...(options.readStatus ? { readStatus: options.readStatus } : {}),
|
||||||
...(options.installRuntime ? { installRuntime: options.installRuntime } : {}),
|
...(options.installRuntime ? { installRuntime: options.installRuntime } : {}),
|
||||||
...(options.confirmInstall ? { confirmInstall: options.confirmInstall } : {}),
|
...(options.confirmInstall ? { confirmInstall: options.confirmInstall } : {}),
|
||||||
|
...(options.spinner ? { spinner: options.spinner } : {}),
|
||||||
});
|
});
|
||||||
const createPythonCompute = options.createPythonCompute ?? createPythonSemanticLayerComputePort;
|
const createPythonCompute = options.createPythonCompute ?? createPythonSemanticLayerComputePort;
|
||||||
return createPythonCompute({
|
return createPythonCompute({
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,13 @@ function silentIo(): KtxCliIo {
|
||||||
}
|
}
|
||||||
|
|
||||||
function stubPackageInfo(): KtxCliPackageInfo {
|
function stubPackageInfo(): KtxCliPackageInfo {
|
||||||
return { name: '@ktx/cli', version: '0.0.0-docs', contextPackageName: '@ktx/context' };
|
return {
|
||||||
|
name: '@ktx/cli',
|
||||||
|
version: '0.0.0-docs',
|
||||||
|
packageVersion: '0.0.0-private',
|
||||||
|
runtimeVersion: '0.0.0-docs',
|
||||||
|
contextPackageName: '@ktx/context',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderKtxCommandTree(): string {
|
export function renderKtxCommandTree(): string {
|
||||||
|
|
|
||||||
55
packages/cli/src/release-version.ts
Normal file
55
packages/cli/src/release-version.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
import { dirname, join, parse } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const semverPattern =
|
||||||
|
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
||||||
|
|
||||||
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertReleaseVersion(value: unknown, source: string): string {
|
||||||
|
if (typeof value !== 'string' || !semverPattern.test(value)) {
|
||||||
|
throw new Error(`Invalid KTX release version in ${source}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findReleasePolicyPath(startDir: string): string | undefined {
|
||||||
|
let current = startDir;
|
||||||
|
const root = parse(current).root;
|
||||||
|
while (true) {
|
||||||
|
const candidate = join(current, 'release-policy.json');
|
||||||
|
if (existsSync(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
if (current === root) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
current = dirname(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSourceReleaseVersion(startDir = dirname(fileURLToPath(import.meta.url))): string | undefined {
|
||||||
|
const policyPath = findReleasePolicyPath(startDir);
|
||||||
|
if (!policyPath) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const policy = JSON.parse(readFileSync(policyPath, 'utf8')) as unknown;
|
||||||
|
if (!isPlainObject(policy)) {
|
||||||
|
throw new Error(`Invalid KTX release policy: ${policyPath}`);
|
||||||
|
}
|
||||||
|
return assertReleaseVersion(policy.publicNpmPackageVersion, policyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveKtxRuntimeVersion(input: {
|
||||||
|
packageName: string;
|
||||||
|
packageVersion: string;
|
||||||
|
startDir?: string;
|
||||||
|
}): string {
|
||||||
|
if (input.packageName === '@kaelio/ktx') {
|
||||||
|
return assertReleaseVersion(input.packageVersion, `${input.packageName}/package.json`);
|
||||||
|
}
|
||||||
|
return readSourceReleaseVersion(input.startDir) ?? input.packageVersion;
|
||||||
|
}
|
||||||
|
|
@ -176,12 +176,33 @@ describe('setup embeddings step', () => {
|
||||||
expect(config.scan.enrichment.embeddings).toMatchObject(config.ingest.embeddings);
|
expect(config.scan.enrichment.embeddings).toMatchObject(config.ingest.embeddings);
|
||||||
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
||||||
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('embeddings');
|
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('embeddings');
|
||||||
expect(spinnerEvents).toContainEqual(
|
expect(spinnerEvents).toContainEqual('start:Testing local embeddings (all-MiniLM-L6-v2)');
|
||||||
'start:Testing local sentence-transformers embeddings (all-MiniLM-L6-v2, 384 dimensions). First run may take up to 60 seconds.',
|
|
||||||
);
|
|
||||||
expect(io.stdout()).toContain('Embeddings ready: yes');
|
expect(io.stdout()).toContain('Embeddings ready: yes');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses a short non-animated local embeddings health-check status by default', async () => {
|
||||||
|
const io = makeIo();
|
||||||
|
const healthCheck = vi.fn(async () => ({ ok: true as const }));
|
||||||
|
const prompts = makePromptAdapter({ selectValues: ['sentence-transformers'] });
|
||||||
|
|
||||||
|
const result = await runKtxSetupEmbeddingsStep(
|
||||||
|
{
|
||||||
|
projectDir: tempDir,
|
||||||
|
inputMode: 'auto',
|
||||||
|
cliVersion: '0.2.0',
|
||||||
|
runtimeInstallPolicy: 'auto',
|
||||||
|
skipEmbeddings: false,
|
||||||
|
},
|
||||||
|
io.io,
|
||||||
|
{ prompts, env: {}, healthCheck, ensureLocalEmbeddings: vi.fn(async () => managedDaemon()) },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.status).toBe('ready');
|
||||||
|
expect(io.stderr()).toContain('Testing local embeddings (all-MiniLM-L6-v2)');
|
||||||
|
expect(io.stderr()).not.toContain('First run may take up to 60 seconds');
|
||||||
|
expect(io.stderr().match(/Testing local embeddings/g)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('shows live progress while local sentence-transformers embeddings are being tested', async () => {
|
it('shows live progress while local sentence-transformers embeddings are being tested', async () => {
|
||||||
const io = makeIo();
|
const io = makeIo();
|
||||||
const prompts = makePromptAdapter({ selectValues: ['sentence-transformers'] });
|
const prompts = makePromptAdapter({ selectValues: ['sentence-transformers'] });
|
||||||
|
|
@ -213,9 +234,7 @@ describe('setup embeddings step', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
expect(spinnerEvents).toContainEqual(
|
expect(spinnerEvents).toContainEqual('start:Testing local embeddings (all-MiniLM-L6-v2)');
|
||||||
'start:Testing local sentence-transformers embeddings (all-MiniLM-L6-v2, 384 dimensions). First run may take up to 60 seconds.',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(resolveHealthCheck).toBeDefined();
|
expect(resolveHealthCheck).toBeDefined();
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
} from '@ktx/context/project';
|
} from '@ktx/context/project';
|
||||||
import { type KtxEmbeddingConfig, type KtxEmbeddingHealthCheckResult, runKtxEmbeddingHealthCheck } from '@ktx/llm';
|
import { type KtxEmbeddingConfig, type KtxEmbeddingHealthCheckResult, runKtxEmbeddingHealthCheck } from '@ktx/llm';
|
||||||
import type { KtxCliIo } from './cli-runtime.js';
|
import type { KtxCliIo } from './cli-runtime.js';
|
||||||
import { createClackSpinner, type KtxCliSpinner } from './clack.js';
|
import { createStaticCliSpinner, type KtxCliSpinner } from './clack.js';
|
||||||
import {
|
import {
|
||||||
ensureManagedLocalEmbeddingsDaemon,
|
ensureManagedLocalEmbeddingsDaemon,
|
||||||
managedLocalEmbeddingHealthConfig,
|
managedLocalEmbeddingHealthConfig,
|
||||||
|
|
@ -316,10 +316,7 @@ async function promptAfterLocalEmbeddingFailure(
|
||||||
|
|
||||||
function healthCheckStartText(backend: KtxSetupEmbeddingBackend, model: string, dimensions: number): string {
|
function healthCheckStartText(backend: KtxSetupEmbeddingBackend, model: string, dimensions: number): string {
|
||||||
if (backend === LOCAL_EMBEDDING_BACKEND) {
|
if (backend === LOCAL_EMBEDDING_BACKEND) {
|
||||||
return [
|
return `Testing local embeddings (${model})`;
|
||||||
`Testing local sentence-transformers embeddings (${model}, ${dimensions} dimensions).`,
|
|
||||||
'First run may take up to 60 seconds.',
|
|
||||||
].join(' ');
|
|
||||||
}
|
}
|
||||||
return `Checking ${backend} embeddings (${model}, ${dimensions} dimensions).`;
|
return `Checking ${backend} embeddings (${model}, ${dimensions} dimensions).`;
|
||||||
}
|
}
|
||||||
|
|
@ -424,7 +421,7 @@ export async function runKtxSetupEmbeddingsStep(
|
||||||
dimensions,
|
dimensions,
|
||||||
credentialValue,
|
credentialValue,
|
||||||
});
|
});
|
||||||
const healthSpinner = (deps.spinner ?? createClackSpinner)();
|
const healthSpinner = (deps.spinner ?? (() => createStaticCliSpinner(io)))();
|
||||||
const progress = startHealthCheckProgress(healthSpinner, healthCheckStartText(selectedBackend, model, dimensions));
|
const progress = startHealthCheckProgress(healthSpinner, healthCheckStartText(selectedBackend, model, dimensions));
|
||||||
let health: KtxEmbeddingHealthCheckResult;
|
let health: KtxEmbeddingHealthCheckResult;
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ describe('setup Anthropic model step', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('offers Vertex AI as an Anthropic model provider option', async () => {
|
it('offers Anthropic provider paths in the preferred order', async () => {
|
||||||
const prompts = makePromptAdapter({ providerChoice: 'back' });
|
const prompts = makePromptAdapter({ providerChoice: 'back' });
|
||||||
|
|
||||||
const result = await runKtxSetupAnthropicModelStep(
|
const result = await runKtxSetupAnthropicModelStep(
|
||||||
|
|
@ -177,10 +177,12 @@ describe('setup Anthropic model step', () => {
|
||||||
expect(prompts.select).toHaveBeenCalledWith(
|
expect(prompts.select).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
message: expect.stringContaining('Which LLM provider should KTX use?'),
|
message: expect.stringContaining('Which LLM provider should KTX use?'),
|
||||||
options: expect.arrayContaining([
|
options: [
|
||||||
|
{ value: 'claude-code', label: 'Claude subscription (Pro/Max)' },
|
||||||
|
{ value: 'anthropic', label: 'Anthropic API key' },
|
||||||
{ value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' },
|
{ value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' },
|
||||||
{ value: 'back', label: 'Back' },
|
{ value: 'back', label: 'Back' },
|
||||||
]),
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -509,12 +509,12 @@ async function chooseBackend(
|
||||||
}
|
}
|
||||||
const choice = await prompts.select({
|
const choice = await prompts.select({
|
||||||
message: 'Which LLM provider should KTX use?',
|
message: 'Which LLM provider should KTX use?',
|
||||||
options: [
|
options: [
|
||||||
{ value: 'anthropic', label: 'Anthropic API' },
|
{ value: 'claude-code', label: 'Claude subscription (Pro/Max)' },
|
||||||
{ value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' },
|
{ value: 'anthropic', label: 'Anthropic API key' },
|
||||||
{ value: 'claude-code', label: 'Local Claude Code session' },
|
{ value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' },
|
||||||
{ value: 'back', label: 'Back' },
|
{ value: 'back', label: 'Back' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
if (choice === 'back') {
|
if (choice === 'back') {
|
||||||
return { status: 'back' };
|
return { status: 'back' };
|
||||||
|
|
|
||||||
137
packages/cli/src/setup-sources-notion.test.ts
Normal file
137
packages/cli/src/setup-sources-notion.test.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import {
|
||||||
|
initKtxProject,
|
||||||
|
type KtxProjectConnectionConfig,
|
||||||
|
parseKtxProjectConfig,
|
||||||
|
serializeKtxProjectConfig,
|
||||||
|
} from '@ktx/context/project';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
runKtxSetupSourcesStep,
|
||||||
|
type KtxSetupSourcesPromptAdapter,
|
||||||
|
} from './setup-sources.js';
|
||||||
|
|
||||||
|
const notionMocks = vi.hoisted(() => ({
|
||||||
|
tokens: [] as string[],
|
||||||
|
retrieveBotUser: vi.fn(async () => ({ name: 'Docs Bot' })),
|
||||||
|
retrievePage: vi.fn(async () => ({ id: 'page-1' })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@ktx/context/ingest', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import('@ktx/context/ingest')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
NotionClient: vi.fn().mockImplementation(function NotionClient(token: string) {
|
||||||
|
notionMocks.tokens.push(token);
|
||||||
|
return {
|
||||||
|
retrieveBotUser: notionMocks.retrieveBotUser,
|
||||||
|
retrievePage: notionMocks.retrievePage,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeIo() {
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
return {
|
||||||
|
io: {
|
||||||
|
stdout: {
|
||||||
|
isTTY: true,
|
||||||
|
write: (chunk: string) => {
|
||||||
|
stdout += chunk;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stderr: {
|
||||||
|
write: (chunk: string) => {
|
||||||
|
stderr += chunk;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stdout: () => stdout,
|
||||||
|
stderr: () => stderr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function prompts(values: { multiselect?: string[][]; select?: string[] }): KtxSetupSourcesPromptAdapter {
|
||||||
|
const multiselectValues = [...(values.multiselect ?? [])];
|
||||||
|
const selectValues = [...(values.select ?? [])];
|
||||||
|
return {
|
||||||
|
multiselect: vi.fn(async () => multiselectValues.shift() ?? []),
|
||||||
|
select: vi.fn(async () => selectValues.shift() ?? 'back'),
|
||||||
|
text: vi.fn(async () => ''),
|
||||||
|
password: vi.fn(async () => undefined),
|
||||||
|
cancel: vi.fn(),
|
||||||
|
log: vi.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('setup sources Notion validation', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
let projectDir: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
notionMocks.tokens.length = 0;
|
||||||
|
notionMocks.retrieveBotUser.mockClear();
|
||||||
|
notionMocks.retrievePage.mockClear();
|
||||||
|
tempDir = await mkdtemp(join(tmpdir(), 'ktx-setup-sources-notion-'));
|
||||||
|
projectDir = join(tempDir, 'project');
|
||||||
|
await initKtxProject({ projectDir });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
async function readConfig() {
|
||||||
|
return parseKtxProjectConfig(await readFile(join(projectDir, 'ktx.yaml'), 'utf-8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeConfigConnection(connectionId: string, connection: KtxProjectConnectionConfig) {
|
||||||
|
const config = await readConfig();
|
||||||
|
await writeFile(
|
||||||
|
join(projectDir, 'ktx.yaml'),
|
||||||
|
serializeKtxProjectConfig({
|
||||||
|
...config,
|
||||||
|
connections: {
|
||||||
|
...config.connections,
|
||||||
|
warehouse: { driver: 'postgres', url: 'env:DATABASE_URL' },
|
||||||
|
[connectionId]: connection,
|
||||||
|
},
|
||||||
|
setup: {
|
||||||
|
...config.setup,
|
||||||
|
database_connection_ids: ['warehouse'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('validates an existing Notion source that uses an inline auth token', async () => {
|
||||||
|
await writeConfigConnection('notion', {
|
||||||
|
driver: 'notion',
|
||||||
|
auth_token: 'ntn_inline_token',
|
||||||
|
crawl_mode: 'all_accessible',
|
||||||
|
});
|
||||||
|
const io = makeIo();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runKtxSetupSourcesStep(
|
||||||
|
{ projectDir, inputMode: 'auto', runInitialSourceIngest: false, skipSources: false },
|
||||||
|
io.io,
|
||||||
|
{
|
||||||
|
prompts: prompts({
|
||||||
|
multiselect: [['notion']],
|
||||||
|
select: ['existing:notion'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).resolves.toEqual({ status: 'ready', projectDir, connectionIds: ['notion'] });
|
||||||
|
|
||||||
|
expect(notionMocks.tokens).toEqual(['ntn_inline_token']);
|
||||||
|
expect(notionMocks.retrieveBotUser).toHaveBeenCalledOnce();
|
||||||
|
expect(io.stderr()).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -2,7 +2,10 @@ import { mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join, relative, resolve } from 'node:path';
|
import { join, relative, resolve } from 'node:path';
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import { localConnectionTypeForConfig, resolveNotionAuthToken } from '@ktx/context/connections';
|
import {
|
||||||
|
localConnectionTypeForConfig,
|
||||||
|
resolveNotionConnectionAuthToken,
|
||||||
|
} from '@ktx/context/connections';
|
||||||
import { resolveKtxConfigReference } from '@ktx/context/core';
|
import { resolveKtxConfigReference } from '@ktx/context/core';
|
||||||
import {
|
import {
|
||||||
cloneOrPull,
|
cloneOrPull,
|
||||||
|
|
@ -620,7 +623,10 @@ async function defaultValidateLookml(connection: KtxProjectConnectionConfig): Pr
|
||||||
}
|
}
|
||||||
|
|
||||||
async function defaultValidateNotion(connection: KtxProjectConnectionConfig): Promise<SourceValidationResult> {
|
async function defaultValidateNotion(connection: KtxProjectConnectionConfig): Promise<SourceValidationResult> {
|
||||||
const token = await resolveNotionAuthToken(String(connection.auth_token_ref));
|
const token = await resolveNotionConnectionAuthToken({
|
||||||
|
auth_token: stringField(connection.auth_token) ?? null,
|
||||||
|
auth_token_ref: stringField(connection.auth_token_ref) ?? null,
|
||||||
|
});
|
||||||
const client: NotionApi = new NotionClient(token);
|
const client: NotionApi = new NotionClient(token);
|
||||||
await client.retrieveBotUser();
|
await client.retrieveBotUser();
|
||||||
const roots = Array.isArray(connection.root_page_ids)
|
const roots = Array.isArray(connection.root_page_ids)
|
||||||
|
|
|
||||||
85
packages/cli/src/source-mapping.test.ts
Normal file
85
packages/cli/src/source-mapping.test.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import type { KtxCliIo } from './cli-runtime.js';
|
||||||
|
import { runKtxSourceMapping } from './source-mapping.js';
|
||||||
|
|
||||||
|
function makeIo() {
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
return {
|
||||||
|
io: {
|
||||||
|
stdout: {
|
||||||
|
write: (chunk: string) => {
|
||||||
|
stdout += chunk;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stderr: {
|
||||||
|
write: (chunk: string) => {
|
||||||
|
stderr += chunk;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies KtxCliIo,
|
||||||
|
stdout: () => stdout,
|
||||||
|
stderr: () => stderr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('source mapping commands', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
tempDir = await mkdtemp(join(tmpdir(), 'ktx-source-mapping-'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
async function writeConfig(metabaseMappings: string[]): Promise<void> {
|
||||||
|
await writeFile(
|
||||||
|
join(tempDir, 'ktx.yaml'),
|
||||||
|
[
|
||||||
|
'connections:',
|
||||||
|
' warehouse:',
|
||||||
|
' driver: postgres',
|
||||||
|
' url: env:DATABASE_URL',
|
||||||
|
' metabase:',
|
||||||
|
' driver: metabase',
|
||||||
|
' api_url: https://metabase.example.com',
|
||||||
|
...metabaseMappings,
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('fails Metabase validation when no sync-enabled target mapping exists', async () => {
|
||||||
|
await writeConfig([]);
|
||||||
|
const io = makeIo();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runKtxSourceMapping({ command: 'validate', projectDir: tempDir, connectionId: 'metabase' }, io.io),
|
||||||
|
).resolves.toBe(1);
|
||||||
|
|
||||||
|
expect(io.stderr()).toContain('no sync-enabled mappings with a target connection for Metabase connection metabase');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes Metabase validation when a sync-enabled target mapping exists', async () => {
|
||||||
|
await writeConfig([
|
||||||
|
' mappings:',
|
||||||
|
' databaseMappings:',
|
||||||
|
' "3": warehouse',
|
||||||
|
' syncEnabled:',
|
||||||
|
' "3": true',
|
||||||
|
]);
|
||||||
|
const io = makeIo();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runKtxSourceMapping({ command: 'validate', projectDir: tempDir, connectionId: 'metabase' }, io.io),
|
||||||
|
).resolves.toBe(0);
|
||||||
|
|
||||||
|
expect(io.stdout()).toContain('Mapping validation passed: metabase');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
discoverMetabaseDatabases,
|
discoverMetabaseDatabases,
|
||||||
lookerCredentialsFromLocalConnection,
|
lookerCredentialsFromLocalConnection,
|
||||||
metabaseRuntimeConfigFromLocalConnection,
|
metabaseRuntimeConfigFromLocalConnection,
|
||||||
|
planMetabaseFanoutChildren,
|
||||||
seedLocalMappingStateFromKtxYaml,
|
seedLocalMappingStateFromKtxYaml,
|
||||||
validateLookerMappings,
|
validateLookerMappings,
|
||||||
validateMappingPhysicalMatch,
|
validateMappingPhysicalMatch,
|
||||||
|
|
@ -198,6 +199,14 @@ export async function runKtxSourceMapping(
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = await store.listDatabaseMappings(args.connectionId);
|
const rows = await store.listDatabaseMappings(args.connectionId);
|
||||||
|
planMetabaseFanoutChildren({
|
||||||
|
metabaseConnectionId: args.connectionId,
|
||||||
|
mappings: rows.map((row) => ({
|
||||||
|
metabaseDatabaseId: row.metabaseDatabaseId,
|
||||||
|
targetConnectionId: row.targetConnectionId,
|
||||||
|
syncEnabled: row.syncEnabled,
|
||||||
|
})),
|
||||||
|
});
|
||||||
const failures = rows.flatMap((row) => {
|
const failures = rows.flatMap((row) => {
|
||||||
if (!row.targetConnectionId) {
|
if (!row.targetConnectionId) {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "ktx-daemon"
|
name = "ktx-daemon"
|
||||||
version = "0.1.0"
|
version = "0.0.0+private"
|
||||||
description = "Portable compute package for KTX semantic-layer operations"
|
description = "Portable compute package for KTX semantic-layer operations"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,28 @@
|
||||||
"""Portable compute package for KTX."""
|
"""Portable compute package for KTX."""
|
||||||
|
|
||||||
PACKAGE_NAME = "ktx-daemon"
|
from collections.abc import Callable
|
||||||
VERSION = "0.1.0"
|
from importlib.metadata import PackageNotFoundError, version
|
||||||
|
|
||||||
__all__ = ["PACKAGE_NAME", "VERSION"]
|
PACKAGE_NAME = "ktx-daemon"
|
||||||
|
RUNTIME_DISTRIBUTION_NAME = "kaelio-ktx"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_package_version(
|
||||||
|
version_loader: Callable[[str], str] = version,
|
||||||
|
) -> str:
|
||||||
|
for distribution_name in (RUNTIME_DISTRIBUTION_NAME, PACKAGE_NAME):
|
||||||
|
try:
|
||||||
|
return version_loader(distribution_name)
|
||||||
|
except PackageNotFoundError:
|
||||||
|
continue
|
||||||
|
return "0.0.0+local"
|
||||||
|
|
||||||
|
|
||||||
|
VERSION = resolve_package_version()
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"PACKAGE_NAME",
|
||||||
|
"RUNTIME_DISTRIBUTION_NAME",
|
||||||
|
"VERSION",
|
||||||
|
"resolve_package_version",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from typing import Any
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
from fastapi.responses import Response
|
from fastapi.responses import Response
|
||||||
|
|
||||||
|
from ktx_daemon import VERSION
|
||||||
from ktx_daemon.code_execution import (
|
from ktx_daemon.code_execution import (
|
||||||
ExecuteCodeRequest,
|
ExecuteCodeRequest,
|
||||||
ExecuteCodeResponse,
|
ExecuteCodeResponse,
|
||||||
|
|
@ -84,7 +85,7 @@ def create_app(
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="KTX Daemon",
|
title="KTX Daemon",
|
||||||
description="Stateless portable compute server for KTX.",
|
description="Stateless portable compute server for KTX.",
|
||||||
version="0.1.0",
|
version=VERSION,
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,19 @@
|
||||||
from ktx_daemon import PACKAGE_NAME, VERSION
|
from ktx_daemon import PACKAGE_NAME, VERSION, resolve_package_version
|
||||||
|
|
||||||
|
|
||||||
def test_package_metadata() -> None:
|
def test_package_metadata() -> None:
|
||||||
assert PACKAGE_NAME == "ktx-daemon"
|
assert PACKAGE_NAME == "ktx-daemon"
|
||||||
assert VERSION == "0.1.0"
|
assert VERSION == resolve_package_version()
|
||||||
|
|
||||||
|
|
||||||
|
def test_package_version_prefers_bundled_runtime_distribution() -> None:
|
||||||
|
calls: list[str] = []
|
||||||
|
|
||||||
|
def fake_version(distribution_name: str) -> str:
|
||||||
|
calls.append(distribution_name)
|
||||||
|
if distribution_name == "kaelio-ktx":
|
||||||
|
return "0.1.0rc1"
|
||||||
|
raise AssertionError(f"unexpected distribution lookup: {distribution_name}")
|
||||||
|
|
||||||
|
assert resolve_package_version(version_loader=fake_version) == "0.1.0rc1"
|
||||||
|
assert calls == ["kaelio-ktx"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "ktx-sl"
|
name = "ktx-sl"
|
||||||
version = "0.1.0"
|
version = "0.0.0+private"
|
||||||
description = "Agent-first semantic layer engine with aggregate locality"
|
description = "Agent-first semantic layer engine with aggregate locality"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,13 @@ import { dirname, join, resolve } from 'node:path';
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
|
|
||||||
|
import { publicPythonRuntimePackageVersion } from './public-npm-release-metadata.mjs';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
export const RUNTIME_WHEEL_DISTRIBUTION_NAME = 'kaelio-ktx';
|
export const RUNTIME_WHEEL_DISTRIBUTION_NAME = 'kaelio-ktx';
|
||||||
export const RUNTIME_WHEEL_NORMALIZED_NAME = 'kaelio_ktx';
|
export const RUNTIME_WHEEL_NORMALIZED_NAME = 'kaelio_ktx';
|
||||||
export const RUNTIME_WHEEL_PACKAGE_VERSION = '0.1.0';
|
export const RUNTIME_WHEEL_PACKAGE_VERSION = publicPythonRuntimePackageVersion();
|
||||||
|
|
||||||
function scriptRootDir() {
|
function scriptRootDir() {
|
||||||
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,11 @@ describe('runtimeWheelLayout', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('runtimeWheelPyproject', () => {
|
describe('runtimeWheelPyproject', () => {
|
||||||
it('describes one kaelio-ktx wheel with lazy local embeddings', () => {
|
it('describes one kaelio-ktx wheel with the release-derived Python version and lazy local embeddings', () => {
|
||||||
const pyproject = runtimeWheelPyproject();
|
const pyproject = runtimeWheelPyproject();
|
||||||
|
|
||||||
assert.match(pyproject, /name = "kaelio-ktx"/);
|
assert.match(pyproject, /name = "kaelio-ktx"/);
|
||||||
assert.match(pyproject, /version = "0\.1\.0"/);
|
assert.match(pyproject, /version = "0\.1\.0rc1"/);
|
||||||
assert.match(pyproject, /ktx-daemon = "ktx_daemon\.__main__:main"/);
|
assert.match(pyproject, /ktx-daemon = "ktx_daemon\.__main__:main"/);
|
||||||
assert.match(pyproject, /packages = \["semantic_layer", "ktx_daemon"\]/);
|
assert.match(pyproject, /packages = \["semantic_layer", "ktx_daemon"\]/);
|
||||||
assert.match(pyproject, /\[project\.optional-dependencies\]/);
|
assert.match(pyproject, /\[project\.optional-dependencies\]/);
|
||||||
|
|
@ -110,6 +110,6 @@ describe('runtimeWheelBuildCommand', () => {
|
||||||
cwd: '/repo/ktx',
|
cwd: '/repo/ktx',
|
||||||
});
|
});
|
||||||
assert.equal(RUNTIME_WHEEL_DISTRIBUTION_NAME, 'kaelio-ktx');
|
assert.equal(RUNTIME_WHEEL_DISTRIBUTION_NAME, 'kaelio-ktx');
|
||||||
assert.equal(RUNTIME_WHEEL_PACKAGE_VERSION, '0.1.0');
|
assert.equal(RUNTIME_WHEEL_PACKAGE_VERSION, '0.1.0rc1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ const codeExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.
|
||||||
const runtimeAssetPatterns = [/^packages\/[^/]+\/prompts\/.+\.md$/, /^packages\/[^/]+\/skills\/.+\.md$/];
|
const runtimeAssetPatterns = [/^packages\/[^/]+\/prompts\/.+\.md$/, /^packages\/[^/]+\/skills\/.+\.md$/];
|
||||||
const identifierSkipPrefixes = ['docs/', 'docs-site/', 'examples/', 'python/ktx-sl/plans/', 'python/ktx-sl/openspec/'];
|
const identifierSkipPrefixes = ['docs/', 'docs-site/', 'examples/', 'python/ktx-sl/plans/', 'python/ktx-sl/openspec/'];
|
||||||
const identifierAllowPatterns = [
|
const identifierAllowPatterns = [
|
||||||
/^packages\/cli\/src\/(?:index|managed-local-embeddings|managed-python-command|managed-python-daemon|managed-python-runtime|runtime)(?:\.test)?\.ts$/,
|
/^packages\/cli\/src\/(?:index|managed-local-embeddings|managed-python-command|managed-python-daemon|managed-python-runtime|release-version|runtime)(?:\.test)?\.ts$/,
|
||||||
|
/^python\/ktx-daemon\/src\/ktx_daemon\/__init__\.py$/,
|
||||||
/^scripts\/(?:build-public-npm-package|build-python-runtime-wheel|local-embeddings-runtime-smoke|package-artifacts|public-npm-release-metadata|publish-public-npm-package|published-package-smoke|release-readiness)(?:\.test)?\.mjs$/,
|
/^scripts\/(?:build-public-npm-package|build-python-runtime-wheel|local-embeddings-runtime-smoke|package-artifacts|public-npm-release-metadata|publish-public-npm-package|published-package-smoke|release-readiness)(?:\.test)?\.mjs$/,
|
||||||
];
|
];
|
||||||
const forbiddenIdentifierTerms = ['kae' + 'lio', 'Kae' + 'lio', 'KAE' + 'LIO_'];
|
const forbiddenIdentifierTerms = ['kae' + 'lio', 'Kae' + 'lio', 'KAE' + 'LIO_'];
|
||||||
|
|
@ -87,7 +88,10 @@ function scansForLlmBoundaries(relativePath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTestSource(relativePath) {
|
function isTestSource(relativePath) {
|
||||||
return /(?:^|\/)[^/]+\.(?:test|spec)\.[cm]?[jt]sx?$/.test(relativePath);
|
return (
|
||||||
|
/(?:^|\/)[^/]+\.(?:test|spec)\.[cm]?[jt]sx?$/.test(relativePath) ||
|
||||||
|
/(?:^|\/)tests\/(?:.+\/)?(?:test_[^/]+|[^/]+_test)\.py$/.test(relativePath)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scansForContextProductionLlmBoundaries(relativePath) {
|
function scansForContextProductionLlmBoundaries(relativePath) {
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ describe('scanFileContent', () => {
|
||||||
|
|
||||||
assert.equal(scanFileContent('packages/cli/src/setup.test.ts', `project: ${name}-dev`).length, 0);
|
assert.equal(scanFileContent('packages/cli/src/setup.test.ts', `project: ${name}-dev`).length, 0);
|
||||||
assert.equal(scanFileContent('packages/context/src/ingest/importer.test.ts', `email: system@${name}.dev`).length, 0);
|
assert.equal(scanFileContent('packages/context/src/ingest/importer.test.ts', `email: system@${name}.dev`).length, 0);
|
||||||
|
assert.equal(scanFileContent('python/ktx-daemon/tests/test_package.py', `${name}-ktx`).length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows public package identifiers in release packaging and managed runtime source', () => {
|
it('allows public package identifiers in release packaging and managed runtime source', () => {
|
||||||
|
|
@ -79,7 +80,9 @@ describe('scanFileContent', () => {
|
||||||
assert.equal(scanFileContent('scripts/package-artifacts.test.mjs', `${name}-ktx`).length, 0);
|
assert.equal(scanFileContent('scripts/package-artifacts.test.mjs', `${name}-ktx`).length, 0);
|
||||||
assert.equal(scanFileContent('scripts/public-npm-release-metadata.mjs', `@${name}/ktx`).length, 0);
|
assert.equal(scanFileContent('scripts/public-npm-release-metadata.mjs', `@${name}/ktx`).length, 0);
|
||||||
assert.equal(scanFileContent('scripts/publish-public-npm-package.test.mjs', `@${name}/ktx`).length, 0);
|
assert.equal(scanFileContent('scripts/publish-public-npm-package.test.mjs', `@${name}/ktx`).length, 0);
|
||||||
|
assert.equal(scanFileContent('packages/cli/src/release-version.ts', `@${name}/ktx`).length, 0);
|
||||||
assert.equal(scanFileContent('packages/cli/src/managed-python-runtime.ts', `${name}_ktx`).length, 0);
|
assert.equal(scanFileContent('packages/cli/src/managed-python-runtime.ts', `${name}_ktx`).length, 0);
|
||||||
|
assert.equal(scanFileContent('python/ktx-daemon/src/ktx_daemon/__init__.py', `${name}-ktx`).length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows clean source files and clean runtime prompt assets', () => {
|
it('allows clean source files and clean runtime prompt assets', () => {
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ async function writeUploadableArtifactFixtures(layout) {
|
||||||
`${packageInfo.name}-tarball`,
|
`${packageInfo.name}-tarball`,
|
||||||
]),
|
]),
|
||||||
[
|
[
|
||||||
join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'),
|
join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'),
|
||||||
'kaelio-ktx-runtime-wheel',
|
'kaelio-ktx-runtime-wheel',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
@ -139,7 +139,7 @@ describe('packageReleaseMetadata', () => {
|
||||||
ecosystem: 'python',
|
ecosystem: 'python',
|
||||||
packageName: 'kaelio-ktx',
|
packageName: 'kaelio-ktx',
|
||||||
packageRoot: 'python/runtime-wheel',
|
packageRoot: 'python/runtime-wheel',
|
||||||
packageVersion: '0.1.0',
|
packageVersion: '0.1.0rc1',
|
||||||
private: false,
|
private: false,
|
||||||
releaseMode: 'ci-artifact-only',
|
releaseMode: 'ci-artifact-only',
|
||||||
},
|
},
|
||||||
|
|
@ -154,10 +154,10 @@ describe('findPythonArtifacts', () => {
|
||||||
it('finds the bundled runtime wheel only', async () => {
|
it('finds the bundled runtime wheel only', async () => {
|
||||||
const root = await mkdtemp(join(tmpdir(), 'ktx-artifacts-test-'));
|
const root = await mkdtemp(join(tmpdir(), 'ktx-artifacts-test-'));
|
||||||
try {
|
try {
|
||||||
await writeFile(join(root, 'kaelio_ktx-0.1.0-py3-none-any.whl'), '');
|
await writeFile(join(root, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), '');
|
||||||
|
|
||||||
assert.deepEqual(await findPythonArtifacts(root), {
|
assert.deepEqual(await findPythonArtifacts(root), {
|
||||||
runtimeWheel: join(root, 'kaelio_ktx-0.1.0-py3-none-any.whl'),
|
runtimeWheel: join(root, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'),
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true });
|
await rm(root, { recursive: true, force: true });
|
||||||
|
|
@ -210,7 +210,7 @@ describe('artifact manifest', () => {
|
||||||
ecosystem: 'python',
|
ecosystem: 'python',
|
||||||
packageName: 'kaelio-ktx',
|
packageName: 'kaelio-ktx',
|
||||||
packageRoot: 'python/runtime-wheel',
|
packageRoot: 'python/runtime-wheel',
|
||||||
packageVersion: '0.1.0',
|
packageVersion: '0.1.0rc1',
|
||||||
private: false,
|
private: false,
|
||||||
releaseMode: 'ci-artifact-only',
|
releaseMode: 'ci-artifact-only',
|
||||||
},
|
},
|
||||||
|
|
@ -252,8 +252,8 @@ describe('artifact manifest', () => {
|
||||||
artifactKind: 'wheel',
|
artifactKind: 'wheel',
|
||||||
ecosystem: 'python',
|
ecosystem: 'python',
|
||||||
packageName: 'kaelio-ktx',
|
packageName: 'kaelio-ktx',
|
||||||
packageVersion: '0.1.0',
|
packageVersion: '0.1.0rc1',
|
||||||
path: 'python/kaelio_ktx-0.1.0-py3-none-any.whl',
|
path: 'python/kaelio_ktx-0.1.0rc1-py3-none-any.whl',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -362,17 +362,17 @@ describe('copyRuntimeWheelAssets', () => {
|
||||||
try {
|
try {
|
||||||
await mkdir(layout.pythonDir, { recursive: true });
|
await mkdir(layout.pythonDir, { recursive: true });
|
||||||
await writeFile(
|
await writeFile(
|
||||||
join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'),
|
join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'),
|
||||||
'kaelio-ktx-runtime-wheel',
|
'kaelio-ktx-runtime-wheel',
|
||||||
);
|
);
|
||||||
|
|
||||||
const assets = await copyRuntimeWheelAssets(layout, {
|
const assets = await copyRuntimeWheelAssets(layout, {
|
||||||
runtimeWheel: join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'),
|
runtimeWheel: join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'),
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
assets.wheelPath,
|
assets.wheelPath,
|
||||||
join(root, 'packages', 'cli', 'assets', 'python', 'kaelio_ktx-0.1.0-py3-none-any.whl'),
|
join(root, 'packages', 'cli', 'assets', 'python', 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'),
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
assets.manifestPath,
|
assets.manifestPath,
|
||||||
|
|
@ -385,7 +385,7 @@ describe('copyRuntimeWheelAssets', () => {
|
||||||
normalizedName: RUNTIME_WHEEL_NORMALIZED_NAME,
|
normalizedName: RUNTIME_WHEEL_NORMALIZED_NAME,
|
||||||
version: RUNTIME_WHEEL_PACKAGE_VERSION,
|
version: RUNTIME_WHEEL_PACKAGE_VERSION,
|
||||||
wheel: {
|
wheel: {
|
||||||
file: 'kaelio_ktx-0.1.0-py3-none-any.whl',
|
file: 'kaelio_ktx-0.1.0rc1-py3-none-any.whl',
|
||||||
sha256: createHash('sha256')
|
sha256: createHash('sha256')
|
||||||
.update('kaelio-ktx-runtime-wheel')
|
.update('kaelio-ktx-runtime-wheel')
|
||||||
.digest('hex'),
|
.digest('hex'),
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ export const PUBLIC_NPM_RELEASE_TAGS = new Set(['latest', 'next']);
|
||||||
|
|
||||||
const SEMVER_PATTERN =
|
const SEMVER_PATTERN =
|
||||||
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
||||||
|
const SEMVER_PARTS_PATTERN =
|
||||||
|
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
||||||
|
|
||||||
function scriptRootDir() {
|
function scriptRootDir() {
|
||||||
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
|
@ -29,6 +31,30 @@ export function assertPublicNpmPackageVersion(version) {
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function publicNpmPackageVersionToPythonVersion(version) {
|
||||||
|
const safeVersion = assertPublicNpmPackageVersion(version);
|
||||||
|
const match = SEMVER_PARTS_PATTERN.exec(safeVersion);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Invalid public npm package version: ${version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, major, minor, patch, prerelease, buildMetadata] = match;
|
||||||
|
if (buildMetadata) {
|
||||||
|
throw new Error(`Unsupported public npm build metadata for Python runtime version: ${safeVersion}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseVersion = `${major}.${minor}.${patch}`;
|
||||||
|
if (!prerelease) {
|
||||||
|
return baseVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rcMatch = /^rc\.([1-9]\d*|0)$/.exec(prerelease);
|
||||||
|
if (!rcMatch) {
|
||||||
|
throw new Error(`Unsupported public npm prerelease for Python runtime version: ${safeVersion}`);
|
||||||
|
}
|
||||||
|
return `${baseVersion}rc${rcMatch[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function assertPublicNpmReleaseTag(tag) {
|
export function assertPublicNpmReleaseTag(tag) {
|
||||||
if (!PUBLIC_NPM_RELEASE_TAGS.has(tag)) {
|
if (!PUBLIC_NPM_RELEASE_TAGS.has(tag)) {
|
||||||
throw new Error(`Invalid public npm release tag: ${tag}`);
|
throw new Error(`Invalid public npm release tag: ${tag}`);
|
||||||
|
|
@ -51,3 +77,7 @@ export function readPublicNpmReleaseMetadata(rootDir = scriptRootDir()) {
|
||||||
export function publicNpmPackageVersion(rootDir = scriptRootDir()) {
|
export function publicNpmPackageVersion(rootDir = scriptRootDir()) {
|
||||||
return readPublicNpmReleaseMetadata(rootDir).version;
|
return readPublicNpmReleaseMetadata(rootDir).version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function publicPythonRuntimePackageVersion(rootDir = scriptRootDir()) {
|
||||||
|
return publicNpmPackageVersionToPythonVersion(publicNpmPackageVersion(rootDir));
|
||||||
|
}
|
||||||
|
|
|
||||||
26
scripts/public-npm-release-metadata.test.mjs
Normal file
26
scripts/public-npm-release-metadata.test.mjs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
|
||||||
|
import { publicNpmPackageVersionToPythonVersion } from './public-npm-release-metadata.mjs';
|
||||||
|
|
||||||
|
describe('publicNpmPackageVersionToPythonVersion', () => {
|
||||||
|
it('keeps stable public npm versions unchanged for Python wheels', () => {
|
||||||
|
assert.equal(publicNpmPackageVersionToPythonVersion('1.2.3'), '1.2.3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts semantic-release rc versions to PEP 440 rc versions', () => {
|
||||||
|
assert.equal(publicNpmPackageVersionToPythonVersion('0.1.0-rc.1'), '0.1.0rc1');
|
||||||
|
assert.equal(publicNpmPackageVersionToPythonVersion('2.0.0-rc.12'), '2.0.0rc12');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects unsupported prerelease and build metadata forms', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => publicNpmPackageVersionToPythonVersion('1.2.3-beta.1'),
|
||||||
|
/Unsupported public npm prerelease for Python runtime version/,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => publicNpmPackageVersionToPythonVersion('1.2.3+build.1'),
|
||||||
|
/Unsupported public npm build metadata for Python runtime version/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -37,7 +37,7 @@ async function writeUploadableArtifactFixtures(layout) {
|
||||||
layout.npmTarballs[packageInfo.name],
|
layout.npmTarballs[packageInfo.name],
|
||||||
`${packageInfo.name}-tarball`,
|
`${packageInfo.name}-tarball`,
|
||||||
]),
|
]),
|
||||||
[join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'), 'kaelio-ktx-runtime-wheel'],
|
[join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), 'kaelio-ktx-runtime-wheel'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for (const [path, contents] of fileContents) {
|
for (const [path, contents] of fileContents) {
|
||||||
|
|
|
||||||
4
uv.lock
generated
4
uv.lock
generated
|
|
@ -440,7 +440,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ktx-daemon"
|
name = "ktx-daemon"
|
||||||
version = "0.1.0"
|
version = "0.0.0+private"
|
||||||
source = { editable = "python/ktx-daemon" }
|
source = { editable = "python/ktx-daemon" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
|
|
@ -495,7 +495,7 @@ dev = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ktx-sl"
|
name = "ktx-sl"
|
||||||
version = "0.1.0"
|
version = "0.0.0+private"
|
||||||
source = { editable = "python/ktx-sl" }
|
source = { editable = "python/ktx-sl" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue