mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
fix(cli): remove legacy ingest and wiki commands
This commit is contained in:
parent
011d694ed3
commit
75e04cfa56
41 changed files with 328 additions and 851 deletions
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: "ktx ingest"
|
||||
description: "Build, inspect, and replay KTX context ingest runs."
|
||||
description: "Build or refresh KTX context from configured connections."
|
||||
---
|
||||
|
||||
`ktx ingest` builds or refreshes KTX context from configured connections.
|
||||
|
|
@ -36,16 +36,6 @@ connections when you use `--all`.
|
|||
database connections. Query-history flags apply only to database connections
|
||||
that support query history.
|
||||
|
||||
## Status and replay
|
||||
|
||||
| Subcommand | Description |
|
||||
|------------|-------------|
|
||||
| `status [runId]` | Print status for the latest or selected stored ingest run or report file |
|
||||
| `replay <runId>` | Replay a stored ingest run or bundle report through memory-flow output |
|
||||
|
||||
Both subcommands accept `--report-file <path>`, `--plain`, `--json`, `--viz`,
|
||||
and `--no-input`.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
|
|
@ -57,14 +47,6 @@ ktx ingest warehouse --query-history-window-days 30
|
|||
ktx ingest notion
|
||||
ktx ingest --all
|
||||
ktx ingest --all --deep
|
||||
|
||||
ktx ingest status
|
||||
ktx ingest status run-abc123
|
||||
ktx ingest status --json
|
||||
|
||||
ktx ingest replay run-abc123
|
||||
ktx ingest replay run-abc123 --viz
|
||||
ktx ingest replay run-abc123 --report-file /tmp/ingest-report.json
|
||||
```
|
||||
|
||||
## Common errors
|
||||
|
|
@ -74,5 +56,4 @@ ktx ingest replay run-abc123 --report-file /tmp/ingest-report.json
|
|||
| Connection not configured | The connection id is not present in `ktx.yaml` | Add the connection with `ktx setup` or update `ktx.yaml` |
|
||||
| Deep readiness is missing | `--deep` or query history needs model, embedding, and scan-enrichment configuration | Run `ktx setup` or rerun with `--fast` |
|
||||
| Query history is unsupported | The selected database driver does not support query history | Run schema ingest without query-history flags |
|
||||
| Latest run not found | No stored ingest report exists in this project | Run `ktx ingest <connectionId>` first |
|
||||
| Visual replay fails in a non-interactive shell | Visual report replay needs a terminal | Use `ktx ingest status --json` for agent and CI workflows |
|
||||
| No ingest target was selected | No connection id was provided and `--all` was omitted | Run `ktx ingest <connectionId>` or `ktx ingest --all` |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: "ktx wiki"
|
||||
description: "List, read, search, or write wiki pages."
|
||||
description: "List or search wiki pages."
|
||||
---
|
||||
|
||||
Manage wiki pages in your KTX project. Wiki pages are Markdown documents that capture business definitions, rules, and gotchas. Agents search them for context when answering questions about your data.
|
||||
|
|
@ -16,9 +16,7 @@ ktx wiki <subcommand> [options]
|
|||
| Subcommand | Description |
|
||||
|-----------|-------------|
|
||||
| `list` | List local wiki pages |
|
||||
| `read <key>` | Read one local wiki page |
|
||||
| `search <query>` | Search local wiki pages |
|
||||
| `write <key>` | Write one local wiki page |
|
||||
|
||||
## Options
|
||||
|
||||
|
|
@ -29,13 +27,6 @@ ktx wiki <subcommand> [options]
|
|||
| `--json` | Print JSON output | `false` |
|
||||
| `--user-id <id>` | Local user id | `local` |
|
||||
|
||||
### `wiki read`
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--json` | Print JSON output | `false` |
|
||||
| `--user-id <id>` | Local user id | `local` |
|
||||
|
||||
### `wiki search`
|
||||
|
||||
| Flag | Description | Default |
|
||||
|
|
@ -44,18 +35,6 @@ ktx wiki <subcommand> [options]
|
|||
| `--user-id <id>` | Local user id | `local` |
|
||||
| `--limit <number>` | Maximum search results | — |
|
||||
|
||||
### `wiki write`
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--user-id <id>` | Local user id | `local` |
|
||||
| `--scope <scope>` | Scope: `global` or `user` | `global` |
|
||||
| `--summary <summary>` | Wiki page summary (required) | — |
|
||||
| `--content <content>` | Wiki page content (required) | — |
|
||||
| `--tag <tag>` | Wiki tag; repeatable | — |
|
||||
| `--ref <ref>` | Wiki ref; repeatable | — |
|
||||
| `--sl-ref <ref>` | Semantic-layer ref; repeatable | — |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
|
|
@ -65,48 +44,17 @@ ktx wiki list
|
|||
# List all wiki pages as JSON
|
||||
ktx wiki list --json
|
||||
|
||||
# Read a specific wiki page
|
||||
ktx wiki read revenue-definitions
|
||||
|
||||
# Read a specific wiki page as JSON
|
||||
ktx wiki read revenue-definitions --json
|
||||
|
||||
# Search wiki pages
|
||||
ktx wiki search "monthly recurring revenue"
|
||||
|
||||
# Search wiki pages as JSON
|
||||
ktx wiki search "monthly recurring revenue" --json --limit 10
|
||||
|
||||
# Write a global wiki page
|
||||
ktx wiki write revenue-definitions \
|
||||
--summary "Canonical revenue metric definitions" \
|
||||
--content "## MRR\nMonthly Recurring Revenue is calculated as..."
|
||||
|
||||
# Write a user-scoped wiki page
|
||||
ktx wiki write my-notes \
|
||||
--scope user \
|
||||
--summary "Personal analysis notes" \
|
||||
--content "Things to check when revenue numbers look off..."
|
||||
|
||||
# Write a page with tags and references
|
||||
ktx wiki write churn-rules \
|
||||
--summary "Churn calculation business rules" \
|
||||
--content "A customer is considered churned when..." \
|
||||
--tag finance \
|
||||
--tag retention \
|
||||
--sl-ref customers \
|
||||
--sl-ref subscriptions
|
||||
|
||||
# Write a page with external references
|
||||
ktx wiki write data-freshness \
|
||||
--summary "Data pipeline SLAs and freshness guarantees" \
|
||||
--content "The orders table refreshes every 15 minutes..." \
|
||||
--ref "https://wiki.example.com/data-pipelines"
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Wiki commands print local wiki pages and search results. Agents should search first, then read the most relevant page by key.
|
||||
Wiki commands print local wiki page listings and search results. Open the
|
||||
matching Markdown files directly when you need the full page contents.
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -128,6 +76,4 @@ Wiki commands print local wiki pages and search results. Agents should search fi
|
|||
| Error | Cause | Recovery |
|
||||
|-------|-------|----------|
|
||||
| Search returns no results | The query terms do not match summaries, tags, or content | Retry with business synonyms, then create a page if the knowledge is missing |
|
||||
| Read fails for a key | The page key is wrong or scoped to a different user | Run `ktx wiki list` or search again to get the exact key |
|
||||
| Write fails due to missing fields | `--summary` or `--content` was omitted | Pass both fields, and keep the summary short enough for search results |
|
||||
| Agent writes duplicate pages | It did not search existing pages first | Always run `ktx wiki search` before `ktx wiki write` |
|
||||
| A page is missing | No Markdown file exists for that business context | Add a file under `wiki/` or run `ktx ingest <connectionId>` |
|
||||
|
|
|
|||
|
|
@ -211,8 +211,8 @@ KTX writes project state as plain files so agents can inspect and edit changes i
|
|||
| `ktx.yaml` | `ktx setup` | Main project configuration: connections, LLM settings, embeddings, and context sources |
|
||||
| `.ktx/secrets/*` | `ktx setup` when file-backed secrets are selected | Local secret files referenced from `ktx.yaml`; do not commit these |
|
||||
| `semantic-layer/<connection-id>/*.yaml` | context build, ingestion, or direct file edits | Semantic source definitions agents use for SQL generation |
|
||||
| `wiki/global/*.md` | ingestion, memory capture, `ktx wiki write --scope global`, or direct file edits | Shared business context and metric definitions |
|
||||
| `wiki/user/<user-id>/*.md` | memory capture, `ktx wiki write --scope user`, or direct file edits | User-scoped notes for one agent/user context |
|
||||
| `wiki/global/*.md` | ingestion, memory capture, or direct file edits | Shared business context and metric definitions |
|
||||
| `wiki/user/<user-id>/*.md` | memory capture or direct file edits | User-scoped notes for one agent/user context |
|
||||
| `.claude/skills/ktx/SKILL.md`, `.agents/skills/ktx/SKILL.md` | CLI-mode agent integration setup | Agent instructions for calling public `ktx` commands |
|
||||
|
||||
## Verify it worked
|
||||
|
|
|
|||
|
|
@ -87,21 +87,7 @@ Useful output flags:
|
|||
| `--json` | Output as JSON |
|
||||
| `--plain` | Plain text output |
|
||||
|
||||
### Inspecting stored reports
|
||||
|
||||
```bash
|
||||
# Check status of the latest ingest
|
||||
ktx ingest status
|
||||
|
||||
# Check a specific run
|
||||
ktx ingest status <run-id>
|
||||
|
||||
# Replay a past ingest run
|
||||
ktx ingest replay <run-id>
|
||||
```
|
||||
|
||||
`ktx ingest replay` opens the stored memory-flow output for a completed run.
|
||||
Foreground context builds do not detach into background control sessions; if a
|
||||
Foreground context builds do not detach into background control sessions. If a
|
||||
run is interrupted, rerun `ktx ingest <connection-id>` or `ktx ingest --all`.
|
||||
|
||||
### Supported context sources
|
||||
|
|
@ -178,12 +164,8 @@ sl_refs: [orders]
|
|||
Orders in "pending" status for more than 48 hours are flagged for review.
|
||||
```
|
||||
|
||||
### Deterministic replay
|
||||
### Ingest transcripts
|
||||
|
||||
Every ingest session records a full transcript — tool calls, LLM responses, and write decisions. You can replay any session to debug why a source was written a certain way:
|
||||
|
||||
```bash
|
||||
ktx ingest replay <run-id> --viz
|
||||
```
|
||||
|
||||
This opens the same TUI view as the original run, letting you step through the agent's reasoning.
|
||||
Every ingest session records a full transcript: tool calls, LLM responses, and
|
||||
write decisions. Inspect the stored transcript files when you need to debug why
|
||||
a source was written a certain way.
|
||||
|
|
|
|||
|
|
@ -248,8 +248,7 @@ wiki/
|
|||
### Editing pages
|
||||
|
||||
Create and edit wiki pages directly as Markdown files in the `wiki/`
|
||||
directory, or with `ktx wiki write`. Ingest and memory capture also create
|
||||
these pages automatically.
|
||||
directory. Ingest and memory capture also create these pages automatically.
|
||||
|
||||
Wiki page fields:
|
||||
|
||||
|
|
|
|||
|
|
@ -125,8 +125,6 @@ All supported agent clients call the same KTX CLI commands:
|
|||
|---------|-------------|
|
||||
| `ktx status --json` | Return project setup and context readiness |
|
||||
| `ktx wiki search <query> --json` | Search wiki pages |
|
||||
| `ktx wiki read <key> --json` | Read a wiki page |
|
||||
| `ktx wiki write <key>` | Write or update a wiki page |
|
||||
| `ktx sl list --json` | List semantic-layer sources |
|
||||
| `ktx sl search <query> --json` | Search semantic-layer sources |
|
||||
| `ktx sl validate <source> --connection-id <id>` | Validate semantic source definitions |
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ Agents must configure and ingest context sources in this order:
|
|||
2. Store tokens as `env:NAME` or `file:/path/to/secret`.
|
||||
3. Run `ktx ingest <connectionId>` for one source or `ktx ingest --all` for
|
||||
every configured source.
|
||||
4. Check progress with `ktx ingest status --json`.
|
||||
4. Review the foreground ingest output.
|
||||
5. Review generated `semantic-layer/` YAML and `wiki/` Markdown files in git.
|
||||
6. Validate changed semantic sources with `ktx sl validate`.
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,27 @@ type CommandPathNode = CommandWithGlobalOptions & {
|
|||
};
|
||||
|
||||
const PROJECT_AWARE_ROOT_COMMANDS = new Set(['setup', 'connection', 'ingest', 'wiki', 'sl', 'status']);
|
||||
const REMOVED_ROOT_COMMANDS = new Set(['scan']);
|
||||
const REMOVED_COMMAND_PATHS = new Set([
|
||||
'scan',
|
||||
'ingest run',
|
||||
'ingest status',
|
||||
'ingest watch',
|
||||
'ingest replay',
|
||||
'wiki read',
|
||||
'wiki write',
|
||||
]);
|
||||
const GLOBAL_OPTIONS_WITH_VALUE = new Set(['--project-dir']);
|
||||
const OPTIONS_WITH_VALUE = new Set([
|
||||
'--project-dir',
|
||||
'--query-history-window-days',
|
||||
'--user-id',
|
||||
'--limit',
|
||||
'--format',
|
||||
'--connection-id',
|
||||
'--source-name',
|
||||
'--query-file',
|
||||
'--max-rows',
|
||||
]);
|
||||
|
||||
export interface CommandWithGlobalOptions {
|
||||
opts: () => object;
|
||||
|
|
@ -175,9 +195,6 @@ function shouldSuppressProjectDirLine(path: string[], options: Record<string, un
|
|||
return true;
|
||||
}
|
||||
|
||||
if (commandPathKey === 'ktx ingest watch') {
|
||||
return options.json !== true && options.plain !== true;
|
||||
}
|
||||
const demoIndex = path.indexOf('demo');
|
||||
if (demoIndex >= 0) {
|
||||
const demoCommand = path[demoIndex + 1];
|
||||
|
|
@ -222,10 +239,6 @@ function createBaseProgram(info: KtxCliPackageInfo, io: KtxCliIo): Command {
|
|||
.version(`${info.name} ${info.version}`, '-v, --version', 'Show CLI version')
|
||||
.helpOption('-h, --help', 'Show this help text')
|
||||
.configureHelp({ showGlobalOptions: true })
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\nAdvanced:\n ktx dev Low-level project initialization and runtime management.\n',
|
||||
)
|
||||
.showHelpAfterError()
|
||||
.exitOverride()
|
||||
.configureOutput({
|
||||
|
|
@ -255,6 +268,45 @@ function formatCliError(error: unknown): string {
|
|||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function commandPathFromArgv(argv: string[]): string[] {
|
||||
const path: string[] = [];
|
||||
for (let index = 0; index < argv.length && path.length < 2; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (arg === '--') {
|
||||
break;
|
||||
}
|
||||
if ((path.length === 0 ? GLOBAL_OPTIONS_WITH_VALUE : OPTIONS_WITH_VALUE).has(arg)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const optionsWithValue = path.length === 0 ? GLOBAL_OPTIONS_WITH_VALUE : OPTIONS_WITH_VALUE;
|
||||
if ([...optionsWithValue].some((option) => arg.startsWith(`${option}=`))) {
|
||||
continue;
|
||||
}
|
||||
if (path.length === 0 && arg === '--debug') {
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith('-')) {
|
||||
continue;
|
||||
}
|
||||
path.push(arg);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function removedCommandName(argv: string[]): string | null {
|
||||
const path = commandPathFromArgv(argv);
|
||||
if (path.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pathKey = path.join(' ');
|
||||
return REMOVED_COMMAND_PATHS.has(pathKey) ? path.at(-1) ?? null : null;
|
||||
}
|
||||
|
||||
async function runBareInteractiveCommand(
|
||||
program: Command,
|
||||
io: KtxCliIo,
|
||||
|
|
@ -309,10 +361,7 @@ export function buildKtxProgram(options: BuildKtxProgramOptions): Command {
|
|||
|
||||
registerSetupCommands(program, context);
|
||||
registerConnectionCommands(program, context);
|
||||
registerIngestCommands(program, context, {
|
||||
runIngestWithProgress: async (ingestArgs, ingestIo, ingestDeps, defaultRunIngest) =>
|
||||
await (ingestDeps.ingest ?? defaultRunIngest)(ingestArgs, ingestIo),
|
||||
});
|
||||
registerIngestCommands(program, context);
|
||||
registerWikiCommands(program, context);
|
||||
registerSlCommands(program, context);
|
||||
registerStatusCommands(program, context);
|
||||
|
|
@ -366,8 +415,9 @@ export async function runCommanderKtxCli(
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (REMOVED_ROOT_COMMANDS.has(argv[0] ?? '')) {
|
||||
io.stderr.write(`error: unknown command '${argv[0]}'\n`);
|
||||
const removedCommand = removedCommandName(argv);
|
||||
if (removedCommand) {
|
||||
io.stderr.write(`error: unknown command '${removedCommand}'\n`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { createRequire } from 'node:module';
|
|||
|
||||
import type { KtxConnectionArgs } from './connection.js';
|
||||
import type { KtxDoctorArgs } from './doctor.js';
|
||||
import type { KtxIngestArgs } from './ingest.js';
|
||||
import type { KtxKnowledgeArgs } from './knowledge.js';
|
||||
import type { KtxPublicIngestArgs } from './public-ingest.js';
|
||||
import type { KtxRuntimeArgs } from './runtime.js';
|
||||
|
|
@ -29,7 +28,6 @@ export interface KtxCliDeps {
|
|||
setup?: (args: KtxSetupArgs, io: KtxCliIo) => Promise<number>;
|
||||
connection?: (args: KtxConnectionArgs, io: KtxCliIo) => Promise<number>;
|
||||
doctor?: (args: KtxDoctorArgs, io: KtxCliIo) => Promise<number>;
|
||||
ingest?: (args: KtxIngestArgs, io: KtxCliIo) => Promise<number>;
|
||||
publicIngest?: (args: KtxPublicIngestArgs, io: KtxCliIo) => Promise<number>;
|
||||
runtime?: (args: KtxRuntimeArgs, io: KtxCliIo) => Promise<number>;
|
||||
knowledge?: (args: KtxKnowledgeArgs, io: KtxCliIo) => Promise<number>;
|
||||
|
|
|
|||
|
|
@ -3,19 +3,6 @@ import { z } from 'zod';
|
|||
const projectDirSchema = z.string().min(1);
|
||||
const stringArraySchema = z.array(z.string());
|
||||
|
||||
export const wikiWriteCommandSchema = z.object({
|
||||
command: z.literal('write'),
|
||||
projectDir: projectDirSchema,
|
||||
key: z.string().min(1),
|
||||
scope: z.enum(['GLOBAL', 'USER']),
|
||||
userId: z.string().min(1),
|
||||
summary: z.string().min(1),
|
||||
content: z.string().min(1),
|
||||
tags: stringArraySchema,
|
||||
refs: stringArraySchema,
|
||||
slRefs: stringArraySchema,
|
||||
});
|
||||
|
||||
const orderBySchema = z.union([
|
||||
z.string().min(1),
|
||||
z.object({
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ describe('walkCommandTree', () => {
|
|||
expect(walkCommandTree(command).arguments).toEqual(['<connectionId>', '[schemas...]']);
|
||||
});
|
||||
|
||||
it('omits Commander hidden commands from the public tree', () => {
|
||||
it('walks registered commands without applying hidden-command policy', () => {
|
||||
const root = new Command('ktx');
|
||||
root.command('scan', { hidden: true }).description('Run a standalone connection scan');
|
||||
const ingest = root.command('ingest').description('Build or inspect KTX context');
|
||||
|
|
@ -64,10 +64,19 @@ describe('walkCommandTree', () => {
|
|||
|
||||
const tree = walkCommandTree(root);
|
||||
|
||||
expect(tree.children.map((child) => child.name)).toEqual(['ingest', 'status']);
|
||||
expect(tree.children.map((child) => child.name)).toEqual(['scan', 'ingest', 'status']);
|
||||
expect(tree.children[0]).toMatchObject({
|
||||
name: 'scan',
|
||||
description: 'Run a standalone connection scan',
|
||||
children: [],
|
||||
});
|
||||
expect(tree.children[1]).toMatchObject({
|
||||
name: 'ingest',
|
||||
children: [{ name: 'status', description: 'Print status', aliases: [], arguments: [], children: [] }],
|
||||
children: [
|
||||
{ name: 'run', description: 'Run local ingest by adapter', aliases: [], arguments: [], children: [] },
|
||||
{ name: 'watch', description: 'Open a stored visual report', aliases: [], arguments: [], children: [] },
|
||||
{ name: 'status', description: 'Print status', aliases: [], arguments: [], children: [] },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,17 +10,13 @@ export interface CommandTreeNode {
|
|||
children: CommandTreeNode[];
|
||||
}
|
||||
|
||||
function isHiddenCommand(command: CommandUnknownOpts): boolean {
|
||||
return (command as CommandUnknownOpts & { _hidden?: boolean })._hidden === true;
|
||||
}
|
||||
|
||||
export function walkCommandTree(command: CommandUnknownOpts): CommandTreeNode {
|
||||
return {
|
||||
name: command.name(),
|
||||
description: command.description(),
|
||||
aliases: command.aliases(),
|
||||
arguments: command.registeredArguments.map(formatArgumentDeclaration),
|
||||
children: command.commands.filter((child) => !isHiddenCommand(child)).map((child) => walkCommandTree(child)),
|
||||
children: command.commands.map((child) => walkCommandTree(child)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,81 +1,15 @@
|
|||
import { resolve } from 'node:path';
|
||||
import { type Command, Option } from '@commander-js/extra-typings';
|
||||
import {
|
||||
type KtxCliCommandContext,
|
||||
type OutputModeOptions,
|
||||
parsePositiveIntegerOption,
|
||||
resolveCommandProjectDir,
|
||||
} from '../cli-program.js';
|
||||
import type { KtxCliDeps, KtxCliIo } from '../index.js';
|
||||
import type { KtxIngestArgs, KtxIngestOutputMode } from '../ingest.js';
|
||||
import { runtimeInstallPolicyFromFlags } from '../managed-python-command.js';
|
||||
import type { KtxPublicIngestArgs } from '../public-ingest.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
|
||||
profileMark('module:commands/ingest-commands');
|
||||
|
||||
interface IngestCommandOptions {
|
||||
runIngestWithProgress: (
|
||||
args: KtxIngestArgs,
|
||||
io: KtxCliIo,
|
||||
deps: KtxCliDeps,
|
||||
defaultRunIngest: (args: KtxIngestArgs, io: KtxCliIo) => Promise<number>,
|
||||
) => Promise<number>;
|
||||
}
|
||||
|
||||
function outputMode(options: OutputModeOptions): KtxIngestOutputMode {
|
||||
if (options.json === true) {
|
||||
return 'json';
|
||||
}
|
||||
if (options.viz === true) {
|
||||
return 'viz';
|
||||
}
|
||||
return 'plain';
|
||||
}
|
||||
|
||||
function watchOutputMode(options: OutputModeOptions): KtxIngestOutputMode {
|
||||
if (options.json === true) {
|
||||
return 'json';
|
||||
}
|
||||
if (options.plain === true) {
|
||||
return 'plain';
|
||||
}
|
||||
return 'viz';
|
||||
}
|
||||
|
||||
function inputMode(options: OutputModeOptions): Pick<KtxIngestArgs, 'inputMode'> {
|
||||
return options.input === false ? { inputMode: 'disabled' } : {};
|
||||
}
|
||||
|
||||
function resolvedOptions<T extends object>(command: { optsWithGlobals?: () => object }, fallback: T): T {
|
||||
return (command.optsWithGlobals ? command.optsWithGlobals() : fallback) as T;
|
||||
}
|
||||
|
||||
function assertOutputModeCompatible(options: OutputModeOptions): void {
|
||||
const requested = [
|
||||
options.plain === true ? '--plain' : undefined,
|
||||
options.json === true ? '--json' : undefined,
|
||||
options.viz === true ? '--viz' : undefined,
|
||||
].filter((option): option is string => option !== undefined);
|
||||
if (requested.length > 1) {
|
||||
throw new Error(`Output mode options cannot be used together: ${requested.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function runIngestArgs(
|
||||
context: KtxCliCommandContext,
|
||||
args: KtxIngestArgs,
|
||||
options: IngestCommandOptions,
|
||||
): Promise<void> {
|
||||
const { runKtxIngest } = await import('../ingest.js');
|
||||
context.setExitCode(await options.runIngestWithProgress(args, context.io, context.deps, runKtxIngest));
|
||||
}
|
||||
|
||||
export function registerIngestCommands(
|
||||
program: Command,
|
||||
context: KtxCliCommandContext,
|
||||
commandOptions: IngestCommandOptions,
|
||||
): void {
|
||||
export function registerIngestCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
const ingest = program
|
||||
.command('ingest')
|
||||
.description('Build or inspect KTX context')
|
||||
|
|
@ -114,123 +48,4 @@ export function registerIngestCommands(
|
|||
ingest.hook('preAction', (_thisCommand, actionCommand) => {
|
||||
context.writeDebug?.('ingest', actionCommand);
|
||||
});
|
||||
|
||||
ingest
|
||||
.command('run', { hidden: true })
|
||||
.description('Run local ingest for one configured connection and source adapter')
|
||||
.requiredOption('--connection-id <connectionId>', 'KTX connection id')
|
||||
.requiredOption('--adapter <adapter>', 'Ingest source adapter name')
|
||||
.option('--source-dir <path>', 'Directory containing source files')
|
||||
.option('--database-introspection-url <url>', 'Daemon URL for live-database introspection')
|
||||
.option('--debug-llm-request-file <path>', 'Write sanitized LLM request structure to a JSONL file')
|
||||
.option('--report-file <path>', 'Unsupported for ingest run; use ingest status/watch instead')
|
||||
.addOption(new Option('--plain', 'Print plain text output').conflicts(['json', 'viz']))
|
||||
.addOption(new Option('--json', 'Print JSON output').conflicts(['plain', 'viz']))
|
||||
.addOption(new Option('--viz', 'Render memory-flow TUI output').conflicts(['plain', 'json']))
|
||||
.option('--yes', 'Install the managed Python runtime without prompting when required', false)
|
||||
.option('--no-input', 'Disable interactive terminal input for visualization')
|
||||
.action(async (options, command) => {
|
||||
const commandOptionsWithGlobals = resolvedOptions(command, options);
|
||||
assertOutputModeCompatible(commandOptionsWithGlobals);
|
||||
if (options.reportFile) {
|
||||
throw new Error('--report-file is only supported for ingest status/watch');
|
||||
}
|
||||
await runIngestArgs(
|
||||
context,
|
||||
{
|
||||
command: 'run',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
connectionId: commandOptionsWithGlobals.connectionId,
|
||||
adapter: commandOptionsWithGlobals.adapter,
|
||||
sourceDir: commandOptionsWithGlobals.sourceDir ? resolve(commandOptionsWithGlobals.sourceDir) : undefined,
|
||||
databaseIntrospectionUrl: commandOptionsWithGlobals.databaseIntrospectionUrl || undefined,
|
||||
cliVersion: context.packageInfo.version,
|
||||
runtimeInstallPolicy: runtimeInstallPolicyFromFlags({ yes: commandOptionsWithGlobals.yes }),
|
||||
...(commandOptionsWithGlobals.debugLlmRequestFile
|
||||
? { debugLlmRequestFile: resolve(commandOptionsWithGlobals.debugLlmRequestFile) }
|
||||
: {}),
|
||||
outputMode: outputMode(commandOptionsWithGlobals),
|
||||
...inputMode(commandOptionsWithGlobals),
|
||||
},
|
||||
commandOptions,
|
||||
);
|
||||
});
|
||||
|
||||
ingest
|
||||
.command('status')
|
||||
.description('Print status for the latest or selected stored ingest report')
|
||||
.argument('[runId]', 'Local ingest id, report id, run id, or job id')
|
||||
.option('--report-file <path>', 'Bundle ingest report JSON file to render')
|
||||
.addOption(new Option('--plain', 'Print plain text output').conflicts(['json', 'viz']))
|
||||
.addOption(new Option('--json', 'Print JSON output').conflicts(['plain', 'viz']))
|
||||
.addOption(new Option('--viz', 'Render memory-flow TUI output').conflicts(['plain', 'json']))
|
||||
.option('--no-input', 'Disable interactive terminal input for visualization')
|
||||
.action(async (runId: string | undefined, options, command) => {
|
||||
const commandOptionsWithGlobals = resolvedOptions(command, options);
|
||||
assertOutputModeCompatible(commandOptionsWithGlobals);
|
||||
await runIngestArgs(
|
||||
context,
|
||||
{
|
||||
command: 'status',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
...(runId ? { runId } : {}),
|
||||
...(commandOptionsWithGlobals.reportFile ? { reportFile: resolve(commandOptionsWithGlobals.reportFile) } : {}),
|
||||
outputMode: outputMode(commandOptionsWithGlobals),
|
||||
...inputMode(commandOptionsWithGlobals),
|
||||
},
|
||||
commandOptions,
|
||||
);
|
||||
});
|
||||
|
||||
ingest
|
||||
.command('watch', { hidden: true })
|
||||
.description('Open the latest or selected stored ingest visual report')
|
||||
.argument('[runId]', 'Local ingest id, report id, run id, or job id')
|
||||
.option('--report-file <path>', 'Bundle ingest report JSON file to render')
|
||||
.addOption(new Option('--plain', 'Print plain text output').conflicts(['json', 'viz']))
|
||||
.addOption(new Option('--json', 'Print JSON output').conflicts(['plain', 'viz']))
|
||||
.addOption(new Option('--viz', 'Render memory-flow TUI output').conflicts(['plain', 'json']))
|
||||
.option('--no-input', 'Disable interactive terminal input for visualization')
|
||||
.action(async (runId: string | undefined, options, command) => {
|
||||
const commandOptionsWithGlobals = resolvedOptions(command, options);
|
||||
assertOutputModeCompatible(commandOptionsWithGlobals);
|
||||
await runIngestArgs(
|
||||
context,
|
||||
{
|
||||
command: 'watch',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
...(runId ? { runId } : {}),
|
||||
...(commandOptionsWithGlobals.reportFile ? { reportFile: resolve(commandOptionsWithGlobals.reportFile) } : {}),
|
||||
outputMode: watchOutputMode(commandOptionsWithGlobals),
|
||||
...inputMode(commandOptionsWithGlobals),
|
||||
},
|
||||
commandOptions,
|
||||
);
|
||||
});
|
||||
|
||||
ingest
|
||||
.command('replay')
|
||||
.description('Replay a stored ingest report through memory-flow output')
|
||||
.argument('<runId>', 'Local ingest id, report id, run id, or job id')
|
||||
.option('--report-file <path>', 'Bundle ingest report JSON file to render')
|
||||
.addOption(new Option('--plain', 'Print plain text output').conflicts(['json', 'viz']))
|
||||
.addOption(new Option('--json', 'Print JSON output').conflicts(['plain', 'viz']))
|
||||
.addOption(new Option('--viz', 'Render memory-flow TUI output').conflicts(['plain', 'json']))
|
||||
.option('--no-input', 'Disable interactive terminal input for visualization')
|
||||
.action(async (runId: string, options, command) => {
|
||||
const commandOptionsWithGlobals = resolvedOptions(command, options);
|
||||
assertOutputModeCompatible(commandOptionsWithGlobals);
|
||||
await runIngestArgs(
|
||||
context,
|
||||
{
|
||||
command: 'replay',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
runId,
|
||||
...(commandOptionsWithGlobals.reportFile ? { reportFile: resolve(commandOptionsWithGlobals.reportFile) } : {}),
|
||||
outputMode: outputMode(commandOptionsWithGlobals),
|
||||
...inputMode(commandOptionsWithGlobals),
|
||||
},
|
||||
commandOptions,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { type Command, Option } from '@commander-js/extra-typings';
|
||||
import type { Command } from '@commander-js/extra-typings';
|
||||
import {
|
||||
collectOption,
|
||||
type KtxCliCommandContext,
|
||||
parsePositiveIntegerOption,
|
||||
resolveCommandProjectDir,
|
||||
} from '../cli-program.js';
|
||||
import { wikiWriteCommandSchema } from '../command-schemas.js';
|
||||
import type { KtxKnowledgeArgs } from '../knowledge.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
|
||||
|
|
@ -19,7 +17,7 @@ async function runKnowledgeArgs(context: KtxCliCommandContext, args: KtxKnowledg
|
|||
export function registerWikiCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
const wiki = program
|
||||
.command('wiki')
|
||||
.description('List, read, search, or write local wiki pages')
|
||||
.description('List or search local wiki pages')
|
||||
.showHelpAfterError()
|
||||
.addHelpText(
|
||||
'after',
|
||||
|
|
@ -40,22 +38,6 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon
|
|||
});
|
||||
});
|
||||
|
||||
wiki
|
||||
.command('read')
|
||||
.description('Read one local wiki page')
|
||||
.argument('<key>', 'Wiki page key')
|
||||
.option('--json', 'Print JSON output', false)
|
||||
.option('--user-id <id>', 'Local user id', 'local')
|
||||
.action(async (key: string, options: { userId: string; json?: boolean }, command) => {
|
||||
await runKnowledgeArgs(context, {
|
||||
command: 'read',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
key,
|
||||
userId: options.userId,
|
||||
json: options.json,
|
||||
});
|
||||
});
|
||||
|
||||
wiki
|
||||
.command('search')
|
||||
.description('Search local wiki pages')
|
||||
|
|
@ -73,31 +55,4 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon
|
|||
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
||||
});
|
||||
});
|
||||
|
||||
wiki
|
||||
.command('write')
|
||||
.description('Write one local wiki page')
|
||||
.argument('<key>', 'Wiki page key')
|
||||
.option('--user-id <id>', 'Local user id', 'local')
|
||||
.addOption(new Option('--scope <scope>', 'global or user').choices(['global', 'user']).default('global'))
|
||||
.requiredOption('--summary <summary>', 'Wiki summary')
|
||||
.requiredOption('--content <content>', 'Wiki content')
|
||||
.option('--tag <tag>', 'Wiki tag; repeatable', collectOption, [])
|
||||
.option('--ref <ref>', 'Wiki ref; repeatable', collectOption, [])
|
||||
.option('--sl-ref <ref>', 'Semantic-layer ref; repeatable', collectOption, [])
|
||||
.action(async (key: string, options, command) => {
|
||||
const args = wikiWriteCommandSchema.parse({
|
||||
command: 'write',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
key,
|
||||
scope: options.scope === 'user' ? 'USER' : 'GLOBAL',
|
||||
userId: options.userId,
|
||||
summary: options.summary,
|
||||
content: options.content,
|
||||
tags: options.tag,
|
||||
refs: options.ref,
|
||||
slRefs: options.slRef,
|
||||
});
|
||||
await runKnowledgeArgs(context, args);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,14 +52,14 @@ describe('dev Commander tree', () => {
|
|||
expect(testIo.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('keeps dev callable while hiding it from root command rows', async () => {
|
||||
it('lists dev in root command rows', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Advanced:');
|
||||
expect(testIo.stdout()).toContain('ktx dev');
|
||||
expect(testIo.stdout()).not.toContain('dev Low-level diagnostics');
|
||||
expect(testIo.stdout()).not.toContain('Advanced:');
|
||||
expect(testIo.stdout()).toContain('dev');
|
||||
expect(testIo.stdout()).toMatch(/Low-level project initialization and runtime\s+management/);
|
||||
expect(testIo.stderr()).toBe('');
|
||||
});
|
||||
|
||||
|
|
@ -132,9 +132,8 @@ describe('dev Commander tree', () => {
|
|||
])('prints generated nested help for $argv', async ({ argv, expected }) => {
|
||||
const io = makeIo();
|
||||
const doctor = vi.fn(async () => 0);
|
||||
const ingest = vi.fn(async () => 0);
|
||||
|
||||
await expect(runKtxCli(argv, io.io, { doctor, ingest })).resolves.toBe(0);
|
||||
await expect(runKtxCli(argv, io.io, { doctor })).resolves.toBe(0);
|
||||
|
||||
for (const text of expected) {
|
||||
expect(io.stdout()).toContain(text);
|
||||
|
|
@ -145,28 +144,25 @@ describe('dev Commander tree', () => {
|
|||
}
|
||||
expect(io.stderr()).toBe('');
|
||||
expect(doctor).not.toHaveBeenCalled();
|
||||
expect(ingest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps legacy adapter-backed ingest run callable but hidden from ingest help', async () => {
|
||||
it('rejects removed adapter-backed ingest run and keeps it out of ingest help', async () => {
|
||||
const helpIo = makeIo();
|
||||
const runIo = makeIo();
|
||||
const ingest = vi.fn(async () => 0);
|
||||
const publicIngest = vi.fn(async () => 0);
|
||||
|
||||
await expect(runKtxCli(['ingest', '--help'], helpIo.io, { ingest })).resolves.toBe(0);
|
||||
await expect(runKtxCli(['ingest', '--help'], helpIo.io, { publicIngest })).resolves.toBe(0);
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase', '--project-dir', '/tmp/project'],
|
||||
runIo.io,
|
||||
{ ingest },
|
||||
{ publicIngest },
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(helpIo.stdout()).not.toMatch(/^ run\s/m);
|
||||
expect(ingest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ command: 'run', connectionId: 'warehouse', adapter: 'metabase' }),
|
||||
runIo.io,
|
||||
);
|
||||
expect(runIo.stderr()).toMatch(/unknown command|error:/);
|
||||
expect(publicIngest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
|
@ -177,17 +173,17 @@ describe('dev Commander tree', () => {
|
|||
{ argv: ['scan', 'warehouse', '--project-dir', '/tmp/project', '--mode', 'relationships'] },
|
||||
])('rejects removed top-level scan command $argv', async ({ argv }) => {
|
||||
const io = makeIo();
|
||||
const ingest = vi.fn(async () => 0);
|
||||
const publicIngest = vi.fn(async () => 0);
|
||||
|
||||
await expect(runKtxCli(argv, io.io, { ingest })).resolves.toBe(1);
|
||||
await expect(runKtxCli(argv, io.io, { publicIngest })).resolves.toBe(1);
|
||||
|
||||
expect(ingest).not.toHaveBeenCalled();
|
||||
expect(publicIngest).not.toHaveBeenCalled();
|
||||
expect(io.stderr()).toMatch(/unknown command|error:/);
|
||||
});
|
||||
|
||||
it('dispatches top-level ingest run through the low-level ingest Commander registration', async () => {
|
||||
it('rejects top-level ingest run through the removed low-level ingest registration', async () => {
|
||||
const io = makeIo();
|
||||
const ingest = vi.fn(async () => 0);
|
||||
const publicIngest = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKtxCli(
|
||||
|
|
@ -203,24 +199,11 @@ describe('dev Commander tree', () => {
|
|||
'--json',
|
||||
],
|
||||
io.io,
|
||||
{ ingest },
|
||||
{ publicIngest },
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(ingest).toHaveBeenCalledWith(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir: '/tmp/project',
|
||||
connectionId: 'warehouse',
|
||||
adapter: 'metabase',
|
||||
sourceDir: undefined,
|
||||
databaseIntrospectionUrl: undefined,
|
||||
cliVersion: '0.0.0-private',
|
||||
runtimeInstallPolicy: 'prompt',
|
||||
outputMode: 'json',
|
||||
},
|
||||
io.io,
|
||||
);
|
||||
expect(io.stderr()).toBe('');
|
||||
expect(publicIngest).not.toHaveBeenCalled();
|
||||
expect(io.stderr()).toMatch(/unknown command|error:/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ profileMark('module:dev');
|
|||
|
||||
export function registerDevCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
const dev = program
|
||||
.command('dev', { hidden: true })
|
||||
.command('dev')
|
||||
.description('Low-level project initialization and runtime management')
|
||||
.showHelpAfterError();
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ describe('standalone local warehouse example', () => {
|
|||
|
||||
it('runs local CLI commands against the copied example project', async () => {
|
||||
const projectDir = await copyExampleProject(tempDir);
|
||||
const sourceDir = join(projectDir, 'source');
|
||||
|
||||
const knowledgeList = await runBuiltCli(['wiki', 'search', 'revenue', '--json', '--project-dir', projectDir]);
|
||||
expect(knowledgeList).toMatchObject({ code: 0, stderr: '' });
|
||||
|
|
@ -105,19 +104,13 @@ describe('standalone local warehouse example', () => {
|
|||
const ingest = await runBuiltCli([
|
||||
'ingest',
|
||||
'run',
|
||||
'--project-dir',
|
||||
projectDir,
|
||||
'--connection-id',
|
||||
'warehouse',
|
||||
'--adapter',
|
||||
'fake',
|
||||
'--source-dir',
|
||||
sourceDir,
|
||||
]);
|
||||
expect(ingest).toMatchObject({ code: 1, stdout: '' });
|
||||
expect(ingest.stderr).toContain(
|
||||
'ktx ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner',
|
||||
);
|
||||
expect(ingest.stderr).toContain("unknown command 'run'");
|
||||
}, 30_000);
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ describe('runKtxCli', () => {
|
|||
|
||||
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
|
||||
expect(testIo.stdout()).toContain('KTX data agent context layer CLI');
|
||||
for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'status']) {
|
||||
for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'status', 'dev']) {
|
||||
expect(testIo.stdout()).toContain(`${command}`);
|
||||
}
|
||||
expect(testIo.stdout()).not.toMatch(/^ scan\s/m);
|
||||
|
|
@ -135,71 +135,60 @@ describe('runKtxCli', () => {
|
|||
expect(testIo.stdout()).toContain('KTX_PROJECT_DIR');
|
||||
expect(testIo.stdout()).toContain('--debug');
|
||||
expect(testIo.stdout()).not.toContain('--' + 'verbose');
|
||||
expect(testIo.stdout()).toContain('Advanced:');
|
||||
expect(testIo.stdout()).toContain('ktx dev');
|
||||
expect(testIo.stdout()).not.toContain('Advanced:');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('routes public wiki read and write commands', async () => {
|
||||
it('routes supported public wiki commands', async () => {
|
||||
const knowledge = vi.fn(async () => 0);
|
||||
|
||||
const readIo = makeIo();
|
||||
await expect(runKtxCli(['--project-dir', tempDir, 'wiki', 'read', 'revenue', '--json'], readIo.io, { knowledge }))
|
||||
const listIo = makeIo();
|
||||
await expect(runKtxCli(['--project-dir', tempDir, 'wiki', 'list', '--json'], listIo.io, { knowledge }))
|
||||
.resolves.toBe(0);
|
||||
expect(knowledge).toHaveBeenCalledWith(
|
||||
{
|
||||
command: 'read',
|
||||
command: 'list',
|
||||
projectDir: tempDir,
|
||||
key: 'revenue',
|
||||
userId: 'local',
|
||||
json: true,
|
||||
},
|
||||
readIo.io,
|
||||
listIo.io,
|
||||
);
|
||||
|
||||
const writeIo = makeIo();
|
||||
const searchIo = makeIo();
|
||||
await expect(
|
||||
runKtxCli(
|
||||
[
|
||||
'--project-dir',
|
||||
tempDir,
|
||||
'wiki',
|
||||
'write',
|
||||
'revenue',
|
||||
'--scope',
|
||||
'user',
|
||||
'--summary',
|
||||
'Revenue',
|
||||
'--content',
|
||||
'Revenue.',
|
||||
'--tag',
|
||||
'finance',
|
||||
'--ref',
|
||||
'https://example.com/revenue',
|
||||
'--sl-ref',
|
||||
'orders',
|
||||
],
|
||||
writeIo.io,
|
||||
{ knowledge },
|
||||
),
|
||||
runKtxCli(['--project-dir', tempDir, 'wiki', 'search', 'revenue', '--limit', '5'], searchIo.io, { knowledge }),
|
||||
).resolves.toBe(0);
|
||||
expect(knowledge).toHaveBeenLastCalledWith(
|
||||
{
|
||||
command: 'write',
|
||||
command: 'search',
|
||||
projectDir: tempDir,
|
||||
key: 'revenue',
|
||||
scope: 'USER',
|
||||
query: 'revenue',
|
||||
userId: 'local',
|
||||
summary: 'Revenue',
|
||||
content: 'Revenue.',
|
||||
tags: ['finance'],
|
||||
refs: ['https://example.com/revenue'],
|
||||
slRefs: ['orders'],
|
||||
json: false,
|
||||
limit: 5,
|
||||
},
|
||||
writeIo.io,
|
||||
searchIo.io,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects removed public wiki read and write commands', async () => {
|
||||
const knowledge = vi.fn(async () => 0);
|
||||
|
||||
for (const argv of [
|
||||
['--project-dir', tempDir, 'wiki', 'read', 'revenue', '--json'],
|
||||
['--project-dir', tempDir, 'wiki', 'write', 'revenue', '--summary', 'Revenue', '--content', 'Revenue.'],
|
||||
]) {
|
||||
const io = makeIo();
|
||||
|
||||
await expect(runKtxCli(argv, io.io, { knowledge })).resolves.toBe(1);
|
||||
|
||||
expect(io.stderr()).toMatch(/unknown command|error:/);
|
||||
}
|
||||
|
||||
expect(knowledge).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects removed public sl read/write commands', async () => {
|
||||
const sl = vi.fn(async () => 0);
|
||||
|
||||
|
|
@ -334,23 +323,15 @@ describe('runKtxCli', () => {
|
|||
expect(testIo.stderr()).toBe(`Project: ${tempDir}\n`);
|
||||
});
|
||||
|
||||
it('skips the project directory line for JSON and TUI output modes', async () => {
|
||||
const ingest = vi.fn(async () => 0);
|
||||
it('skips the project directory line for JSON output mode', async () => {
|
||||
const publicIngest = vi.fn(async () => 0);
|
||||
const jsonIo = makeIo();
|
||||
const vizIo = makeIo({ stdoutIsTty: true });
|
||||
|
||||
await expect(runKtxCli(['--project-dir', tempDir, 'ingest', 'status', 'run-1', '--json'], jsonIo.io, { ingest }))
|
||||
.resolves.toBe(0);
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'ingest', 'status', 'run-1', '--viz'],
|
||||
vizIo.io,
|
||||
{ ingest },
|
||||
),
|
||||
runKtxCli(['--project-dir', tempDir, 'ingest', 'warehouse', '--json'], jsonIo.io, { publicIngest }),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(jsonIo.stderr()).toBe('');
|
||||
expect(vizIo.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('documents runtime stop all in command help', async () => {
|
||||
|
|
@ -692,60 +673,24 @@ describe('runKtxCli', () => {
|
|||
expect(testIo.stderr()).toMatch(/option '--(deep|fast)' cannot be used with option '--(fast|deep)'/);
|
||||
});
|
||||
|
||||
it('prints ingest watch help from Commander', async () => {
|
||||
it.each([
|
||||
{ argv: ['ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'] },
|
||||
{ argv: ['ingest', 'run', '--help'] },
|
||||
{ argv: ['ingest', 'status'] },
|
||||
{ argv: ['ingest', 'status', 'run-1', '--json', '--no-input'] },
|
||||
{ argv: ['ingest', 'watch'] },
|
||||
{ argv: ['ingest', 'watch', '--help'] },
|
||||
{ argv: ['ingest', 'replay', 'run-1'] },
|
||||
{ argv: ['ingest', 'replay', '--help'] },
|
||||
{ argv: ['--project-dir', '/tmp/project', 'ingest', 'status', 'run-1'] },
|
||||
])('rejects removed ingest subcommand $argv', async ({ argv }) => {
|
||||
const testIo = makeIo();
|
||||
const ingest = vi.fn(async () => 0);
|
||||
const publicIngest = vi.fn(async () => 0);
|
||||
|
||||
await expect(runKtxCli(['ingest', 'watch', '--help'], testIo.io, { ingest })).resolves.toBe(0);
|
||||
await expect(runKtxCli(argv, testIo.io, { publicIngest })).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: ktx ingest watch [options] [runId]');
|
||||
expect(testIo.stdout()).toContain('[runId]');
|
||||
expect(testIo.stdout()).toContain('--project-dir <path>');
|
||||
expect(testIo.stdout()).toContain('--json');
|
||||
expect(testIo.stdout()).toContain('--no-input');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
expect(ingest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('dispatches ingest status and watch through Commander', async () => {
|
||||
const statusIo = makeIo();
|
||||
const watchIo = makeIo();
|
||||
const ingest = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKtxCli(['--project-dir', tempDir, 'ingest', 'status', 'run-1', '--json', '--no-input'], statusIo.io, {
|
||||
ingest,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKtxCli(['--project-dir', tempDir, 'ingest', 'watch', '--no-input'], watchIo.io, {
|
||||
ingest,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(ingest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{
|
||||
command: 'status',
|
||||
projectDir: tempDir,
|
||||
runId: 'run-1',
|
||||
outputMode: 'json',
|
||||
inputMode: 'disabled',
|
||||
},
|
||||
statusIo.io,
|
||||
);
|
||||
expect(ingest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{
|
||||
command: 'watch',
|
||||
projectDir: tempDir,
|
||||
outputMode: 'viz',
|
||||
inputMode: 'disabled',
|
||||
},
|
||||
watchIo.io,
|
||||
);
|
||||
expect(statusIo.stderr()).toBe('');
|
||||
expect(watchIo.stderr()).toBe('');
|
||||
expect(testIo.stderr()).toMatch(/unknown command|error:/);
|
||||
expect(publicIngest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects standalone demo commands', async () => {
|
||||
|
|
@ -781,9 +726,9 @@ describe('runKtxCli', () => {
|
|||
|
||||
it('prints ingest help without invoking ingest execution', async () => {
|
||||
const testIo = makeIo();
|
||||
const ingest = vi.fn();
|
||||
const publicIngest = vi.fn();
|
||||
|
||||
await expect(runKtxCli(['ingest', '--help'], testIo.io, { ingest })).resolves.toBe(0);
|
||||
await expect(runKtxCli(['ingest', '--help'], testIo.io, { publicIngest })).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: ktx ingest [options] [connectionId]');
|
||||
expect(testIo.stdout()).toContain('Build or inspect KTX context');
|
||||
|
|
@ -793,37 +738,36 @@ describe('runKtxCli', () => {
|
|||
expect(testIo.stdout()).toContain('--query-history');
|
||||
expect(testIo.stdout()).toContain('--no-query-history');
|
||||
expect(testIo.stdout()).toContain('--query-history-window-days <days>');
|
||||
expect(testIo.stdout()).toContain('status');
|
||||
expect(testIo.stdout()).toContain('replay');
|
||||
expect(testIo.stdout()).not.toMatch(/^ status\s/m);
|
||||
expect(testIo.stdout()).not.toMatch(/^ replay\s/m);
|
||||
expect(testIo.stdout()).not.toMatch(/^ run\s/m);
|
||||
expect(testIo.stdout()).not.toMatch(/^ watch\s/m);
|
||||
expect(testIo.stderr()).toBe('');
|
||||
expect(ingest).not.toHaveBeenCalled();
|
||||
expect(publicIngest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routes ingest run at the top level and rejects removed dev ingest', async () => {
|
||||
const runIo = makeIo();
|
||||
it('rejects removed ingest run at the top level and under dev', async () => {
|
||||
const rootRunIo = makeIo();
|
||||
const devRunIo = makeIo();
|
||||
const ingest = vi.fn(async () => 0);
|
||||
const publicIngest = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKtxCli(['ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], runIo.io, { ingest }),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKtxCli(['dev', 'ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], devRunIo.io, {
|
||||
ingest,
|
||||
runKtxCli(['ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], rootRunIo.io, {
|
||||
publicIngest,
|
||||
}),
|
||||
).resolves.toBe(1);
|
||||
expect(ingest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ command: 'run', connectionId: 'warehouse', adapter: 'metabase' }),
|
||||
expect.anything(),
|
||||
);
|
||||
await expect(
|
||||
runKtxCli(['dev', 'ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], devRunIo.io, {
|
||||
publicIngest,
|
||||
}),
|
||||
).resolves.toBe(1);
|
||||
expect(publicIngest).not.toHaveBeenCalled();
|
||||
expect(rootRunIo.stderr()).toMatch(/unknown command|error:/);
|
||||
expect(devRunIo.stderr()).toMatch(/unknown command|error:/);
|
||||
});
|
||||
|
||||
it('rejects removed dev doctor while keeping ingest parser cases at the root', async () => {
|
||||
it('rejects removed dev doctor and removed ingest parser cases', async () => {
|
||||
const doctor = vi.fn(async () => 0);
|
||||
const ingest = vi.fn(async () => 0);
|
||||
const doctorIo = makeIo();
|
||||
const ingestRunIo = makeIo();
|
||||
const ingestReplayHelpIo = makeIo();
|
||||
|
|
@ -848,94 +792,15 @@ describe('runKtxCli', () => {
|
|||
'--no-input',
|
||||
],
|
||||
ingestRunIo.io,
|
||||
{ ingest },
|
||||
{},
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
await expect(runKtxCli(['ingest', 'replay', '--help'], ingestReplayHelpIo.io, { ingest })).resolves.toBe(0);
|
||||
).resolves.toBe(1);
|
||||
await expect(runKtxCli(['ingest', 'replay', '--help'], ingestReplayHelpIo.io)).resolves.toBe(1);
|
||||
|
||||
expect(doctor).not.toHaveBeenCalled();
|
||||
expect(ingest).toHaveBeenCalledWith(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir: tempDir,
|
||||
connectionId: 'warehouse',
|
||||
adapter: 'fake',
|
||||
sourceDir: tempDir,
|
||||
databaseIntrospectionUrl: undefined,
|
||||
cliVersion: '0.0.0-private',
|
||||
runtimeInstallPolicy: 'prompt',
|
||||
debugLlmRequestFile: `${tempDir}/debug.jsonl`,
|
||||
outputMode: 'json',
|
||||
inputMode: 'disabled',
|
||||
},
|
||||
ingestRunIo.io,
|
||||
);
|
||||
expect(ingestReplayHelpIo.stdout()).toContain('Usage: ktx ingest replay [options] <runId>');
|
||||
expect(ingestReplayHelpIo.stdout()).toContain('<runId>');
|
||||
expect(doctorIo.stderr()).toMatch(/unknown command|error:/);
|
||||
expect(ingestRunIo.stderr()).toBe('');
|
||||
expect(ingestReplayHelpIo.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('routes ingest managed runtime install policy separately from visualization input mode', async () => {
|
||||
const autoIo = makeIo();
|
||||
const nonInteractiveIo = makeIo();
|
||||
const ingest = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKtxCli(
|
||||
[
|
||||
'ingest',
|
||||
'run',
|
||||
'--project-dir',
|
||||
tempDir,
|
||||
'--connection-id',
|
||||
'warehouse',
|
||||
'--adapter',
|
||||
'looker',
|
||||
'--yes',
|
||||
],
|
||||
autoIo.io,
|
||||
{ ingest },
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKtxCli(
|
||||
[
|
||||
'ingest',
|
||||
'run',
|
||||
'--project-dir',
|
||||
tempDir,
|
||||
'--connection-id',
|
||||
'warehouse',
|
||||
'--adapter',
|
||||
'looker',
|
||||
'--yes',
|
||||
'--no-input',
|
||||
],
|
||||
nonInteractiveIo.io,
|
||||
{ ingest },
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(ingest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
command: 'run',
|
||||
cliVersion: '0.0.0-private',
|
||||
runtimeInstallPolicy: 'auto',
|
||||
}),
|
||||
autoIo.io,
|
||||
);
|
||||
expect(ingest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
command: 'run',
|
||||
cliVersion: '0.0.0-private',
|
||||
runtimeInstallPolicy: 'auto',
|
||||
inputMode: 'disabled',
|
||||
}),
|
||||
nonInteractiveIo.io,
|
||||
);
|
||||
expect(nonInteractiveIo.stderr()).toBe(`Project: ${tempDir}\n`);
|
||||
expect(ingestRunIo.stderr()).toMatch(/unknown command|error:/);
|
||||
expect(ingestReplayHelpIo.stderr()).toMatch(/unknown command|error:/);
|
||||
});
|
||||
|
||||
it('dispatches public connection through the existing connection implementation', async () => {
|
||||
|
|
@ -1199,7 +1064,9 @@ describe('runKtxCli', () => {
|
|||
).resolves.toBe(1);
|
||||
|
||||
expect(setup).not.toHaveBeenCalled();
|
||||
expect(testIo.stderr()).toContain('"status" is reserved for ktx ingest status; choose a different connection id.');
|
||||
expect(testIo.stderr()).toContain(
|
||||
'"status" is reserved for the KTX ingest command namespace; choose a different connection id.',
|
||||
);
|
||||
});
|
||||
|
||||
it('dispatches setup source flags', async () => {
|
||||
|
|
@ -1573,12 +1440,12 @@ describe('runKtxCli', () => {
|
|||
{ argv: ['scan', 'warehouse', '--mode', 'relationships'] },
|
||||
])('rejects removed top-level scan command $argv', async ({ argv }) => {
|
||||
const testIo = makeIo();
|
||||
const ingest = vi.fn().mockResolvedValue(0);
|
||||
const publicIngest = vi.fn().mockResolvedValue(0);
|
||||
|
||||
await expect(runKtxCli(argv, testIo.io, { ingest })).resolves.toBe(1);
|
||||
await expect(runKtxCli(argv, testIo.io, { publicIngest })).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stderr()).toMatch(/unknown command|error:/);
|
||||
expect(ingest).not.toHaveBeenCalled();
|
||||
expect(publicIngest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects removed public serve command options before dispatch', async () => {
|
||||
|
|
@ -1626,13 +1493,13 @@ describe('runKtxCli', () => {
|
|||
it('rejects removed dev command groups without invoking execution', async () => {
|
||||
for (const command of ['scan', 'ingest', 'mapping']) {
|
||||
const testIo = makeIo();
|
||||
const ingest = vi.fn().mockResolvedValue(0);
|
||||
const publicIngest = vi.fn().mockResolvedValue(0);
|
||||
const sl = vi.fn().mockResolvedValue(0);
|
||||
|
||||
await expect(runKtxCli(['dev', command], testIo.io, { ingest, sl })).resolves.toBe(1);
|
||||
await expect(runKtxCli(['dev', command], testIo.io, { publicIngest, sl })).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stderr()).toMatch(/unknown command|error:/);
|
||||
expect(ingest).not.toHaveBeenCalled();
|
||||
expect(publicIngest).not.toHaveBeenCalled();
|
||||
expect(sl).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
|
@ -1645,19 +1512,16 @@ describe('runKtxCli', () => {
|
|||
expect(testIo.stderr()).toMatch(/unknown command|error:/);
|
||||
});
|
||||
|
||||
it('rejects mutually exclusive output modes before invoking runners', async () => {
|
||||
const ingest = vi.fn(async () => 0);
|
||||
it('rejects mutually exclusive public ingest output modes before invoking runners', async () => {
|
||||
const publicIngest = vi.fn(async () => 0);
|
||||
|
||||
for (const argv of [
|
||||
['ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'fake', '--json', '--plain'],
|
||||
['ingest', 'status', 'run-1', '--json', '--viz'],
|
||||
]) {
|
||||
const testIo = makeIo();
|
||||
await expect(runKtxCli(argv, testIo.io, { ingest })).resolves.toBe(1);
|
||||
expect(testIo.stderr()).toMatch(/conflict|cannot be used/i);
|
||||
}
|
||||
const testIo = makeIo();
|
||||
await expect(runKtxCli(['ingest', 'warehouse', '--json', '--plain'], testIo.io, { publicIngest })).resolves.toBe(
|
||||
1,
|
||||
);
|
||||
|
||||
expect(ingest).not.toHaveBeenCalled();
|
||||
expect(testIo.stderr()).toMatch(/conflict|cannot be used/i);
|
||||
expect(publicIngest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not expose root init after setup owns project creation', async () => {
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ describe('runKtxIngest', () => {
|
|||
|
||||
expect(runIo.stdout()).toBe('');
|
||||
expect(runIo.stderr()).toContain(
|
||||
'ktx ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.',
|
||||
'ktx ingest requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.',
|
||||
);
|
||||
expect(runIo.stderr()).toContain(
|
||||
`ktx setup --project-dir ${projectDir} --anthropic-api-key-env ANTHROPIC_API_KEY --anthropic-model claude-sonnet-4-6 --no-input`,
|
||||
|
|
@ -854,7 +854,7 @@ describe('runKtxIngest', () => {
|
|||
).resolves.toBe(1);
|
||||
|
||||
expect(io.stderr()).toContain('source-dir uploads are not supported for the Metabase fan-out adapter');
|
||||
expect(io.stderr()).not.toContain('ktx ingest run requires llm.provider.backend');
|
||||
expect(io.stderr()).not.toContain('ktx ingest requires llm.provider.backend');
|
||||
expect(io.stdout()).toBe('');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import { profileMark } from './startup-profile.js';
|
|||
|
||||
profileMark('module:ingest');
|
||||
|
||||
export type KtxIngestOutputMode = 'plain' | 'json' | 'viz';
|
||||
type KtxIngestOutputMode = 'plain' | 'json' | 'viz';
|
||||
type KtxIngestInputMode = 'auto' | 'disabled';
|
||||
|
||||
export type KtxIngestArgs =
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ describe('runKtxKnowledge', () => {
|
|||
|
||||
expect(searchIo.stdout()).toBe('');
|
||||
expect(searchIo.stderr()).toContain('No local wiki pages found');
|
||||
expect(searchIo.stderr()).toContain('ktx wiki write');
|
||||
expect(searchIo.stderr()).toContain('ktx ingest <connectionId>');
|
||||
});
|
||||
|
||||
it('uses configured embeddings for semantic wiki search', async () => {
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ export async function runKtxKnowledge(
|
|||
const pages = await listLocalKnowledgePages(project, { userId: args.userId });
|
||||
if (pages.length === 0) {
|
||||
io.stderr.write(
|
||||
`No local wiki pages found in ${project.projectDir}. Create one with \`ktx wiki write <key> --summary <summary> --content <content>\` or run ingest.\n`,
|
||||
`No local wiki pages found in ${project.projectDir}. Add Markdown files under wiki/ or run \`ktx ingest <connectionId>\`.\n`,
|
||||
);
|
||||
} else {
|
||||
io.stderr.write(
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ describe('renderKtxCommandTree', () => {
|
|||
.filter((line) => /^ {2}[├└]── \S/.test(line))
|
||||
.map((line) => line.replace(/^ {2}[├└]── /, '').trim().split(' ')[0]);
|
||||
|
||||
for (const expected of ['setup', 'connection', 'ingest', 'sl']) {
|
||||
for (const expected of ['setup', 'connection', 'ingest', 'sl', 'dev']) {
|
||||
expect(topLevel).toContain(expected);
|
||||
}
|
||||
|
||||
|
|
@ -24,9 +24,15 @@ describe('renderKtxCommandTree', () => {
|
|||
expect(output).not.toContain('│ ├── metabase');
|
||||
expect(output).not.toContain('│ ├── notion');
|
||||
expect(output).not.toContain('scan <connectionId>');
|
||||
expect(output).not.toContain('│ ├── status');
|
||||
expect(output).not.toContain('│ ├── replay');
|
||||
expect(output).not.toContain('│ └── replay');
|
||||
expect(output).not.toContain('│ ├── run');
|
||||
expect(output).not.toContain('│ ├── watch');
|
||||
expect(output).not.toContain('│ └── watch');
|
||||
expect(output).not.toContain('│ ├── read');
|
||||
expect(output).not.toContain('│ ├── write');
|
||||
expect(output).not.toContain('│ └── write');
|
||||
});
|
||||
|
||||
it('ends with a single trailing newline', () => {
|
||||
|
|
|
|||
|
|
@ -32,10 +32,9 @@ describe('project directory defaults', () => {
|
|||
|
||||
const connection = vi.fn(async () => 0);
|
||||
const doctor = vi.fn(async () => 0);
|
||||
const ingest = vi.fn(async () => 0);
|
||||
const publicIngest = vi.fn(async () => 0);
|
||||
const setup = vi.fn(async () => 0);
|
||||
const deps: KtxCliDeps = { connection, doctor, ingest, publicIngest, setup };
|
||||
const deps: KtxCliDeps = { connection, doctor, publicIngest, setup };
|
||||
|
||||
const cases: Array<{
|
||||
argv: string[];
|
||||
|
|
@ -55,12 +54,6 @@ describe('project directory defaults', () => {
|
|||
expected: { command: 'project', projectDir: '/tmp/ktx-env-project' },
|
||||
expectedStderr: 'Project: /tmp/ktx-env-project\n',
|
||||
},
|
||||
{
|
||||
argv: ['ingest', 'status', 'run-1'],
|
||||
spy: ingest,
|
||||
expected: { command: 'status', projectDir: '/tmp/ktx-env-project', runId: 'run-1', outputMode: 'plain' },
|
||||
expectedStderr: 'Project: /tmp/ktx-env-project\n',
|
||||
},
|
||||
{
|
||||
argv: ['setup', '--no-input'],
|
||||
spy: setup,
|
||||
|
|
@ -87,31 +80,32 @@ describe('project directory defaults', () => {
|
|||
process.env.KTX_PROJECT_DIR = '/tmp/ktx-env-project';
|
||||
|
||||
const publicIngest = vi.fn(async () => 0);
|
||||
const ingest = vi.fn(async () => 0);
|
||||
const publicIngestIo = makeIo();
|
||||
const ingestIo = makeIo();
|
||||
const beforeCommandIo = makeIo();
|
||||
const afterCommandIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxCli(['--project-dir', '/tmp/ktx-explicit-project', 'ingest', 'warehouse', '--no-input'], publicIngestIo.io, {
|
||||
runKtxCli(['--project-dir', '/tmp/ktx-explicit-project', 'ingest', 'warehouse', '--no-input'], beforeCommandIo.io, {
|
||||
publicIngest,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKtxCli(['ingest', 'status', 'run-1', '--project-dir=/tmp/ktx-explicit-project'], ingestIo.io, {
|
||||
ingest,
|
||||
runKtxCli(['ingest', 'warehouse', '--project-dir=/tmp/ktx-explicit-project', '--no-input'], afterCommandIo.io, {
|
||||
publicIngest,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(publicIngest).toHaveBeenCalledWith(
|
||||
expect(publicIngest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ command: 'run', projectDir: '/tmp/ktx-explicit-project' }),
|
||||
publicIngestIo.io,
|
||||
beforeCommandIo.io,
|
||||
);
|
||||
expect(ingest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ command: 'status', projectDir: '/tmp/ktx-explicit-project' }),
|
||||
ingestIo.io,
|
||||
expect(publicIngest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ command: 'run', projectDir: '/tmp/ktx-explicit-project' }),
|
||||
afterCommandIo.io,
|
||||
);
|
||||
expect(publicIngestIo.stderr()).toBe('Project: /tmp/ktx-explicit-project\n');
|
||||
expect(ingestIo.stderr()).toBe('Project: /tmp/ktx-explicit-project\n');
|
||||
expect(beforeCommandIo.stderr()).toBe('Project: /tmp/ktx-explicit-project\n');
|
||||
expect(afterCommandIo.stderr()).toBe('Project: /tmp/ktx-explicit-project\n');
|
||||
});
|
||||
|
||||
it('uses nearest ancestor containing ktx.yaml when no explicit or environment project-dir exists', async () => {
|
||||
|
|
|
|||
|
|
@ -984,46 +984,4 @@ describe('runKtxPublicIngest', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('routes public status and watch to the ingest status renderer', async () => {
|
||||
const runIngest = vi.fn(async () => 0);
|
||||
const statusIo = makeIo();
|
||||
const watchIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxPublicIngest(
|
||||
{ command: 'status', projectDir: '/tmp/ktx', json: false, inputMode: 'disabled' },
|
||||
statusIo.io,
|
||||
{ runIngest },
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKtxPublicIngest(
|
||||
{ command: 'watch', projectDir: '/tmp/ktx', runId: 'run-1', json: false, inputMode: 'auto' },
|
||||
watchIo.io,
|
||||
{ runIngest },
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(runIngest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{
|
||||
command: 'status',
|
||||
projectDir: '/tmp/ktx',
|
||||
outputMode: 'plain',
|
||||
inputMode: 'disabled',
|
||||
},
|
||||
statusIo.io,
|
||||
);
|
||||
expect(runIngest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{
|
||||
command: 'watch',
|
||||
projectDir: '/tmp/ktx',
|
||||
runId: 'run-1',
|
||||
outputMode: 'viz',
|
||||
inputMode: 'auto',
|
||||
},
|
||||
watchIo.io,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,26 +23,19 @@ type KtxPublicIngestQueryHistoryFlag = 'default' | 'enabled' | 'disabled';
|
|||
type HistoricSqlDialect = 'postgres' | 'bigquery' | 'snowflake';
|
||||
|
||||
export type KtxPublicIngestArgs =
|
||||
| {
|
||||
command: 'run';
|
||||
projectDir: string;
|
||||
targetConnectionId?: string;
|
||||
all: boolean;
|
||||
json: boolean;
|
||||
inputMode: KtxPublicIngestInputMode;
|
||||
depth?: KtxPublicIngestDepth;
|
||||
queryHistory?: KtxPublicIngestQueryHistoryFlag;
|
||||
queryHistoryWindowDays?: number;
|
||||
scanMode?: Extract<KtxScanArgs, { command: 'run' }>['mode'];
|
||||
detectRelationships?: boolean;
|
||||
}
|
||||
| {
|
||||
command: 'status' | 'watch';
|
||||
projectDir: string;
|
||||
runId?: string;
|
||||
json: boolean;
|
||||
inputMode: KtxPublicIngestInputMode;
|
||||
};
|
||||
{
|
||||
command: 'run';
|
||||
projectDir: string;
|
||||
targetConnectionId?: string;
|
||||
all: boolean;
|
||||
json: boolean;
|
||||
inputMode: KtxPublicIngestInputMode;
|
||||
depth?: KtxPublicIngestDepth;
|
||||
queryHistory?: KtxPublicIngestQueryHistoryFlag;
|
||||
queryHistoryWindowDays?: number;
|
||||
scanMode?: Extract<KtxScanArgs, { command: 'run' }>['mode'];
|
||||
detectRelationships?: boolean;
|
||||
};
|
||||
|
||||
export interface KtxPublicIngestPlanTarget {
|
||||
connectionId: string;
|
||||
|
|
@ -775,20 +768,6 @@ export async function runKtxPublicIngest(
|
|||
io: KtxCliIo,
|
||||
deps: KtxPublicIngestDeps = {},
|
||||
): Promise<number> {
|
||||
if (args.command !== 'run') {
|
||||
const { runKtxIngest } = await import('./ingest.js');
|
||||
return await (deps.runIngest ?? runKtxIngest)(
|
||||
{
|
||||
command: args.command,
|
||||
projectDir: args.projectDir,
|
||||
...(args.runId ? { runId: args.runId } : {}),
|
||||
outputMode: args.json ? 'json' : args.command === 'watch' ? 'viz' : 'plain',
|
||||
inputMode: args.inputMode,
|
||||
},
|
||||
io,
|
||||
);
|
||||
}
|
||||
|
||||
const loadProject = deps.loadProject ?? loadKtxProject;
|
||||
const project = await loadProject({ projectDir: args.projectDir });
|
||||
if (shouldUseForegroundContextBuildView(args, io)) {
|
||||
|
|
|
|||
|
|
@ -1839,7 +1839,9 @@ describe('setup databases step', () => {
|
|||
);
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(io.stderr()).toContain('"replay" is reserved for ktx ingest replay; choose a different connection id.');
|
||||
expect(io.stderr()).toContain(
|
||||
'"replay" is reserved for the KTX ingest command namespace; choose a different connection id.',
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves setup incomplete when databases are skipped', async () => {
|
||||
|
|
|
|||
|
|
@ -276,7 +276,9 @@ describe('setup sources step', () => {
|
|||
);
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(io.stderr()).toContain('"status" is reserved for ktx ingest status; choose a different connection id.');
|
||||
expect(io.stderr()).toContain(
|
||||
'"status" is reserved for the KTX ingest command namespace; choose a different connection id.',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses selected Notion roots when root page ids are provided even if crawl mode says all accessible', async () => {
|
||||
|
|
|
|||
|
|
@ -50,28 +50,6 @@ async function runBuiltCli(args: string[], options: { env?: NodeJS.ProcessEnv }
|
|||
}
|
||||
}
|
||||
|
||||
async function writeWarehouseConfig(projectDir: string): Promise<void> {
|
||||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
'ingest:',
|
||||
' adapters:',
|
||||
' - fake',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
|
||||
async function writeSourceFixture(sourceDir: string): Promise<void> {
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
await writeFile(join(sourceDir, 'orders', 'orders.json'), '{"name":"orders"}\n', 'utf-8');
|
||||
}
|
||||
|
||||
function createSqliteWarehouse(dbPath: string): void {
|
||||
const db = new Database(dbPath);
|
||||
try {
|
||||
|
|
@ -157,33 +135,23 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('reports missing local ingest LLM config through the built binary', async () => {
|
||||
it('rejects removed low-level ingest run through the built binary', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
|
||||
const init = await runSetupNewProject(projectDir);
|
||||
expectProjectStderr(init, projectDir);
|
||||
expect(init.stdout).toContain(`Project: ${projectDir}`);
|
||||
|
||||
await writeWarehouseConfig(projectDir);
|
||||
await writeSourceFixture(sourceDir);
|
||||
|
||||
const run = await runBuiltCli([
|
||||
'ingest',
|
||||
'run',
|
||||
'--project-dir',
|
||||
projectDir,
|
||||
'--connection-id',
|
||||
'warehouse',
|
||||
'--adapter',
|
||||
'fake',
|
||||
'--source-dir',
|
||||
sourceDir,
|
||||
]);
|
||||
expect(run).toMatchObject({ code: 1, stdout: '' });
|
||||
expect(run.stderr).toContain(
|
||||
'ktx ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner',
|
||||
);
|
||||
expect(run.stderr).toContain("unknown command 'run'");
|
||||
});
|
||||
|
||||
it('rejects the removed agent command through the built binary', async () => {
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ export async function localPullConfigForAdapter(
|
|||
): Promise<unknown> {
|
||||
if (adapter.source === 'metabase') {
|
||||
throw new Error(
|
||||
'Metabase scheduled pulls fan out by mapping. Call runLocalMetabaseIngest() or use `ktx ingest run --adapter metabase --connection-id <metabase-source-id>` from the CLI.',
|
||||
'Metabase scheduled pulls fan out by mapping. Call runLocalMetabaseIngest() or use `ktx ingest <metabase-source-id>` from the CLI.',
|
||||
);
|
||||
}
|
||||
const connection = project.config.connections[connectionId];
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ describe('createLocalBundleIngestRuntime', () => {
|
|||
}),
|
||||
).toThrow(
|
||||
[
|
||||
'ktx ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.',
|
||||
'ktx ingest requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.',
|
||||
`Configure an Anthropic provider, then rerun ingest:`,
|
||||
` ktx setup --project-dir ${project.projectDir} --anthropic-api-key-env ANTHROPIC_API_KEY --anthropic-model claude-sonnet-4-6 --no-input`,
|
||||
].join('\n'),
|
||||
|
|
|
|||
|
|
@ -571,7 +571,7 @@ function nextLocalJobId(): string {
|
|||
|
||||
function localIngestLlmProviderGuardMessage(projectDir: string): string {
|
||||
return [
|
||||
'ktx ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.',
|
||||
'ktx ingest requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.',
|
||||
'Configure an Anthropic provider, then rerun ingest:',
|
||||
` ktx setup --project-dir ${projectDir} --anthropic-api-key-env ANTHROPIC_API_KEY --anthropic-model claude-sonnet-4-6 --no-input`,
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ connections:
|
|||
${connectionId}:
|
||||
driver: postgres
|
||||
`),
|
||||
).toThrow(`"${connectionId}" is reserved for ktx ingest ${connectionId}`);
|
||||
).toThrow(`"${connectionId}" is reserved for the KTX ingest command namespace`);
|
||||
});
|
||||
|
||||
it('builds the default standalone project config', () => {
|
||||
|
|
|
|||
|
|
@ -113,10 +113,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||
}
|
||||
|
||||
const RESERVED_INGEST_CONNECTION_IDS = new Map([
|
||||
['status', 'ktx ingest status'],
|
||||
['replay', 'ktx ingest replay'],
|
||||
['run', 'ktx ingest run'],
|
||||
['watch', 'ktx ingest watch'],
|
||||
['status', 'the KTX ingest command namespace'],
|
||||
['replay', 'the KTX ingest command namespace'],
|
||||
['run', 'the KTX ingest command namespace'],
|
||||
['watch', 'the KTX ingest command namespace'],
|
||||
]);
|
||||
|
||||
export function reservedKtxIngestConnectionIdMessage(connectionId: string): string | null {
|
||||
|
|
|
|||
|
|
@ -120,6 +120,22 @@ async function writeLiveDatabaseConfig(projectDir: string): Promise<void> {
|
|||
);
|
||||
}
|
||||
|
||||
async function writeDatabaseConfigWithoutIngestAdapters(projectDir: string): Promise<void> {
|
||||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
' readonly: true',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
|
||||
function fetchOnlyAdapter(options: { extractedAt?: () => string } = {}): SourceAdapter {
|
||||
return {
|
||||
source: 'live-database',
|
||||
|
|
@ -244,6 +260,27 @@ describe('local scan', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('runs a structural database scan when live-database is not listed in ktx.yaml', async () => {
|
||||
await writeDatabaseConfigWithoutIngestAdapters(project.projectDir);
|
||||
project = await loadKtxProject({ projectDir: project.projectDir });
|
||||
|
||||
const result = await runLocalScan({
|
||||
project,
|
||||
adapters: [fetchOnlyAdapter()],
|
||||
connectionId: 'warehouse',
|
||||
jobId: 'scan-run-without-public-adapter',
|
||||
now: () => new Date('2026-04-29T09:10:00.000Z'),
|
||||
});
|
||||
|
||||
expect(result.report).toMatchObject({
|
||||
connectionId: 'warehouse',
|
||||
runId: 'scan-run-without-public-adapter',
|
||||
artifactPaths: {
|
||||
reportPath: 'raw-sources/warehouse/live-database/2026-04-29-091000-scan-run-without-public-adapter/scan-report.json',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('reuses scan report and raw-source paths when the same local scan run id is retried', async () => {
|
||||
const first = await runLocalScan({
|
||||
project,
|
||||
|
|
|
|||
|
|
@ -342,6 +342,22 @@ function createFilteredConnector(connector: KtxScanConnector, enabledTables: Set
|
|||
};
|
||||
}
|
||||
|
||||
function withInternalLiveDatabaseAdapter(project: KtxLocalProject): KtxLocalProject {
|
||||
if (project.config.ingest.adapters.includes(LIVE_DATABASE_ADAPTER)) {
|
||||
return project;
|
||||
}
|
||||
return {
|
||||
...project,
|
||||
config: {
|
||||
...project.config,
|
||||
ingest: {
|
||||
...project.config.ingest,
|
||||
adapters: [...project.config.ingest.adapters, LIVE_DATABASE_ADAPTER],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function runLocalScan(options: RunLocalScanOptions): Promise<LocalScanRunResult> {
|
||||
const mode = options.mode ?? 'structural';
|
||||
assertSupportedMode(mode);
|
||||
|
|
@ -367,7 +383,7 @@ export async function runLocalScan(options: RunLocalScanOptions): Promise<LocalS
|
|||
|
||||
await options.progress?.update(0.15, 'Inspecting database schema');
|
||||
const record = await runLocalStageOnlyIngest({
|
||||
project: options.project,
|
||||
project: withInternalLiveDatabaseAdapter(options.project),
|
||||
adapters,
|
||||
adapter: LIVE_DATABASE_ADAPTER,
|
||||
connectionId: options.connectionId,
|
||||
|
|
|
|||
|
|
@ -257,7 +257,6 @@ describe('standalone example docs', () => {
|
|||
assert.match(ingestReference, /--query-history-window-days <days>/);
|
||||
assert.match(buildingContext, /ktx ingest <connection-id>/);
|
||||
assert.match(buildingContext, /ktx ingest --all/);
|
||||
assert.match(buildingContext, /ktx ingest replay <run-id>/);
|
||||
assert.match(contextSources, /ktx ingest <connectionId>/);
|
||||
assert.match(contextAsCode, /ktx ingest --all --no-input/);
|
||||
assert.match(quickstart, /schema context/);
|
||||
|
|
@ -272,11 +271,15 @@ describe('standalone example docs', () => {
|
|||
|
||||
assert.doesNotMatch(cliMeta, /ktx-scan/);
|
||||
assert.doesNotMatch(ingestReference, /ktx ingest run/);
|
||||
assert.doesNotMatch(ingestReference, /ktx ingest status/);
|
||||
assert.doesNotMatch(ingestReference, /ktx ingest replay/);
|
||||
assert.doesNotMatch(ingestReference, /--adapter/);
|
||||
assert.doesNotMatch(ingestReference, /ktx ingest watch/);
|
||||
assert.doesNotMatch(ingestReference, /live-database/);
|
||||
assert.doesNotMatch(devReference, /ktx scan/);
|
||||
assert.doesNotMatch(buildingContext, /ktx ingest watch/);
|
||||
assert.doesNotMatch(buildingContext, /ktx ingest status/);
|
||||
assert.doesNotMatch(buildingContext, /ktx ingest replay/);
|
||||
assert.doesNotMatch(buildingContext, /historic-sql/);
|
||||
assert.doesNotMatch(buildingContext, /live-database/);
|
||||
assert.doesNotMatch(contextSources, /ktx ingest run --connection-id/);
|
||||
|
|
|
|||
|
|
@ -113,10 +113,6 @@ export function buildLiveDatabaseIngestArgs(projectDir, _databaseIntrospectionUr
|
|||
];
|
||||
}
|
||||
|
||||
export function buildLiveDatabaseStatusArgs(projectDir, runId) {
|
||||
return ['exec', 'ktx', 'ingest', 'status', '--project-dir', projectDir, runId];
|
||||
}
|
||||
|
||||
async function run(command, args, options = {}) {
|
||||
process.stdout.write(`$ ${command} ${args.join(' ')}\n`);
|
||||
return new Promise((resolve) => {
|
||||
|
|
@ -167,7 +163,7 @@ function requireOutput(label, result, pattern) {
|
|||
function getRunId(stdout) {
|
||||
const match = stdout.match(/^Run: (.+)$/m);
|
||||
if (!match) {
|
||||
throw new Error(`ingest run output did not include a run id\nstdout:\n${stdout}`);
|
||||
throw new Error(`ingest output did not include a run id\nstdout:\n${stdout}`);
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
|
@ -322,16 +318,6 @@ async function main() {
|
|||
requireOutput('ktx ingest warehouse --fast', ingestRun, /Database schema/);
|
||||
|
||||
const runId = getRunId(ingestRun.stdout);
|
||||
const ingestStatus = await run('pnpm', buildLiveDatabaseStatusArgs(projectDir, runId), {
|
||||
cwd: cleanInstallDir,
|
||||
env: managedRuntimeEnv(cleanInstallDir),
|
||||
timeout: 30_000,
|
||||
});
|
||||
requireSuccess('ktx ingest status live-database', ingestStatus);
|
||||
requireOutput('ktx ingest status live-database', ingestStatus, new RegExp(`Run: ${runId}`));
|
||||
requireOutput('ktx ingest status live-database', ingestStatus, /Status: done/);
|
||||
requireOutput('ktx ingest status live-database', ingestStatus, /Raw files: 4/);
|
||||
requireOutput('ktx ingest status live-database', ingestStatus, /Work units: 2/);
|
||||
await assertPathExists(join(projectDir, '.ktx', 'db.sqlite'), 'SQLite local ingest state');
|
||||
process.stdout.write(`Installed live-database artifact smoke passed: ${runId}\n`);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
buildDockerRunArgs,
|
||||
buildKtxYaml,
|
||||
buildLiveDatabaseIngestArgs,
|
||||
buildLiveDatabaseStatusArgs,
|
||||
buildPostgresUrl,
|
||||
buildPostgresReadyArgs,
|
||||
buildSeedSql,
|
||||
|
|
@ -95,7 +94,7 @@ describe('installed live-database artifact smoke helpers', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('builds installed CLI public database ingest and status commands', () => {
|
||||
it('builds the installed CLI public database ingest command', () => {
|
||||
assert.deepEqual(buildLiveDatabaseIngestArgs('/tmp/project', 'http://127.0.0.1:8765'), [
|
||||
'exec',
|
||||
'ktx',
|
||||
|
|
@ -107,14 +106,5 @@ describe('installed live-database artifact smoke helpers', () => {
|
|||
'--no-input',
|
||||
]);
|
||||
|
||||
assert.deepEqual(buildLiveDatabaseStatusArgs('/tmp/project', 'local-run-1'), [
|
||||
'exec',
|
||||
'ktx',
|
||||
'ingest',
|
||||
'status',
|
||||
'--project-dir',
|
||||
'/tmp/project',
|
||||
'local-run-1',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -561,7 +561,7 @@ function requireIncludes(values, expected, label) {
|
|||
|
||||
function getRunId(stdout) {
|
||||
const match = stdout.match(/^Run: (.+)$/m);
|
||||
assert.ok(match, 'ingest run output did not include a run id');
|
||||
assert.ok(match, 'ingest output did not include a run id');
|
||||
return match[1];
|
||||
}
|
||||
|
||||
|
|
@ -588,7 +588,6 @@ 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);
|
||||
|
|
@ -842,27 +841,8 @@ try {
|
|||
const enrichedScanRunId = getRunId(enrichedScan.stdout);
|
||||
process.stdout.write('ktx ingest deep 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', 'ingest', 'run',
|
||||
'--project-dir',
|
||||
projectDir,
|
||||
'--connection-id',
|
||||
'warehouse',
|
||||
'--adapter',
|
||||
'fake',
|
||||
'--source-dir',
|
||||
sourceDir,
|
||||
]);
|
||||
assert.equal(ingestRun.code, 1, 'ktx ingest run without an LLM provider must fail');
|
||||
assert.match(
|
||||
ingestRun.stderr,
|
||||
/ktx 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 ingest provider guard verified\\n');
|
||||
process.stdout.write('ktx ingest state verified\\n');
|
||||
} finally {
|
||||
if (daemonStarted) {
|
||||
await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'stop']);
|
||||
|
|
|
|||
|
|
@ -495,11 +495,11 @@ describe('verification snippets', () => {
|
|||
assert.match(source, /ktx ingest deep verified/);
|
||||
assert.match(source, /enrichment:/);
|
||||
assert.match(source, /mode: deterministic/);
|
||||
assert.match(source, /run\('pnpm', \['exec', 'ktx', 'ingest', 'run'/);
|
||||
assert.doesNotMatch(source, /run\('pnpm', \['exec', 'ktx', 'ingest', 'run'/);
|
||||
assert.match(source, /access\(join\(projectDir, '\.ktx', 'db\.sqlite'\)\)/);
|
||||
assert.match(source, /SQLite wiki index/);
|
||||
assert.match(source, /ktx ingest run requires llm\\.provider\\.backend: anthropic, vertex, or gateway/);
|
||||
assert.match(source, /ktx ingest provider guard verified/);
|
||||
assert.doesNotMatch(source, /ktx ingest run requires llm\\.provider\\.backend: anthropic, vertex, or gateway/);
|
||||
assert.match(source, /ktx ingest state verified/);
|
||||
});
|
||||
|
||||
describe('npmCliSmokeSource', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue